File indexing completed on 2024-04-21 15:01:23

0001 /*
0002     SPDX-FileCopyrightText: 2014 Christian Mollekopf <mollekopf@kolabsys.com>
0003     SPDX-FileCopyrightText: 2014 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include <krecursivefilterproxymodel.h>
0009 
0010 #include <QStandardItemModel>
0011 #include <QTest>
0012 
0013 Q_DECLARE_METATYPE(QModelIndex)
0014 
0015 class ModelSignalSpy : public QObject
0016 {
0017     Q_OBJECT
0018 public:
0019     explicit ModelSignalSpy(QAbstractItemModel &model)
0020     {
0021         connect(&model, SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(onRowsInserted(QModelIndex, int, int)));
0022         connect(&model, SIGNAL(rowsRemoved(QModelIndex, int, int)), this, SLOT(onRowsRemoved(QModelIndex, int, int)));
0023         connect(&model, SIGNAL(rowsAboutToBeInserted(QModelIndex, int, int)), this, SLOT(onRowsAboutToBeInserted(QModelIndex, int, int)));
0024         connect(&model, SIGNAL(rowsAboutToBeRemoved(QModelIndex, int, int)), this, SLOT(onRowsAboutToBeRemoved(QModelIndex, int, int)));
0025         connect(&model, SIGNAL(rowsMoved(QModelIndex, int, int, QModelIndex, int)), this, SLOT(onRowsMoved(QModelIndex, int, int, QModelIndex, int)));
0026         connect(&model, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(onDataChanged(QModelIndex, QModelIndex)));
0027         connect(&model, SIGNAL(layoutChanged()), this, SLOT(onLayoutChanged()));
0028         connect(&model, SIGNAL(modelReset()), this, SLOT(onModelReset()));
0029     }
0030 
0031     QStringList mSignals;
0032 
0033 private Q_SLOTS:
0034     void onRowsInserted(QModelIndex p, int start, int end)
0035     {
0036         mSignals << QLatin1String("rowsInserted(") + textForRowSpy(p, start, end) + ')';
0037     }
0038     void onRowsRemoved(QModelIndex p, int start, int end)
0039     {
0040         mSignals << QLatin1String("rowsRemoved(") + textForRowSpy(p, start, end) + ')';
0041     }
0042     void onRowsAboutToBeInserted(QModelIndex p, int start, int end)
0043     {
0044         mSignals << QLatin1String("rowsAboutToBeInserted(") + textForRowSpy(p, start, end) + ')';
0045     }
0046     void onRowsAboutToBeRemoved(QModelIndex p, int start, int end)
0047     {
0048         mSignals << QLatin1String("rowsAboutToBeRemoved(") + textForRowSpy(p, start, end) + ')';
0049     }
0050     void onRowsMoved(QModelIndex, int, int, QModelIndex, int)
0051     {
0052         mSignals << QStringLiteral("rowsMoved");
0053     }
0054     void onDataChanged(const QModelIndex &from, const QModelIndex &)
0055     {
0056         mSignals << QStringLiteral("dataChanged(%1)").arg(from.data().toString());
0057     }
0058     void onLayoutChanged()
0059     {
0060         mSignals << QStringLiteral("layoutChanged");
0061     }
0062     void onModelReset()
0063     {
0064         mSignals << QStringLiteral("modelReset");
0065     }
0066 
0067 private:
0068     QString textForRowSpy(const QModelIndex &parent, int start, int end)
0069     {
0070         QString txt = parent.data().toString();
0071         if (!txt.isEmpty()) {
0072             txt += QLatin1Char('.');
0073         }
0074         txt += QString::number(start + 1);
0075         if (start != end) {
0076             txt += QLatin1Char('-') + QString::number(end + 1);
0077         }
0078         return txt;
0079     }
0080 };
0081 
0082 class TestModel : public KRecursiveFilterProxyModel
0083 {
0084     Q_OBJECT
0085 public:
0086     TestModel(QAbstractItemModel *sourceModel)
0087         : KRecursiveFilterProxyModel()
0088     {
0089         setSourceModel(sourceModel);
0090     }
0091 
0092     bool acceptRow(int sourceRow, const QModelIndex &sourceParent) const override
0093     {
0094         // qDebug() << sourceModel()->index(sourceRow, 0, sourceParent).data().toString()
0095         //          << sourceModel()->index(sourceRow, 0, sourceParent).data(Qt::UserRole+1).toBool();
0096         return sourceModel()->index(sourceRow, 0, sourceParent).data(Qt::UserRole + 1).toBool();
0097     }
0098 };
0099 
0100 // Represents this tree
0101 // - A
0102 // - - B
0103 // - - - C
0104 // - - - D
0105 // - - E
0106 // as a single string, englobing children in brackets, like this:
0107 // [A[B[C D] E]]
0108 // In addition, items that match the filtering (data(UserRole+1) == true) have a * after their value.
0109 static QString treeAsString(const QAbstractItemModel &model, const QModelIndex &parent = QModelIndex())
0110 {
0111     QString ret;
0112     const int rowCount = model.rowCount(parent);
0113     if (rowCount > 0) {
0114         ret += QLatin1Char('[');
0115         for (int row = 0; row < rowCount; ++row) {
0116             if (row > 0) {
0117                 ret += ' ';
0118             }
0119             const QModelIndex child = model.index(row, 0, parent);
0120             ret += child.data().toString();
0121             if (child.data(Qt::UserRole + 1).toBool()) {
0122                 ret += QLatin1Char('*');
0123             }
0124             ret += treeAsString(model, child);
0125         }
0126         ret += QLatin1Char(']');
0127     }
0128     return ret;
0129 }
0130 
0131 // Fill a tree model based on a string representation (see treeAsString)
0132 static void fillModel(QStandardItemModel &model, const QString &str)
0133 {
0134     QCOMPARE(str.count('['), str.count(']'));
0135     QStandardItem *item = nullptr;
0136     QString data;
0137     for (int i = 0; i < str.length(); ++i) {
0138         const QChar ch = str.at(i);
0139         if ((ch == '[' || ch == ']' || ch == ' ') && !data.isEmpty()) {
0140             if (data.endsWith('*')) {
0141                 item->setData(true);
0142                 data.chop(1);
0143             }
0144             item->setText(data);
0145             data.clear();
0146         }
0147         if (ch == '[') {
0148             // Create new child
0149             QStandardItem *child = new QStandardItem;
0150             if (item) {
0151                 item->appendRow(child);
0152             } else {
0153                 model.appendRow(child);
0154             }
0155             item = child;
0156         } else if (ch == ']') {
0157             // Go up to parent
0158             item = item->parent();
0159         } else if (ch == ' ') {
0160             // Create new sibling
0161             QStandardItem *child = new QStandardItem;
0162             QStandardItem *parent = item->parent();
0163             if (parent) {
0164                 parent->appendRow(child);
0165             } else {
0166                 model.appendRow(child);
0167             }
0168             item = child;
0169         } else {
0170             data += ch;
0171         }
0172     }
0173 }
0174 
0175 class KRecursiveFilterProxyModelTest : public QObject
0176 {
0177     Q_OBJECT
0178 private:
0179 private Q_SLOTS:
0180     void testInitialFiltering_data()
0181     {
0182         QTest::addColumn<QString>("sourceStr");
0183         QTest::addColumn<QString>("proxyStr");
0184 
0185         QTest::newRow("empty") << "[]"
0186                                << "";
0187         QTest::newRow("no") << "[1]"
0188                             << "";
0189         QTest::newRow("yes") << "[1*]"
0190                              << "[1*]";
0191         QTest::newRow("second") << "[1 2*]"
0192                                 << "[2*]";
0193         QTest::newRow("child_yes") << "[1 2[2.1*]]"
0194                                    << "[2[2.1*]]";
0195         QTest::newRow("grandchild_yes") << "[1 2[2.1[2.1.1*]]]"
0196                                         << "[2[2.1[2.1.1*]]]";
0197         // 1, 3.1 and 4.2.1 match, so their parents are in the model
0198         QTest::newRow("more") << "[1* 2[2.1] 3[3.1*] 4[4.1 4.2[4.2.1*]]]"
0199                               << "[1* 3[3.1*] 4[4.2[4.2.1*]]]";
0200     }
0201 
0202     void testInitialFiltering()
0203     {
0204         QFETCH(QString, sourceStr);
0205         QFETCH(QString, proxyStr);
0206 
0207         QStandardItemModel model;
0208         fillModel(model, sourceStr);
0209         QCOMPARE(treeAsString(model), sourceStr);
0210 
0211         TestModel proxy(&model);
0212         QCOMPARE(treeAsString(proxy), proxyStr);
0213     }
0214 
0215     // Test changing a role that is unrelated to the filtering.
0216     void testUnrelatedDataChange()
0217     {
0218         QStandardItemModel model;
0219         const QString sourceStr = QStringLiteral("[1[1.1[1.1.1*]]]");
0220         fillModel(model, sourceStr);
0221         QCOMPARE(treeAsString(model), sourceStr);
0222 
0223         TestModel proxy(&model);
0224         QCOMPARE(treeAsString(proxy), sourceStr);
0225 
0226         ModelSignalSpy spy(proxy);
0227         QStandardItem *item_1_1_1 = model.item(0)->child(0)->child(0);
0228 
0229         // When changing the text on the item
0230         item_1_1_1->setText(QStringLiteral("ME"));
0231 
0232         QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[ME*]]]"));
0233 
0234         QCOMPARE(spy.mSignals,
0235                  QStringList() << QStringLiteral("dataChanged(ME)")
0236                                << QStringLiteral("dataChanged(1.1)") // ### yep, unneeded, but the proxy has no way to know that...
0237                                << QStringLiteral("dataChanged(1)") // ### unneeded too
0238         );
0239     }
0240 
0241     // Test changing a role that is unrelated to the filtering, in a hidden item.
0242     void testHiddenDataChange()
0243     {
0244         QStandardItemModel model;
0245         const QString sourceStr = QStringLiteral("[1[1.1[1.1.1]]]");
0246         fillModel(model, sourceStr);
0247         QCOMPARE(treeAsString(model), sourceStr);
0248 
0249         TestModel proxy(&model);
0250         QCOMPARE(treeAsString(proxy), QString());
0251 
0252         ModelSignalSpy spy(proxy);
0253         QStandardItem *item_1_1_1 = model.item(0)->child(0)->child(0);
0254 
0255         // When changing the text on a hidden item
0256         item_1_1_1->setText(QStringLiteral("ME"));
0257 
0258         QCOMPARE(treeAsString(proxy), QString());
0259         QCOMPARE(spy.mSignals, QStringList());
0260     }
0261 
0262     // Test that we properly react to a data-changed signal in a descendant and include all required rows
0263     void testDataChangeIn_data()
0264     {
0265         QTest::addColumn<QString>("sourceStr");
0266         QTest::addColumn<QString>("initialProxyStr");
0267         QTest::addColumn<QString>("add"); // set the flag on this item
0268         QTest::addColumn<QString>("expectedProxyStr");
0269         QTest::addColumn<QStringList>("expectedSignals");
0270 
0271         QTest::newRow("toplevel") << "[1]"
0272                                   << ""
0273                                   << "1"
0274                                   << "[1*]" << (QStringList() << QStringLiteral("rowsAboutToBeInserted(1)") << QStringLiteral("rowsInserted(1)"));
0275         QTest::newRow("show_parents") << "[1[1.1[1.1.1]]]"
0276                                       << ""
0277                                       << "1.1.1"
0278                                       << "[1[1.1[1.1.1*]]]"
0279                                       << (QStringList() << QStringLiteral("rowsAboutToBeInserted(1)") << QStringLiteral("rowsInserted(1)"));
0280 
0281         const QStringList insert_1_1_1 = QStringList() << QStringLiteral("rowsAboutToBeInserted(1.1.1)") << QStringLiteral("rowsInserted(1.1.1)")
0282                                                        << QStringLiteral("dataChanged(1.1)") << QStringLiteral("dataChanged(1)"); // both unneeded
0283         QTest::newRow("parent_visible") << "[1[1.1*[1.1.1]]]"
0284                                         << "[1[1.1*]]"
0285                                         << "1.1.1"
0286                                         << "[1[1.1*[1.1.1*]]]" << insert_1_1_1;
0287 
0288         QTest::newRow("sibling_visible") << "[1[1.1[1.1.1 1.1.2*]]]"
0289                                          << "[1[1.1[1.1.2*]]]"
0290                                          << "1.1.1"
0291                                          << "[1[1.1[1.1.1* 1.1.2*]]]" << insert_1_1_1;
0292 
0293         QTest::newRow("visible_cousin") << "[1[1.1[1.1.1 1.1.2[1.1.2.1*]]]]"
0294                                         << "[1[1.1[1.1.2[1.1.2.1*]]]]"
0295                                         << "1.1.1"
0296                                         << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << insert_1_1_1;
0297 
0298         QTest::newRow("show_parent") << "[1[1.1[1.1.1 1.1.2] 1.2*]]"
0299                                      << "[1[1.2*]]"
0300                                      << "1.1.1"
0301                                      << "[1[1.1[1.1.1*] 1.2*]]"
0302                                      << (QStringList() << QStringLiteral("rowsAboutToBeInserted(1.1)") << QStringLiteral("rowsInserted(1.1)")
0303                                                        << QStringLiteral("dataChanged(1)")); // unneeded
0304 
0305         QTest::newRow("with_children") << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]"
0306                                        << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]"
0307                                        << "1.1.1"
0308                                        << "[1[1.1[1.1.1*[1.1.1.1*]]] 2*]"
0309                                        << (QStringList() << QStringLiteral("dataChanged(1.1.1)") << QStringLiteral("dataChanged(1.1)")
0310                                                          << QStringLiteral("dataChanged(1)") // both unneeded
0311                                           );
0312     }
0313 
0314     void testDataChangeIn()
0315     {
0316         QFETCH(QString, sourceStr);
0317         QFETCH(QString, initialProxyStr);
0318         QFETCH(QString, add);
0319         QFETCH(QString, expectedProxyStr);
0320         QFETCH(QStringList, expectedSignals);
0321 
0322         QStandardItemModel model;
0323         fillModel(model, sourceStr);
0324         QCOMPARE(treeAsString(model), sourceStr);
0325 
0326         TestModel proxy(&model);
0327         QCOMPARE(treeAsString(proxy), initialProxyStr);
0328 
0329         ModelSignalSpy spy(proxy);
0330         // When changing the data on the designated item to show this row
0331         QStandardItem *itemToChange = itemByText(model, add);
0332         QVERIFY(!itemToChange->data().toBool());
0333         itemToChange->setData(true);
0334 
0335         // The proxy should update as expected
0336         QCOMPARE(treeAsString(proxy), expectedProxyStr);
0337 
0338         // qDebug() << spy.mSignals;
0339         QCOMPARE(spy.mSignals, expectedSignals);
0340     }
0341 
0342     void testDataChangeOut_data()
0343     {
0344         QTest::addColumn<QString>("sourceStr");
0345         QTest::addColumn<QString>("initialProxyStr");
0346         QTest::addColumn<QString>("remove"); // unset the flag on this item
0347         QTest::addColumn<QString>("expectedProxyStr");
0348         QTest::addColumn<QStringList>("expectedSignals");
0349 
0350         const QStringList remove1_1_1 = (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1.1.1)") << QStringLiteral("rowsRemoved(1.1.1)")
0351                                                        << QStringLiteral("dataChanged(1.1)") // unneeded
0352                                                        << QStringLiteral("dataChanged(1)") // unneeded
0353         );
0354 
0355         QTest::newRow("toplevel") << "[1*]"
0356                                   << "[1*]"
0357                                   << "1"
0358                                   << "" << (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1)") << QStringLiteral("rowsRemoved(1)"));
0359 
0360         QTest::newRow("hide_parent") << "[1[1.1[1.1.1*]]]"
0361                                      << "[1[1.1[1.1.1*]]]"
0362                                      << "1.1.1"
0363                                      << ""
0364                                      << (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1.1.1)") // ### unneeded but the proxy has no way to know that...
0365                                                        << QStringLiteral("rowsRemoved(1.1.1)") // unneeded
0366                                                        << QStringLiteral("rowsAboutToBeRemoved(1.1)") // unneeded
0367                                                        << QStringLiteral("rowsRemoved(1.1)") // unneeded
0368                                                        << QStringLiteral("rowsAboutToBeRemoved(1)") << QStringLiteral("rowsRemoved(1)"));
0369 
0370         QTest::newRow("parent_visible") << "[1[1.1*[1.1.1*]]]"
0371                                         << "[1[1.1*[1.1.1*]]]"
0372                                         << "1.1.1"
0373                                         << "[1[1.1*]]" << remove1_1_1;
0374 
0375         QTest::newRow("visible") << "[1[1.1[1.1.1* 1.1.2*]]]"
0376                                  << "[1[1.1[1.1.1* 1.1.2*]]]"
0377                                  << "1.1.1"
0378                                  << "[1[1.1[1.1.2*]]]" << remove1_1_1;
0379         QTest::newRow("visible_cousin") << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]"
0380                                         << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]"
0381                                         << "1.1.1"
0382                                         << "[1[1.1[1.1.2[1.1.2.1*]]]]" << remove1_1_1;
0383 
0384         // The following tests trigger the removal of an ascendant.
0385         QTest::newRow("remove_parent") << "[1[1.1[1.1.1* 1.1.2] 1.2*]]"
0386                                        << "[1[1.1[1.1.1*] 1.2*]]"
0387                                        << "1.1.1"
0388                                        << "[1[1.2*]]"
0389                                        << (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1.1.1)") << QStringLiteral("rowsRemoved(1.1.1)")
0390                                                          << QStringLiteral("rowsAboutToBeRemoved(1.1)") << QStringLiteral("rowsRemoved(1.1)")
0391                                                          << QStringLiteral("dataChanged(1)") // unneeded
0392                                           );
0393 
0394         QTest::newRow("with_children") << "[1[1.1[1.1.1*[1.1.1.1*]]] 2*]"
0395                                        << "[1[1.1[1.1.1*[1.1.1.1*]]] 2*]"
0396                                        << "1.1.1"
0397                                        << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]"
0398                                        << (QStringList() << QStringLiteral("dataChanged(1.1.1)") << QStringLiteral("dataChanged(1.1)")
0399                                                          << QStringLiteral("dataChanged(1)") // both unneeded
0400                                           );
0401 
0402         QTest::newRow("last_visible") << "[1[1.1[1.1.1* 1.1.2]]]"
0403                                       << "[1[1.1[1.1.1*]]]"
0404                                       << "1.1.1"
0405                                       << ""
0406                                       << (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1.1.1)") << QStringLiteral("rowsRemoved(1.1.1)") // unneeded
0407                                                         << QStringLiteral("rowsAboutToBeRemoved(1.1)") << QStringLiteral("rowsRemoved(1.1)") // unneeded
0408                                                         << QStringLiteral("rowsAboutToBeRemoved(1)") << QStringLiteral("rowsRemoved(1)"));
0409     }
0410 
0411     void testDataChangeOut()
0412     {
0413         QFETCH(QString, sourceStr);
0414         QFETCH(QString, initialProxyStr);
0415         QFETCH(QString, remove);
0416         QFETCH(QString, expectedProxyStr);
0417         QFETCH(QStringList, expectedSignals);
0418 
0419         QStandardItemModel model;
0420         fillModel(model, sourceStr);
0421         QCOMPARE(treeAsString(model), sourceStr);
0422 
0423         TestModel proxy(&model);
0424         QCOMPARE(treeAsString(proxy), initialProxyStr);
0425 
0426         ModelSignalSpy spy(proxy);
0427 
0428         // When changing the data on the designated item to exclude this row again
0429         QStandardItem *itemToChange = itemByText(model, remove);
0430         QVERIFY(itemToChange->data().toBool());
0431         itemToChange->setData(false);
0432 
0433         // The proxy should update as expected
0434         QCOMPARE(treeAsString(proxy), expectedProxyStr);
0435 
0436         // qDebug() << spy.mSignals;
0437         QCOMPARE(spy.mSignals, expectedSignals);
0438     }
0439 
0440     void testInsert()
0441     {
0442         QStandardItemModel model;
0443         const QString sourceStr = QStringLiteral("[1[1.1[1.1.1]]]");
0444         fillModel(model, sourceStr);
0445         QCOMPARE(treeAsString(model), sourceStr);
0446 
0447         TestModel proxy(&model);
0448         QCOMPARE(treeAsString(proxy), QString());
0449 
0450         ModelSignalSpy spy(proxy);
0451         QStandardItem *item_1_1_1 = model.item(0)->child(0)->child(0);
0452         QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1"));
0453         item_1_1_1_1->setData(true);
0454         item_1_1_1->appendRow(item_1_1_1_1);
0455         QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1[1.1.1.1*]]]]"));
0456 
0457         QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsAboutToBeInserted(1)") << QStringLiteral("rowsInserted(1)"));
0458     }
0459 
0460     // Start from [1[1.1[1.1.1 1.1.2[1.1.2.1*]]]]
0461     // where 1.1.1 is hidden but 1.1 is shown, we want to insert a shown child in 1.1.1.
0462     // The proxy ensures dataChanged is called on 1.1,
0463     // so that 1.1.1 and 1.1.1.1 are included in the model.
0464     void testInsertCousin()
0465     {
0466         QStandardItemModel model;
0467         const QString sourceStr = QStringLiteral("[1[1.1[1.1.1 1.1.2[1.1.2.1*]]]]");
0468         fillModel(model, sourceStr);
0469         QCOMPARE(treeAsString(model), sourceStr);
0470 
0471         TestModel proxy(&model);
0472         QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.2[1.1.2.1*]]]]"));
0473 
0474         ModelSignalSpy spy(proxy);
0475         {
0476             QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1"));
0477             item_1_1_1_1->setData(true);
0478             QStandardItem *item_1_1_1 = model.item(0)->child(0)->child(0);
0479             item_1_1_1->appendRow(item_1_1_1_1);
0480         }
0481 
0482         QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1[1.1.1.1*] 1.1.2[1.1.2.1*]]]]"));
0483         QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsAboutToBeInserted(1.1.1)") << QStringLiteral("rowsInserted(1.1.1)"));
0484     }
0485 
0486     void testInsertWithChildren()
0487     {
0488         QStandardItemModel model;
0489         const QString sourceStr = QStringLiteral("[1[1.1]]");
0490         fillModel(model, sourceStr);
0491         QCOMPARE(treeAsString(model), sourceStr);
0492 
0493         TestModel proxy(&model);
0494         QCOMPARE(treeAsString(proxy), QString());
0495 
0496         ModelSignalSpy spy(proxy);
0497         {
0498             QStandardItem *item_1_1_1 = new QStandardItem(QStringLiteral("1.1.1"));
0499             QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1"));
0500             item_1_1_1_1->setData(true);
0501             item_1_1_1->appendRow(item_1_1_1_1);
0502 
0503             QStandardItem *item_1_1 = model.item(0)->child(0);
0504             item_1_1->appendRow(item_1_1_1);
0505         }
0506 
0507         QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1[1.1.1.1*]]]]"));
0508         QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsAboutToBeInserted(1)") << QStringLiteral("rowsInserted(1)"));
0509     }
0510 
0511     void testInsertIntoVisibleWithChildren()
0512     {
0513         QStandardItemModel model;
0514         const QString sourceStr = QStringLiteral("[1[1.1[1.1.1*]]]");
0515         fillModel(model, sourceStr);
0516         QCOMPARE(treeAsString(model), sourceStr);
0517 
0518         TestModel proxy(&model);
0519         QCOMPARE(treeAsString(proxy), sourceStr);
0520 
0521         ModelSignalSpy spy(proxy);
0522         {
0523             QStandardItem *item_1_1_2 = new QStandardItem(QStringLiteral("1.1.2"));
0524             QStandardItem *item_1_1_2_1 = new QStandardItem(QStringLiteral("1.1.2.1"));
0525             item_1_1_2_1->setData(true);
0526             item_1_1_2->appendRow(item_1_1_2_1);
0527 
0528             QStandardItem *item_1_1 = model.item(0)->child(0);
0529             item_1_1->appendRow(item_1_1_2);
0530         }
0531 
0532         QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]"));
0533         QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsAboutToBeInserted(1.1.2)") << QStringLiteral("rowsInserted(1.1.2)"));
0534     }
0535 
0536     void testInsertHidden() // inserting filtered-out rows shouldn't emit anything
0537     {
0538         QStandardItemModel model;
0539         const QString sourceStr = QStringLiteral("[1[1.1]]");
0540         fillModel(model, sourceStr);
0541         QCOMPARE(treeAsString(model), sourceStr);
0542 
0543         TestModel proxy(&model);
0544         QCOMPARE(treeAsString(proxy), QString());
0545 
0546         ModelSignalSpy spy(proxy);
0547         {
0548             QStandardItem *item_1_1_1 = new QStandardItem(QStringLiteral("1.1.1"));
0549             QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1"));
0550             item_1_1_1->appendRow(item_1_1_1_1);
0551 
0552             QStandardItem *item_1_1 = model.item(0)->child(0);
0553             item_1_1->appendRow(item_1_1_1);
0554         }
0555 
0556         QCOMPARE(treeAsString(proxy), QString());
0557         QCOMPARE(spy.mSignals, QStringList());
0558     }
0559 
0560     void testConsecutiveInserts_data()
0561     {
0562         testInitialFiltering_data();
0563     }
0564 
0565     void testConsecutiveInserts()
0566     {
0567         QFETCH(QString, sourceStr);
0568         QFETCH(QString, proxyStr);
0569 
0570         QStandardItemModel model;
0571         TestModel proxy(&model); // this time the proxy listens to the model while we fill it
0572 
0573         fillModel(model, sourceStr);
0574         QCOMPARE(treeAsString(model), sourceStr);
0575         QCOMPARE(treeAsString(proxy), proxyStr);
0576     }
0577 
0578     void testRemove_data()
0579     {
0580         QTest::addColumn<QString>("sourceStr");
0581         QTest::addColumn<QString>("initialProxyStr");
0582         QTest::addColumn<QString>("remove"); // remove this item
0583         QTest::addColumn<QString>("expectedProxyStr");
0584         QTest::addColumn<QStringList>("expectedSignals");
0585 
0586         const QStringList remove1_1_1 = (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1.1.1)") << QStringLiteral("rowsRemoved(1.1.1)"));
0587 
0588         QTest::newRow("toplevel") << "[1* 2* 3*]"
0589                                   << "[1* 2* 3*]"
0590                                   << "1"
0591                                   << "[2* 3*]" << (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1)") << QStringLiteral("rowsRemoved(1)"));
0592 
0593         QTest::newRow("remove_hidden") << "[1 2* 3*]"
0594                                        << "[2* 3*]"
0595                                        << "1"
0596                                        << "[2* 3*]" << QStringList();
0597 
0598         QTest::newRow("parent_hidden") << "[1[1.1[1.1.1]]]"
0599                                        << ""
0600                                        << "1.1.1"
0601                                        << "" << QStringList();
0602 
0603         QTest::newRow("child_hidden") << "[1[1.1*[1.1.1]]]"
0604                                       << "[1[1.1*]]"
0605                                       << "1.1.1"
0606                                       << "[1[1.1*]]" << QStringList();
0607 
0608         QTest::newRow("parent_visible") << "[1[1.1*[1.1.1*]]]"
0609                                         << "[1[1.1*[1.1.1*]]]"
0610                                         << "1.1.1"
0611                                         << "[1[1.1*]]" << remove1_1_1;
0612 
0613         QTest::newRow("visible") << "[1[1.1[1.1.1* 1.1.2*]]]"
0614                                  << "[1[1.1[1.1.1* 1.1.2*]]]"
0615                                  << "1.1.1"
0616                                  << "[1[1.1[1.1.2*]]]" << remove1_1_1;
0617         QTest::newRow("visible_cousin") << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]"
0618                                         << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]"
0619                                         << "1.1.1"
0620                                         << "[1[1.1[1.1.2[1.1.2.1*]]]]" << remove1_1_1;
0621 
0622         // The following tests trigger the removal of an ascendant.
0623         // We could optimize the rows{AboutToBe,}Removed(1.1.1) away, but that would
0624         // require a filterAcceptsRow variant that ignores the about-to-be-removed row,
0625         // in order to move the going-up loop from Removed to AboutToBeRemoved.
0626         // It doesn't really hurt to have both pairs of signals though.
0627 
0628         QTest::newRow("remove_parent") << "[1[1.1[1.1.1* 1.1.2] 1.2*]]"
0629                                        << "[1[1.1[1.1.1*] 1.2*]]"
0630                                        << "1.1.1"
0631                                        << "[1[1.2*]]"
0632                                        << (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1.1.1)") << QStringLiteral("rowsRemoved(1.1.1)")
0633                                                          << QStringLiteral("rowsAboutToBeRemoved(1.1)") << QStringLiteral("rowsRemoved(1.1)"));
0634 
0635         QTest::newRow("with_children") << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]"
0636                                        << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]"
0637                                        << "1.1.1"
0638                                        << "[2*]"
0639                                        << (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1.1.1)") << QStringLiteral("rowsRemoved(1.1.1)")
0640                                                          << QStringLiteral("rowsAboutToBeRemoved(1)") << QStringLiteral("rowsRemoved(1)"));
0641 
0642         QTest::newRow("last_visible") << "[1[1.1[1.1.1* 1.1.2]]]"
0643                                       << "[1[1.1[1.1.1*]]]"
0644                                       << "1.1.1"
0645                                       << ""
0646                                       << (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1.1.1)") << QStringLiteral("rowsRemoved(1.1.1)")
0647                                                         << QStringLiteral("rowsAboutToBeRemoved(1)") << QStringLiteral("rowsRemoved(1)"));
0648     }
0649 
0650     void testRemove()
0651     {
0652         QFETCH(QString, sourceStr);
0653         QFETCH(QString, initialProxyStr);
0654         QFETCH(QString, remove);
0655         QFETCH(QString, expectedProxyStr);
0656         QFETCH(QStringList, expectedSignals);
0657 
0658         QStandardItemModel model;
0659         fillModel(model, sourceStr);
0660         QCOMPARE(treeAsString(model), sourceStr);
0661 
0662         TestModel proxy(&model);
0663         QCOMPARE(treeAsString(proxy), initialProxyStr);
0664 
0665         ModelSignalSpy spy(proxy);
0666         QStandardItem *itemToRemove = itemByText(model, remove);
0667         QVERIFY(itemToRemove);
0668         if (itemToRemove->parent()) {
0669             itemToRemove->parent()->removeRow(itemToRemove->row());
0670         } else {
0671             model.removeRow(itemToRemove->row());
0672         }
0673         QCOMPARE(treeAsString(proxy), expectedProxyStr);
0674 
0675         // qDebug() << spy.mSignals;
0676         QCOMPARE(spy.mSignals, expectedSignals);
0677     }
0678 
0679 private:
0680     QStandardItem *itemByText(const QStandardItemModel &model, const QString &text) const
0681     {
0682         QModelIndexList list = model.match(model.index(0, 0), Qt::DisplayRole, text, 1, Qt::MatchRecursive);
0683         return list.isEmpty() ? nullptr : model.itemFromIndex(list.first());
0684     }
0685 };
0686 
0687 QTEST_MAIN(KRecursiveFilterProxyModelTest)
0688 
0689 #include "krecursivefilterproxymodeltest.moc"