File indexing completed on 2024-04-21 03:56:09

0001 /*
0002     Copyright (C) 2019 David Edmundson <davidedmundson@kde.org>
0003 
0004     This library is free software; you can redistribute it and/or modify it
0005     under the terms of the GNU Lesser General Public License as published by
0006     the Free Software Foundation; either version 2.1 of the License, or (at your
0007     option) any later version.
0008 
0009     This library is distributed in the hope that it will be useful, but WITHOUT
0010     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0011     FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
0012     License for more details.
0013 
0014     You should have received a copy of the GNU Library General Public License
0015     along with this library; see the file COPYING.LIB.  If not, write to the
0016     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
0017     02110-1301, USA.
0018 */
0019 
0020 #include <QObject>
0021 
0022 #include <QSignalSpy>
0023 #include <QTest>
0024 
0025 #include <QQmlApplicationEngine>
0026 #include <QQmlContext>
0027 #include <QQmlEngine>
0028 
0029 #include <QAbstractItemModel>
0030 #include <QSortFilterProxyModel>
0031 #include <QStandardItemModel>
0032 
0033 #ifdef IMPORT_ITEMMODELSPLUGIN
0034 #include <QPluginLoader>
0035 Q_IMPORT_PLUGIN(Plugin)
0036 #endif
0037 
0038 class tst_KSortFilterProxyModelQml : public QObject
0039 {
0040     Q_OBJECT
0041 
0042 public:
0043     enum Roles {
0044         // Some non-default role ID with a QString data
0045         MonthNameRole = Qt::UserRole,
0046         IntUserRole,
0047     };
0048     Q_ENUM(Roles)
0049 
0050 private Q_SLOTS:
0051     void init();
0052     void testFilterCallback();
0053     void testSortRole_data();
0054     void testSortRole();
0055     void testFilterRegExp();
0056     void testFilterRegExpRole();
0057     void testRoleVsModelRace();
0058 
0059 private:
0060     QAbstractItemModel *createMonthTestModel(QObject *parent);
0061 };
0062 
0063 QAbstractItemModel *tst_KSortFilterProxyModelQml::createMonthTestModel(QObject *parent)
0064 {
0065     auto testModel = new QStandardItemModel(parent);
0066     for (int i = 1; i <= 12; i++) {
0067         auto entry = new QStandardItem();
0068         const auto month = QLocale::c().monthName(i);
0069         entry->setData(month, Qt::DisplayRole);
0070         entry->setData(month, MonthNameRole);
0071         entry->setData(i, IntUserRole);
0072         testModel->appendRow(entry);
0073     }
0074     testModel->setItemRoleNames({
0075         {Qt::DisplayRole, "display"},
0076         {MonthNameRole, "month"},
0077         {IntUserRole, "user"},
0078     });
0079     return testModel;
0080 }
0081 
0082 void tst_KSortFilterProxyModelQml::init()
0083 {
0084     qmlRegisterType<tst_KSortFilterProxyModelQml>("org.kde.kitemmodels.test", 1, 0, "Self");
0085 }
0086 
0087 void tst_KSortFilterProxyModelQml::testFilterCallback()
0088 {
0089     QQmlApplicationEngine app;
0090     app.loadData(
0091         "import QtQml 2.0\n"
0092         "import org.kde.kitemmodels 1.0\n"
0093         "KSortFilterProxyModel\n"
0094         "{\n"
0095         "    property int modulo: 2\n"
0096         "    sourceModel: KNumberModel {\n"
0097         "        minimumValue: 1\n"
0098         "        maximumValue: 10\n"
0099         "    }\n"
0100         "    filterRowCallback: function(source_row, source_parent) {\n"
0101         "        return sourceModel.data(sourceModel.index(source_row, 0, source_parent), Qt.DisplayRole) % modulo == 1;\n"
0102         "    };\n"
0103         "}\n");
0104     QCOMPARE(app.rootObjects().count(), 1);
0105 
0106     auto filterModel = qobject_cast<QAbstractItemModel *>(app.rootObjects().first());
0107     QVERIFY(filterModel);
0108 
0109     QCOMPARE(filterModel->rowCount(), 5);
0110     QCOMPARE(filterModel->data(filterModel->index(0, 0)).toString(), "1");
0111     QCOMPARE(filterModel->data(filterModel->index(1, 0)).toString(), "3");
0112     QCOMPARE(filterModel->data(filterModel->index(2, 0)).toString(), "5");
0113     QCOMPARE(filterModel->data(filterModel->index(3, 0)).toString(), "7");
0114     QCOMPARE(filterModel->data(filterModel->index(4, 0)).toString(), "9");
0115 
0116     filterModel->setProperty("modulo", 3);
0117 
0118     // Nothing should change until we call invalidateFilter
0119     QCOMPARE(filterModel->rowCount(), 5);
0120     QCOMPARE(filterModel->data(filterModel->index(0, 0)).toString(), "1");
0121     QCOMPARE(filterModel->data(filterModel->index(1, 0)).toString(), "3");
0122     QCOMPARE(filterModel->data(filterModel->index(2, 0)).toString(), "5");
0123     QCOMPARE(filterModel->data(filterModel->index(3, 0)).toString(), "7");
0124     QCOMPARE(filterModel->data(filterModel->index(4, 0)).toString(), "9");
0125 
0126     // Simulate call from QML by going through metaobject rather than calling it directly
0127     const bool invalidateFilterCallOk = QMetaObject::invokeMethod(filterModel, "invalidateFilter");
0128     QVERIFY(invalidateFilterCallOk);
0129 
0130     QCOMPARE(filterModel->rowCount(), 4);
0131     QCOMPARE(filterModel->data(filterModel->index(0, 0)).toString(), "1");
0132     QCOMPARE(filterModel->data(filterModel->index(1, 0)).toString(), "4");
0133     QCOMPARE(filterModel->data(filterModel->index(2, 0)).toString(), "7");
0134     QCOMPARE(filterModel->data(filterModel->index(3, 0)).toString(), "10");
0135 }
0136 
0137 void tst_KSortFilterProxyModelQml::testSortRole_data()
0138 {
0139     // test model consists of all month names + month number as Display and UserRoler respectively
0140 
0141     QTest::addColumn<QString>("qmlContents");
0142     QTest::addColumn<QString>("result");
0143 
0144     QTest::newRow("sort by role name - display") << R"(
0145         KSortFilterProxyModel {
0146             sourceModel: testModel
0147             sortRoleName: "display"
0148         }
0149     )"
0150                                                  << "April";
0151 
0152     QTest::newRow("sort by role name - value") << R"(
0153         KSortFilterProxyModel {
0154             sourceModel: testModel
0155             sortRoleName: "user"
0156         }
0157     )"
0158                                                << "January";
0159 
0160     QTest::newRow("sort by role name - reset") << R"(
0161         KSortFilterProxyModel {
0162             sourceModel: testModel
0163             sortRoleName: ""
0164             Component.onCompleted: sortRoleName = ""
0165         }
0166     )"
0167                                                << "January";
0168 }
0169 
0170 void tst_KSortFilterProxyModelQml::testSortRole()
0171 {
0172     QQmlApplicationEngine app;
0173     QFETCH(QString, qmlContents);
0174     QFETCH(QString, result);
0175 
0176     qmlContents = R"(
0177         import org.kde.kitemmodels 1.0
0178         import QtQuick 2.0
0179 
0180     )" + qmlContents;
0181 
0182     app.rootContext()->setContextProperty("testModel", createMonthTestModel(&app));
0183 
0184     app.loadData(qmlContents.toLatin1());
0185 
0186     QCOMPARE(app.rootObjects().count(), 1);
0187     auto filterModel = qobject_cast<QAbstractItemModel *>(app.rootObjects().first());
0188     QVERIFY(filterModel);
0189     QCOMPARE(filterModel->rowCount(), 12);
0190     QCOMPARE(filterModel->data(filterModel->index(0, 0), Qt::DisplayRole).toString(), result);
0191 }
0192 
0193 void tst_KSortFilterProxyModelQml::testFilterRegExp()
0194 {
0195     // filterRegExp comes from the QSortFilterProxyModel directly, confirm it still works
0196     QQmlApplicationEngine app;
0197 
0198     app.rootContext()->setContextProperty("testModel", createMonthTestModel(&app));
0199 
0200     auto qmlSrc = QByteArray(
0201         "import QtQml 2.0\n"
0202         "import org.kde.kitemmodels 1.0\n"
0203         "KSortFilterProxyModel {\n"
0204         " sourceModel: testModel\n"
0205         " filterRegularExpression: /Ma.*/\n"
0206         "}\n");
0207     app.loadData(qmlSrc);
0208 
0209     QCOMPARE(app.rootObjects().count(), 1);
0210     auto filterModel = qobject_cast<QAbstractItemModel *>(app.rootObjects().first());
0211     QVERIFY(filterModel);
0212     QCOMPARE(filterModel->rowCount(), 2);
0213     QCOMPARE(filterModel->data(filterModel->index(0, 0), Qt::DisplayRole).toString(), "March");
0214     QCOMPARE(filterModel->data(filterModel->index(1, 0), Qt::DisplayRole).toString(), "May");
0215 }
0216 
0217 void tst_KSortFilterProxyModelQml::testFilterRegExpRole()
0218 {
0219     // filterRegExp comes from the QSortFilterProxyModel directly, confirm it still works
0220     QQmlApplicationEngine app;
0221 
0222     app.rootContext()->setContextProperty("testModel", createMonthTestModel(&app));
0223 
0224     auto qmlSrc = QByteArray(
0225         "import QtQml 2.0\n"
0226         "import org.kde.kitemmodels 1.0\n"
0227         "KSortFilterProxyModel {\n"
0228         " sourceModel: testModel\n"
0229         " filterRoleName: \"user\"\n"
0230         " filterRegularExpression: /1[0-9]/\n" // month value is 10 or more
0231         "}\n");
0232     app.loadData(qmlSrc);
0233 
0234     QCOMPARE(app.rootObjects().count(), 1);
0235     auto filterModel = qobject_cast<QAbstractItemModel *>(app.rootObjects().first());
0236     QVERIFY(filterModel);
0237     QCOMPARE(filterModel->rowCount(), 3);
0238     QCOMPARE(filterModel->data(filterModel->index(0, 0), Qt::DisplayRole).toString(), "October");
0239     QCOMPARE(filterModel->data(filterModel->index(1, 0), Qt::DisplayRole).toString(), "November");
0240     QCOMPARE(filterModel->data(filterModel->index(2, 0), Qt::DisplayRole).toString(), "December");
0241 }
0242 
0243 void tst_KSortFilterProxyModelQml::testRoleVsModelRace()
0244 {
0245     // Our QML type extends base type's role property with a roleName
0246     // property, thus we have kinda two sources of truth, and they both reset
0247     // if a model is assigned after component's completion.
0248     // See BUG 476950
0249     QQmlApplicationEngine app;
0250 
0251     auto qmlSrcModel = QByteArray(R"(
0252         import QtQml
0253         import org.kde.kitemmodels as KItemModels
0254         import org.kde.kitemmodels.test as KItemModelsTest
0255 
0256         KItemModels.KSortFilterProxyModel {
0257             id: root
0258 
0259             sortCaseSensitivity: Qt.CaseInsensitive
0260             sortRole: KItemModelsTest.Self.MonthNameRole
0261 
0262             filterCaseSensitivity: Qt.CaseInsensitive
0263             filterRole: KItemModelsTest.Self.MonthNameRole
0264         }
0265     )");
0266     app.loadData(qmlSrcModel);
0267     auto filterModel = qobject_cast<QSortFilterProxyModel *>(app.rootObjects().first());
0268     QVERIFY(filterModel);
0269 
0270     // There is no model yet, so the role names are default so far
0271     QCOMPARE(filterModel->property("sortRole").toInt(), MonthNameRole);
0272     QCOMPARE(filterModel->property("sortRoleName").toString(), QStringLiteral("display"));
0273     QCOMPARE(filterModel->property("filterRole").toInt(), MonthNameRole);
0274     QCOMPARE(filterModel->property("filterRoleName").toString(), QStringLiteral("display"));
0275 
0276     const auto model = createMonthTestModel(&app);
0277 
0278     QVERIFY(filterModel->setProperty("filterString", QStringLiteral("a")));
0279     QCOMPARE(filterModel->rowCount(), 0);
0280 
0281     filterModel->setSourceModel(model);
0282     QCOMPARE(filterModel->rowCount(), 6);
0283 
0284     QCOMPARE(filterModel->property("sortRole").toInt(), MonthNameRole);
0285     QCOMPARE(filterModel->property("sortRoleName").toString(), QStringLiteral("month"));
0286     QCOMPARE(filterModel->property("filterRole").toInt(), MonthNameRole);
0287     QCOMPARE(filterModel->property("filterRoleName").toString(), QStringLiteral("month"));
0288 
0289     // and now swap the source of truth to roleName
0290     QVERIFY(filterModel->setProperty("sortRoleName", QStringLiteral("user")));
0291     QCOMPARE(filterModel->property("sortRole").toInt(), IntUserRole);
0292     QVERIFY(filterModel->setProperty("filterRoleName", QStringLiteral("user")));
0293     QCOMPARE(filterModel->property("filterRole").toInt(), IntUserRole);
0294 
0295     QVERIFY(filterModel->setProperty("filterString", QStringLiteral("9")));
0296 
0297     // Reset the model. Roles should persist.
0298     QCOMPARE(filterModel->rowCount(), 1);
0299     filterModel->setSourceModel(nullptr);
0300     filterModel->setSourceModel(model);
0301     QCOMPARE(filterModel->rowCount(), 1);
0302 
0303     QCOMPARE(filterModel->property("sortRole").toInt(), IntUserRole);
0304     QCOMPARE(filterModel->property("sortRoleName").toString(), QStringLiteral("user"));
0305     QCOMPARE(filterModel->property("filterRole").toInt(), IntUserRole);
0306     QCOMPARE(filterModel->property("filterRoleName").toString(), QStringLiteral("user"));
0307 }
0308 
0309 QTEST_GUILESS_MAIN(tst_KSortFilterProxyModelQml)
0310 
0311 #include "ksortfilterproxymodel_qml.moc"