status-desktop/ui/StatusQ/tests/tst_ObjectProxyModel.cpp

753 lines
24 KiB
C++

#include <QSignalSpy>
#include <QTest>
#include <QJsonArray>
#include <QJsonObject>
#include <QQmlComponent>
#include <QQmlContext>
#include <QQmlEngine>
#include <memory>
#include <string>
#include <StatusQ/objectproxymodel.h>
#include <TestHelpers/listmodelwrapper.h>
#include <TestHelpers/modelsignalsspy.h>
#include <TestHelpers/modeltestutils.h>
class TestObjectProxyModel: 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();
}
private slots:
void basicTest() {
QQmlEngine engine;
QQmlComponent delegate(&engine);
auto delegateData = R"(
import QtQml 2.15
QtObject {
readonly property int count: model.balances.count
readonly property QtObject proxyObject: this
}
)";
delegate.setData(delegateData, QUrl());
ObjectProxyModel model;
auto source = R"([
{ balances: [ { balance: 4 } ], name: "name 1" },
{ balances: [ { balance: 4 }, {balance: 43} ], name: "name 2" },
{ balances: [], name: "name 3" }
])";
ListModelWrapper sourceModel(engine, source);
QSignalSpy sourceModelChangedSpy(
&model, &ObjectProxyModel::sourceModelChanged);
QSignalSpy delegateChangedSpy(
&model, &ObjectProxyModel::delegateChanged);
QSignalSpy expectedRolesChangedSpy(
&model, &ObjectProxyModel::expectedRolesChanged);
QSignalSpy exposedRolesChangedSpy(
&model, &ObjectProxyModel::exposedRolesChanged);
model.setSourceModel(sourceModel);
model.setDelegate(&delegate);
model.setExpectedRoles(QStringList({ QStringLiteral("balances") }));
model.setExposedRoles({ QStringLiteral("proxyObject"),
QStringLiteral("count")});
QCOMPARE(sourceModelChangedSpy.count(), 1);
QCOMPARE(delegateChangedSpy.count(), 1);
QCOMPARE(expectedRolesChangedSpy.count(), 1);
QCOMPARE(model.sourceModel(), sourceModel);
QCOMPARE(model.delegate(), &delegate);
QCOMPARE(model.expectedRoles(), QStringList({ QStringLiteral("balances") }));
QCOMPARE(model.exposedRoles(), QStringList({ QStringLiteral("proxyObject"),
QStringLiteral("count") }));
QCOMPARE(model.rowCount(), 3);
QCOMPARE(model.data(model.index(0, 0), sourceModel.role("name")), "name 1");
QVERIFY(model.data(model.index(0, 0),
sourceModel.role("balances")).isValid());
auto roles = model.roleNames();
auto object = model.data(model.index(0, 0),
roleForName(roles, "proxyObject")).value<QObject*>();
QVERIFY(object);
QCOMPARE(object->property("count"), 1);
QCOMPARE(QQmlEngine::objectOwnership(object),
QQmlEngine::CppOwnership);
}
void submodelTypeTest() {
QQmlEngine engine;
QQmlComponent delegate(&engine);
auto delegateData = R"(
import QtQml 2.15
QtObject {
property var count: model.balances.count
}
)";
delegate.setData(delegateData, QUrl());
ObjectProxyModel model;
auto source = R"([
{ balances: [ { balance: 4 } ], name: "name 1" }
])";
ListModelWrapper sourceModel(engine, source);
model.setSourceModel(sourceModel);
model.setDelegate(&delegate);
model.setExpectedRoles({ QStringLiteral("balances") });
QCOMPARE(model.rowCount(), 1);
QVariant balances1 = model.data(model.index(0, 0),
sourceModel.role("balances"));
QVERIFY(balances1.isValid());
QVariant balances2 = model.data(model.index(0, 0),
sourceModel.role("balances"));
QVERIFY(balances2.isValid());
// ObjectProxyModel may create proxy objects on demand, then first
// call to data(...) returns freshly created object, the next calls
// related to the same row should return cached object. It's important
// to have QVariant type identical in both cases. E.g. returning raw
// pointer in first call and pointer wrapped into QPointer in the next
// one leads to problems in UI components in some scenarios even if
// those QVariant types are automatically convertible.
QCOMPARE(balances2.type(), balances1.type());
// Check if the same instance is returned.
QCOMPARE(balances2.value<QObject*>(), balances1.value<QObject*>());
}
void signalsDisconnectionTest() {
struct Model : public QIdentityProxyModel
{
using QObject::receivers;
};
Model sourceModel1, sourceModel2;
ObjectProxyModel model;
auto signal = SIGNAL(dataChanged(const QModelIndex&,
const QModelIndex&,
const QVector<int>));
model.setSourceModel(&sourceModel1);
QVERIFY(sourceModel1.receivers(signal) > 0);
model.setSourceModel(nullptr);
QVERIFY(sourceModel1.receivers(signal) == 0);
model.setSourceModel(&sourceModel1);
QVERIFY(sourceModel1.receivers(signal) > 0);
model.setSourceModel(&sourceModel2);
QVERIFY(sourceModel1.receivers(signal) == 0);
}
void deletingDelegateTest() {
QQmlEngine engine;
auto delegate = std::make_unique<QQmlComponent>(&engine);
delegate->setData(QByteArrayLiteral(R"(
import QtQml 2.15
QtObject {
property var sub: model.balances
}
)"), QUrl());
ObjectProxyModel model;
ListModelWrapper sourceModel(engine, QJsonArray {
QJsonObject {{ "balances", 11 }, { "name", "name 1" }},
QJsonObject {{ "balances", 12 }, { "name", "name 2" }},
QJsonObject {{ "balances", 123}, { "name", "name 3" }}
});
model.setSourceModel(sourceModel);
model.setDelegate(delegate.get());
model.setExpectedRoles({ QStringLiteral("balances") });
QSignalSpy delegateChangedSpy(&model,
&ObjectProxyModel::delegateChanged);
QSignalSpy dataChangedSpy(
&model, &ObjectProxyModel::dataChanged);
delegate.reset();
QCOMPARE(delegateChangedSpy.count(), 0);
QCOMPARE(dataChangedSpy.count(), 0);
QCOMPARE(model.rowCount(), 3);
QCOMPARE(model.data(model.index(0, 0),
sourceModel.role("balances")), 11);
}
void deletingSourceModelTest() {
QQmlEngine engine;
QQmlComponent delegate(&engine);
delegate.setData(QByteArrayLiteral(R"(
import QtQml 2.15
QtObject {
property var sub: model.balances
}
)"), QUrl());
ObjectProxyModel model;
auto sourceModel = std::make_unique<ListModelWrapper>(engine,
QJsonArray {
QJsonObject {{ "balances", 11 }, { "name", "name 1" }},
QJsonObject {{ "balances", 12 }, { "name", "name 2" }},
QJsonObject {{ "balances", 123}, { "name", "name 3" }}
}
);
model.setSourceModel(sourceModel->model());
model.setDelegate(&delegate);
model.setExpectedRoles({ QStringLiteral("balances") });
sourceModel.reset();
QCOMPARE(model.rowCount(), 0);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*"));
QCOMPARE(model.data(model.index(0, 0), 0), {});
}
void settingUndefinedExposedRoleNameTest() {
QQmlEngine engine;
auto delegate = std::make_unique<QQmlComponent>(&engine);
delegate->setData(QByteArrayLiteral(R"(
import QtQml 2.15
QtObject {
property var sub: model.balances
}
)"), QUrl());
ObjectProxyModel model;
ListModelWrapper sourceModel(engine, QJsonArray {
QJsonObject {{ "balances", 11 }, { "name", "name 1" }},
QJsonObject {{ "balances", 12 }, { "name", "name 2" }},
QJsonObject {{ "balances", 123}, { "name", "name 3" }}
});
model.setSourceModel(sourceModel);
model.setDelegate(delegate.get());
const auto expRoleName = QStringLiteral("undefined");
QTest::ignoreMessage(QtWarningMsg,
QRegularExpression(QStringLiteral(".*findExpectedRoles*.")));
QTest::ignoreMessage(QtWarningMsg, QStringLiteral("Expected role \"%1\" not found!").arg(expRoleName).toLatin1());
model.setExpectedRoles({ expRoleName });
QCOMPARE(model.rowCount(), 3);
}
void addingNewRoleToTopLevelModelTest() {
QQmlEngine engine;
auto delegate = std::make_unique<QQmlComponent>(&engine);
delegate->setData(QByteArrayLiteral(R"(
import QtQml.Models 2.15
ListModel {
property int extraValue: model.balances.count
}
)"), QUrl());
ObjectProxyModel model;
ListModelWrapper sourceModel(engine, R"([
{ "balances": [], "name": "name 1" },
{ "balances": [ { balance: 1 } ], "name": "name 2" },
{ "balances": [], "name": "name 3" }
])");
model.setSourceModel(sourceModel);
model.setDelegate(delegate.get());
model.setExpectedRoles({ QStringLiteral("balances") });
model.setExposedRoles({ QStringLiteral("extraValue") });
ListModelWrapper expected(engine, R"([
{ "balances": [], "name": "name 1", "extraValue": 0 },
{ "balances": [{ balance: 1 }], "name": "name 2", "extraValue": 1 },
{ "balances": [], "name": "name 3", "extraValue": 0 }
])");
QCOMPARE(model.rowCount(), 3);
auto roles = model.roleNames();
QCOMPARE(roles.size(), 3);
QVERIFY(isSame(&model, expected));
ModelSignalsSpy signalsSpy(&model);
QObject* proxy = model.proxyObject(0);
proxy->setProperty("extraValue", 42);
ListModelWrapper expected2(engine, R"([
{ "balances": [], "name": "name 1", "extraValue": 42 },
{ "balances": [{ balance: 1 }], "name": "name 2", "extraValue": 1 },
{ "balances": [], "name": "name 3", "extraValue": 0 }
])");
// dataChanged signal emission is scheduled to event loop, not called
// immediately
QCOMPARE(signalsSpy.count(), 0);
QVERIFY(QTest::qWaitFor([&signalsSpy]() {
return signalsSpy.count() == 1;
}));
QCOMPARE(signalsSpy.count(), 1);
QCOMPARE(signalsSpy.dataChangedSpy.count(), 1);
QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(0), model.index(0, 0));
QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(1),
model.index(model.rowCount() - 1, 0));
QVector<int> expectedChangedRoles = { roleForName(roles, "extraValue") };
QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(2).value<QVector<int>>(),
expectedChangedRoles);
QVERIFY(isSame(&model, expected2));
}
void additionalRoleDataChangedWhenEmptyTest() {
QQmlEngine engine;
auto delegate = std::make_unique<QQmlComponent>(&engine);
delegate->setData(QByteArrayLiteral(R"(
import QtQml.Models 2.15
ListModel {
property int extraValue: 0
}
)"), QUrl());
ListModelWrapper sourceModel(engine, R"([
{ "balances": [], "name": "name 1" }
])");
ObjectProxyModel model;
model.setSourceModel(sourceModel);
model.setDelegate(delegate.get());
model.setExpectedRoles({ QStringLiteral("balances") });
model.setExposedRoles({ QStringLiteral("extraValue") });
QCOMPARE(model.rowCount(), 1);
auto roles = model.roleNames();
QCOMPARE(roles.size(), 3);
ModelSignalsSpy signalsSpy(&model);
QObject* proxy = model.proxyObject(0);
// dataChanged signal emission is scheduled to event loop, not called
// immediately. In the meantime the source may be cleared and then no
// dataChanged event should be emited.
proxy->setProperty("extraValue", 42);
sourceModel.remove(0);
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 1);
QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 1);
QTest::qWait(100);
QCOMPARE(signalsSpy.count(), 2);
}
void modelResetWhenRoleChangedTest() {
QQmlEngine engine;
auto delegateWithRole = std::make_unique<QQmlComponent>(&engine);
delegateWithRole->setData(QByteArrayLiteral(R"(
import QtQml.Models 2.15
ListModel {
property int extraValue: 0
}
)"), QUrl());
auto delegateNoRole = std::make_unique<QQmlComponent>(&engine);
delegateNoRole->setData(QByteArrayLiteral(R"(
import QtQml.Models 2.15
ListModel {}
)"), QUrl());
ListModelWrapper sourceModel(engine, R"([
{ "balances": [], "name": "name 1" }
])");
// 1. set source
// 2. set delegate model
// 3. set expected role names
// 4. set exposed role names
{
ObjectProxyModel model;
ModelSignalsSpy signalsSpy(&model);
model.setSourceModel(sourceModel);
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 2);
model.setDelegate(delegateWithRole.get());
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 2);
model.setExpectedRoles({ QStringLiteral("balances") });
QCOMPARE(signalsSpy.count(), 4);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 2);
QCOMPARE(signalsSpy.modelResetSpy.count(), 2);
QCOMPARE(model.roleNames().count(), 2);
model.setExposedRoles({ QStringLiteral("extraValue") });
QCOMPARE(signalsSpy.count(), 6);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 3);
QCOMPARE(signalsSpy.modelResetSpy.count(), 3);
QCOMPARE(model.roleNames().count(), 3);
}
// 1. set delegate model
// 2. set source
// 3. set submodel role name
// 4. set exposed role names
{
ObjectProxyModel model;
ModelSignalsSpy signalsSpy(&model);
model.setDelegate(delegateWithRole.get());
QCOMPARE(signalsSpy.count(), 0);
QCOMPARE(model.roleNames().count(), 0);
model.setSourceModel(sourceModel);
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 2);
model.setExpectedRoles({ QStringLiteral("balances") });
QCOMPARE(signalsSpy.count(), 4);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 2);
QCOMPARE(signalsSpy.modelResetSpy.count(), 2);
QCOMPARE(model.roleNames().count(), 2);
model.setExposedRoles({ QStringLiteral("extraValue") });
QCOMPARE(signalsSpy.count(), 6);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 3);
QCOMPARE(signalsSpy.modelResetSpy.count(), 3);
QCOMPARE(model.roleNames().count(), 3);
}
// 1. set submodel role name
// 1. set expected role name
// 2. set delegate model
// 3. set source
{
ObjectProxyModel model;
ModelSignalsSpy signalsSpy(&model);
model.setExpectedRoles({ QStringLiteral("balances") });
model.setExposedRoles({ QStringLiteral("extraValue") });
model.setDelegate(delegateWithRole.get());
QCOMPARE(signalsSpy.count(), 0);
QCOMPARE(model.roleNames().count(), 0);
model.setSourceModel(sourceModel);
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 3);
}
// 1. set source
// 2. set delegate model (no extra roles)
// 3. set submodel role name
{
ObjectProxyModel model;
ModelSignalsSpy signalsSpy(&model);
model.setSourceModel(sourceModel);
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 2);
model.setDelegate(delegateNoRole.get());
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 2);
model.setExpectedRoles({ QStringLiteral("balances") });
QCOMPARE(signalsSpy.count(), 4);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 2);
QCOMPARE(signalsSpy.modelResetSpy.count(), 2);
QCOMPARE(model.roleNames().count(), 2);
}
}
void sourceModelResetTest() {
class IdentityModel : public QIdentityProxyModel {};
QQmlEngine engine;
auto delegate = std::make_unique<QQmlComponent>(&engine);
delegate->setData(QByteArrayLiteral(R"(
import QtQml.Models 2.15
ListModel {
property int extraValueRole: 0
}
)"), QUrl());
ListModelWrapper sourceModel1(engine, R"([
{ "balances": [], "name": "name 1" }
])");
ListModelWrapper sourceModel2(engine, R"([
{ "key": "1", "balances": [], "name": "name 1", "color": "red" }
])");
IdentityModel identity;
identity.setSourceModel(sourceModel1);
ObjectProxyModel model;
model.setSourceModel(&identity);
model.setDelegate(delegate.get());
model.setExpectedRoles({ QStringLiteral("balances") });
model.setExposedRoles({ QStringLiteral("extraValue") });
QCOMPARE(model.rowCount(), 1);
auto roles = model.roleNames();
QCOMPARE(roles.size(), 3);
ModelSignalsSpy signalsSpy(&model);
identity.setSourceModel(sourceModel2);
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.rowCount(), 1);
roles = model.roleNames();
QCOMPARE(roles.size(), 5);
}
void sourceModelLateRolesInitTest() {
QQmlEngine engine;
auto delegate = std::make_unique<QQmlComponent>(&engine);
delegate->setData(QByteArrayLiteral(R"(
import QtQml.Models 2.15
ListModel {
property int extraValue: 0
}
)"), QUrl());
ListModelWrapper sourceModel(engine, R"([])");
ObjectProxyModel model;
model.setSourceModel(sourceModel);
model.setDelegate(delegate.get());
model.setExpectedRoles({ QStringLiteral("balances") });
model.setExposedRoles({ QStringLiteral("extraValue") });
QCOMPARE(model.rowCount(), 0);
auto roles = model.roleNames();
QCOMPARE(roles.size(), 0);
ModelSignalsSpy signalsSpy(&model);
sourceModel.append(QJsonArray {
QJsonObject {{ "name", "D"}, { "balances", "d1" }},
QJsonObject {{ "name", "D"}, { "balances", "d2" }}
});
QCOMPARE(model.rowCount(), 2);
roles = model.roleNames();
QCOMPARE(roles.size(), 3);
}
void changingSourceModelTest() {
QQmlEngine engine;
QQmlComponent delegate(&engine);
auto delegateData = R"(
import QtQml 2.15
QtObject {
readonly property int count: model.balances.count
}
)";
delegate.setData(delegateData, QUrl());
ObjectProxyModel model;
auto source1 = R"([
{ balances: [ { balance: 4 } ], name: "name 1" },
{ balances: [ { balance: 4 }, {balance: 43} ], name: "name 2" }
])";
auto source2 = R"([
{ id: 4, balances: [ { balance: 4 } ], name: "name 1", color: "red" }
])";
ListModelWrapper sourceModel1(engine, source1);
ListModelWrapper sourceModel2(engine, source2);
model.setSourceModel(sourceModel1);
model.setDelegate(&delegate);
model.setExpectedRoles(QStringList({ QStringLiteral("balances") }));
model.setExposedRoles({ QStringLiteral("count")});
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.roleNames().size(), 3);
QSignalSpy sourceModelChangedSpy(
&model, &ObjectProxyModel::sourceModelChanged);
model.setSourceModel(sourceModel2);
QCOMPARE(sourceModelChangedSpy.count(), 1);
QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.roleNames().size(), 5);
model.setSourceModel(nullptr);
QCOMPARE(sourceModelChangedSpy.count(), 2);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames().size(), 0);
}
void sourceModelDataChangeTest() {
QQmlEngine engine;
QQmlComponent delegate(&engine);
auto delegateData = R"(
import QtQml 2.15
QtObject {
readonly property int doubledBalance: model.balance * 2
}
)";
delegate.setData(delegateData, QUrl());
ObjectProxyModel model;
auto source = R"([
{ balance: 4 },
{ balance: 10 }
])";
ListModelWrapper sourceModel(engine, source);
model.setSourceModel(sourceModel);
model.setDelegate(&delegate);
model.setExpectedRoles(QStringList({ QStringLiteral("balance") }));
model.setExposedRoles({ QStringLiteral("doubledBalance")});
ModelSignalsSpy signalsSpy(&model);
sourceModel.setProperty(0, "balance", 42);
sourceModel.setProperty(1, "balance", 0);
{
ListModelWrapper expected(engine, R"([
{ balance: 42, doubledBalance: 84 },
{ balance: 0, doubledBalance: 0 }
])");
QVERIFY(isSame(&model, expected));
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.dataChangedSpy.count(), 2);
}
QTest::qWait(100);
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.dataChangedSpy.count(), 2);
sourceModel.setProperty(0, "balance", 1);
sourceModel.setProperty(1, "balance", 2);
{
ListModelWrapper expected(engine, R"([
{ balance: 1, doubledBalance: 2 },
{ balance: 2, doubledBalance: 4 }
])");
QVERIFY(isSame(&model, expected));
QCOMPARE(signalsSpy.count(), 4);
QCOMPARE(signalsSpy.dataChangedSpy.count(), 4);
}
QTest::qWait(100);
QCOMPARE(signalsSpy.count(), 5);
QCOMPARE(signalsSpy.dataChangedSpy.count(), 5);
}
};
QTEST_MAIN(TestObjectProxyModel)
#include "tst_ObjectProxyModel.moc"