+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+ QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved);
+
+ QSet<QByteArray> modelRoles = m_model->roles();
+ modelRoles << "isExpanded"
+ << "isExpandable"
+ << "expandedParentsCount";
+ m_model->setRoles(modelRoles);
+
+ m_testDir->createFile("a/b/someFolder/someFile");
+
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+
+ QCOMPARE(m_model->count(), 1); // Only "a/"
+
+ // Expand "a/".
+ m_model->setExpanded(0, true);
+ QVERIFY(itemsInsertedSpy.wait());
+
+ // Expand "a/b/".
+ m_model->setExpanded(1, true);
+ QVERIFY(itemsInsertedSpy.wait());
+
+ // Expand "a/b/someFolder/".
+ m_model->setExpanded(2, true);
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), 4); // 4 items: "a/", "a/b/", "a/b/someFolder", "a/b/someFolder/someFile"
+
+ // Set a filter that matches the expanded folder "someFolder"
+ m_model->setNameFilter("someFolder");
+ QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/b/someFolder"
+
+ // Objects used to rename
+ const KFileItem fileItemA = m_model->fileItem(0);
+ KFileItem fileItemARenamed = fileItemA;
+ fileItemARenamed.setUrl(QUrl("a_renamed"));
+
+ const KFileItem fileItemSomeFolder = m_model->fileItem(2);
+ KFileItem fileItemRenamedFolder = fileItemSomeFolder;
+ fileItemRenamedFolder.setUrl(QUrl("/a_renamed/b/renamedFolder"));
+
+ // Rename "a" to "a_renamed"
+ // This way we test if the algorithm is sane as to NOT hide "a_renamed" since it will have visible children
+ m_model->slotRefreshItems({qMakePair(fileItemA, fileItemARenamed)});
+ QCOMPARE(m_model->count(), 3); // Entire parental chain must still be shown
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a_renamed"
+ << "b"
+ << "someFolder");
+
+ // Rename "a_renamed" back to "a"; and "someFolder" to "renamedFolder"
+ m_model->slotRefreshItems({qMakePair(fileItemARenamed, fileItemA), qMakePair(fileItemSomeFolder, fileItemRenamedFolder)});
+ QCOMPARE(m_model->count(), 0); // Entire parental chain became hidden
+
+ // Rename "renamedFolder" back to "someFolder". Filter is passing again
+ m_model->slotRefreshItems({qMakePair(fileItemRenamedFolder, fileItemSomeFolder)});
+ QCOMPARE(m_model->count(), 3); // Entire parental chain is shown again
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a"
+ << "b"
+ << "someFolder");
+
+ // slotRefreshItems() should preserve "isExpanded" and "expandedParentsCount" values explicitly in this case
+ QCOMPARE(m_model->m_itemData.at(2)->values.value("isExpanded").toBool(), true);
+ QCOMPARE(m_model->m_itemData.at(2)->values.value("expandedParentsCount"), 2);
+}
+
+/**
+ * Verify that removing hidden files and folders from the model does not
+ * result in a crash, see https://bugs.kde.org/show_bug.cgi?id=314046
+ */
+void KFileItemModelTest::testRemoveHiddenItems()
+{
+ m_testDir->createDir(".a");
+ m_testDir->createDir(".b");
+ m_testDir->createDir("c");
+ m_testDir->createDir("d");
+ m_testDir->createFiles({".f", ".g", "h", "i"});
+
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+ QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved);
+
+ m_model->setShowHiddenFiles(true);
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << ".a"
+ << ".b"
+ << "c"
+ << "d"
+ << ".f"
+ << ".g"
+ << "h"
+ << "i");
+ QCOMPARE(itemsInsertedSpy.count(), 1);
+ QCOMPARE(itemsRemovedSpy.count(), 0);
+ KItemRangeList itemRangeList = itemsInsertedSpy.takeFirst().at(0).value<KItemRangeList>();
+ QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 8));
+
+ m_model->setShowHiddenFiles(false);
+ QCOMPARE(itemsInModel(),
+ QStringList() << "c"
+ << "d"
+ << "h"
+ << "i");
+ QCOMPARE(itemsInsertedSpy.count(), 0);
+ QCOMPARE(itemsRemovedSpy.count(), 1);
+ itemRangeList = itemsRemovedSpy.takeFirst().at(0).value<KItemRangeList>();
+ QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 2) << KItemRange(4, 2));
+
+ m_model->setShowHiddenFiles(true);
+ QCOMPARE(itemsInModel(),
+ QStringList() << ".a"
+ << ".b"
+ << "c"
+ << "d"
+ << ".f"
+ << ".g"
+ << "h"
+ << "i");
+ QCOMPARE(itemsInsertedSpy.count(), 1);
+ QCOMPARE(itemsRemovedSpy.count(), 0);
+ itemRangeList = itemsInsertedSpy.takeFirst().at(0).value<KItemRangeList>();
+ QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 2) << KItemRange(2, 2));
+
+ m_model->clear();
+ QCOMPARE(itemsInModel(), QStringList());
+ QCOMPARE(itemsInsertedSpy.count(), 0);
+ QCOMPARE(itemsRemovedSpy.count(), 1);
+ itemRangeList = itemsRemovedSpy.takeFirst().at(0).value<KItemRangeList>();
+ QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 8));
+
+ // Hiding hidden files makes the dir lister emit its itemsDeleted signal.
+ // Verify that this does not make the model crash.
+ m_model->setShowHiddenFiles(false);
+}
+
+/**
+ * Verify that filtered items are removed when their parent is collapsed.
+ */
+void KFileItemModelTest::collapseParentOfHiddenItems()
+{
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+ QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved);
+
+ QSet<QByteArray> modelRoles = m_model->roles();
+ modelRoles << "isExpanded"
+ << "isExpandable"
+ << "expandedParentsCount";
+ m_model->setRoles(modelRoles);
+
+ m_testDir->createFiles({"a/1", "a/b/1", "a/b/c/1", "a/b/c/d/1"});
+
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), 1); // Only "a/"
+
+ // Expand "a/".
+ m_model->setExpanded(0, true);
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/1"
+
+ // Expand "a/b/".
+ m_model->setExpanded(1, true);
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), 5); // 5 items: "a/", "a/b/", "a/b/c", "a/b/1", "a/1"
+
+ // Expand "a/b/c/".
+ m_model->setExpanded(2, true);
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), 7); // 7 items: "a/", "a/b/", "a/b/c", "a/b/c/d/", "a/b/c/1", "a/b/1", "a/1"
+
+ // Set a name filter that matches nothing -> nothing should remain.
+ m_model->setNameFilter("xyz");
+ QCOMPARE(itemsRemovedSpy.count(), 1);
+ QCOMPARE(m_model->count(), 0); //Everything is hidden
+ QCOMPARE(itemsInModel(), QStringList());
+
+ //Filter by the file names. Folder "d" will be hidden since it was collapsed
+ m_model->setNameFilter("1");
+ QCOMPARE(itemsRemovedSpy.count(), 1); // nothing was removed, itemsRemovedSpy count will remain the same:
+ QCOMPARE(m_model->count(), 6); // 6 items: "a/", "a/b/", "a/b/c", "a/b/c/1", "a/b/1", "a/1"
+
+ // Collapse the folder "a/".
+ m_model->setExpanded(0, false);
+ QCOMPARE(itemsRemovedSpy.count(), 2);
+ QCOMPARE(m_model->count(), 1);
+ QCOMPARE(itemsInModel(), QStringList() << "a");
+
+ // Remove the filter -> "a" should still appear (and we should not get a crash).
+ m_model->setNameFilter(QString());
+ QCOMPARE(itemsRemovedSpy.count(), 2); // nothing was removed, itemsRemovedSpy count will remain the same:
+ QCOMPARE(m_model->count(), 1);
+ QCOMPARE(itemsInModel(), QStringList() << "a");
+}
+
+/**
+ * Verify that filtered items are removed when their parent is deleted.
+ */
+void KFileItemModelTest::removeParentOfHiddenItems()
+{
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+ QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved);
+
+ QSet<QByteArray> modelRoles = m_model->roles();
+ modelRoles << "isExpanded"
+ << "isExpandable"
+ << "expandedParentsCount";
+ m_model->setRoles(modelRoles);
+
+ m_testDir->createFiles({"a/1", "a/b/1", "a/b/c/1", "a/b/c/d/1"});
+
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), 1); // Only "a/"
+
+ // Expand "a/".
+ m_model->setExpanded(0, true);
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/1"
+
+ // Expand "a/b/".
+ m_model->setExpanded(1, true);
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), 5); // 5 items: "a/", "a/b/", "a/b/c", "a/b/1", "a/1"
+
+ // Expand "a/b/c/".
+ m_model->setExpanded(2, true);
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), 7); // 7 items: "a/", "a/b/", "a/b/c", "a/b/c/d/", "a/b/c/1", "a/b/1", "a/1"
+
+ // Set a name filter that matches nothing -> nothing should remain.
+ m_model->setNameFilter("xyz");
+ QCOMPARE(itemsRemovedSpy.count(), 1);
+ QCOMPARE(m_model->count(), 0);
+ QCOMPARE(itemsInModel(), QStringList());
+
+ // Filter by "c". Folder "b" will also be shown because it is its parent.
+ m_model->setNameFilter("c");
+ QCOMPARE(itemsRemovedSpy.count(), 1); // nothing was removed, itemsRemovedSpy count will remain the same:
+ QCOMPARE(m_model->count(), 3);
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a"
+ << "b"
+ << "c");
+
+ // Simulate the deletion of the directory "a/b/".
+ m_model->slotItemsDeleted(KFileItemList() << m_model->fileItem(1));
+ QCOMPARE(itemsRemovedSpy.count(), 2);
+ QCOMPARE(m_model->count(), 0); // "a" will be filtered out since it doesn't pass the filter and doesn't have visible children
+
+ // Remove the filter -> only the file "a/1" should appear.
+ m_model->setNameFilter(QString());
+ QCOMPARE(m_model->count(), 2);
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a"
+ << "1");
+}
+
+/**
+ * Create a tree structure where parent-child relationships can not be
+ * determined by parsing the URLs, and verify that KFileItemModel
+ * handles them correctly.
+ */
+void KFileItemModelTest::testGeneralParentChildRelationships()
+{
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+ QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved);
+
+ QSet<QByteArray> modelRoles = m_model->roles();
+ modelRoles << "isExpanded"
+ << "isExpandable"
+ << "expandedParentsCount";
+ m_model->setRoles(modelRoles);
+
+ m_testDir->createFiles({"parent1/realChild1/realGrandChild1", "parent2/realChild2/realGrandChild2"});
+
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "parent1"
+ << "parent2");
+
+ // Expand all folders.
+ m_model->setExpanded(0, true);
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "parent1"
+ << "realChild1"
+ << "parent2");
+
+ m_model->setExpanded(1, true);
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "parent1"
+ << "realChild1"
+ << "realGrandChild1"
+ << "parent2");
+
+ m_model->setExpanded(3, true);
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "parent1"
+ << "realChild1"
+ << "realGrandChild1"
+ << "parent2"
+ << "realChild2");
+
+ m_model->setExpanded(4, true);
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "parent1"
+ << "realChild1"
+ << "realGrandChild1"
+ << "parent2"
+ << "realChild2"
+ << "realGrandChild2");
+
+ // Add some more children and grand-children.
+ const QUrl parent1 = m_model->fileItem(0).url();
+ const QUrl parent2 = m_model->fileItem(3).url();
+ const QUrl realChild1 = m_model->fileItem(1).url();
+ const QUrl realChild2 = m_model->fileItem(4).url();
+
+ m_model->slotItemsAdded(parent1, KFileItemList() << KFileItem(QUrl("child1"), QString(), KFileItem::Unknown));
+ m_model->slotCompleted();
+ QCOMPARE(itemsInModel(),
+ QStringList() << "parent1"
+ << "realChild1"
+ << "realGrandChild1"
+ << "child1"
+ << "parent2"
+ << "realChild2"
+ << "realGrandChild2");
+
+ m_model->slotItemsAdded(parent2, KFileItemList() << KFileItem(QUrl("child2"), QString(), KFileItem::Unknown));
+ m_model->slotCompleted();
+ QCOMPARE(itemsInModel(),
+ QStringList() << "parent1"
+ << "realChild1"
+ << "realGrandChild1"
+ << "child1"
+ << "parent2"
+ << "realChild2"
+ << "realGrandChild2"
+ << "child2");
+
+ m_model->slotItemsAdded(realChild1, KFileItemList() << KFileItem(QUrl("grandChild1"), QString(), KFileItem::Unknown));
+ m_model->slotCompleted();
+ QCOMPARE(itemsInModel(),
+ QStringList() << "parent1"
+ << "realChild1"
+ << "grandChild1"
+ << "realGrandChild1"
+ << "child1"
+ << "parent2"
+ << "realChild2"
+ << "realGrandChild2"
+ << "child2");
+
+ m_model->slotItemsAdded(realChild2, KFileItemList() << KFileItem(QUrl("grandChild2"), QString(), KFileItem::Unknown));
+ m_model->slotCompleted();
+ QCOMPARE(itemsInModel(),
+ QStringList() << "parent1"
+ << "realChild1"
+ << "grandChild1"
+ << "realGrandChild1"
+ << "child1"
+ << "parent2"
+ << "realChild2"
+ << "grandChild2"
+ << "realGrandChild2"
+ << "child2");
+
+ // Set a name filter that matches nothing -> nothing will remain.
+ m_model->setNameFilter("xyz");
+ QCOMPARE(itemsInModel(), QStringList());
+ QCOMPARE(itemsRemovedSpy.count(), 1);
+ QList<QVariant> arguments = itemsRemovedSpy.takeFirst();
+ KItemRangeList itemRangeList = arguments.at(0).value<KItemRangeList>();
+ QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 10));
+
+ // Set a name filter that matches only "realChild". Their prarents should still show.
+ m_model->setNameFilter("realChild");
+ QCOMPARE(itemsInModel(),
+ QStringList() << "parent1"
+ << "realChild1"
+ << "parent2"
+ << "realChild2");
+ QCOMPARE(itemsRemovedSpy.count(), 0); // nothing was removed, itemsRemovedSpy will not be called this time
+
+ // Collapse "parent1".
+ m_model->setExpanded(0, false);
+ QCOMPARE(itemsInModel(),
+ QStringList() << "parent1"
+ << "parent2"
+ << "realChild2");
+ QCOMPARE(itemsRemovedSpy.count(), 1);
+ arguments = itemsRemovedSpy.takeFirst();
+ itemRangeList = arguments.at(0).value<KItemRangeList>();
+ QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 1));
+
+ // Remove "parent2".
+ m_model->slotItemsDeleted(KFileItemList() << m_model->fileItem(1));
+ QCOMPARE(itemsInModel(), QStringList() << "parent1");
+ QCOMPARE(itemsRemovedSpy.count(), 1);
+ arguments = itemsRemovedSpy.takeFirst();
+ itemRangeList = arguments.at(0).value<KItemRangeList>();
+ QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 2));
+
+ // Clear filter, verify that no items reappear.
+ m_model->setNameFilter(QString());
+ QCOMPARE(itemsInModel(), QStringList() << "parent1");
+}
+
+void KFileItemModelTest::testNameRoleGroups()
+{
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+ QSignalSpy itemsMovedSpy(m_model, &KFileItemModel::itemsMoved);
+ QVERIFY(itemsMovedSpy.isValid());
+ QSignalSpy groupsChangedSpy(m_model, &KFileItemModel::groupsChanged);
+ QVERIFY(groupsChangedSpy.isValid());
+
+ m_testDir->createFiles({"b.txt", "c.txt", "d.txt", "e.txt"});
+
+ m_model->setGroupedSorting(true);
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "b.txt"
+ << "c.txt"
+ << "d.txt"
+ << "e.txt");
+
+ QList<QPair<int, QVariant>> expectedGroups;
+ expectedGroups << QPair<int, QVariant>(0, QLatin1String("B"));
+ expectedGroups << QPair<int, QVariant>(1, QLatin1String("C"));
+ expectedGroups << QPair<int, QVariant>(2, QLatin1String("D"));
+ expectedGroups << QPair<int, QVariant>(3, QLatin1String("E"));
+ QCOMPARE(m_model->groups(), expectedGroups);
+
+ // Rename d.txt to a.txt.
+ QHash<QByteArray, QVariant> data;
+ data.insert("text", "a.txt");
+ m_model->setData(2, data);
+ QVERIFY(itemsMovedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a.txt"
+ << "b.txt"
+ << "c.txt"
+ << "e.txt");
+
+ expectedGroups.clear();
+ expectedGroups << QPair<int, QVariant>(0, QLatin1String("A"));
+ expectedGroups << QPair<int, QVariant>(1, QLatin1String("B"));
+ expectedGroups << QPair<int, QVariant>(2, QLatin1String("C"));
+ expectedGroups << QPair<int, QVariant>(3, QLatin1String("E"));
+ QCOMPARE(m_model->groups(), expectedGroups);
+
+ // Rename c.txt to d.txt.
+ data.insert("text", "d.txt");
+ m_model->setData(2, data);
+ QVERIFY(groupsChangedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a.txt"
+ << "b.txt"
+ << "d.txt"
+ << "e.txt");
+
+ expectedGroups.clear();
+ expectedGroups << QPair<int, QVariant>(0, QLatin1String("A"));
+ expectedGroups << QPair<int, QVariant>(1, QLatin1String("B"));
+ expectedGroups << QPair<int, QVariant>(2, QLatin1String("D"));
+ expectedGroups << QPair<int, QVariant>(3, QLatin1String("E"));
+ QCOMPARE(m_model->groups(), expectedGroups);
+
+ // Change d.txt back to c.txt, but this time using the dir lister's refreshItems() signal.
+ const KFileItem fileItemD = m_model->fileItem(2);
+ KFileItem fileItemC = fileItemD;
+ QUrl urlC = fileItemC.url().adjusted(QUrl::RemoveFilename);
+ urlC.setPath(urlC.path() + "c.txt");
+ fileItemC.setUrl(urlC);
+
+ m_model->slotRefreshItems({qMakePair(fileItemD, fileItemC)});
+ QVERIFY(groupsChangedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a.txt"
+ << "b.txt"
+ << "c.txt"
+ << "e.txt");
+
+ expectedGroups.clear();
+ expectedGroups << QPair<int, QVariant>(0, QLatin1String("A"));
+ expectedGroups << QPair<int, QVariant>(1, QLatin1String("B"));
+ expectedGroups << QPair<int, QVariant>(2, QLatin1String("C"));
+ expectedGroups << QPair<int, QVariant>(3, QLatin1String("E"));
+ QCOMPARE(m_model->groups(), expectedGroups);
+}
+
+void KFileItemModelTest::testNameRoleGroupsWithExpandedItems()
+{
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+
+ QSet<QByteArray> modelRoles = m_model->roles();
+ modelRoles << "isExpanded"
+ << "isExpandable"
+ << "expandedParentsCount";
+ m_model->setRoles(modelRoles);
+
+ m_testDir->createFiles({"a/b.txt", "a/c.txt", "d/e.txt", "d/f.txt"});
+
+ m_model->setGroupedSorting(true);
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a"
+ << "d");
+
+ QList<QPair<int, QVariant>> expectedGroups;
+ expectedGroups << QPair<int, QVariant>(0, QLatin1String("A"));
+ expectedGroups << QPair<int, QVariant>(1, QLatin1String("D"));
+ QCOMPARE(m_model->groups(), expectedGroups);
+
+ // Verify that expanding "a" and "d" will not change the groups (except for the index of "D").
+ expectedGroups.clear();
+ expectedGroups << QPair<int, QVariant>(0, QLatin1String("A"));
+ expectedGroups << QPair<int, QVariant>(3, QLatin1String("D"));
+
+ m_model->setExpanded(0, true);
+ QVERIFY(m_model->isExpanded(0));
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a"
+ << "b.txt"
+ << "c.txt"
+ << "d");
+ QCOMPARE(m_model->groups(), expectedGroups);
+
+ m_model->setExpanded(3, true);
+ QVERIFY(m_model->isExpanded(3));
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a"
+ << "b.txt"
+ << "c.txt"
+ << "d"
+ << "e.txt"
+ << "f.txt");
+ QCOMPARE(m_model->groups(), expectedGroups);
+}
+
+void KFileItemModelTest::testInconsistentModel()
+{
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+
+ QSet<QByteArray> modelRoles = m_model->roles();
+ modelRoles << "isExpanded"
+ << "isExpandable"
+ << "expandedParentsCount";
+ m_model->setRoles(modelRoles);
+
+ m_testDir->createFiles({"a/b/c1.txt", "a/b/c2.txt"});
+
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(), QStringList() << "a");
+
+ // Expand "a/" and "a/b/".
+ m_model->setExpanded(0, true);
+ QVERIFY(m_model->isExpanded(0));
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a"
+ << "b");
+
+ m_model->setExpanded(1, true);
+ QVERIFY(m_model->isExpanded(1));
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a"
+ << "b"
+ << "c1.txt"
+ << "c2.txt");
+
+ // Add the files "c1.txt" and "c2.txt" to the model also as top-level items.
+ // Such a thing can in principle happen when performing a search, and there
+ // are files which
+ // (a) match the search string, and
+ // (b) are children of a folder that matches the search string and is expanded.
+ //
+ // Note that the first item in the list of added items must be new (i.e., not
+ // in the model yet). Otherwise, KFileItemModel::slotItemsAdded() will see that
+ // it receives items that are in the model already and ignore them.
+ QUrl url(m_model->directory().url() + "/a2");
+ KFileItem newItem(url);
+
+ KFileItemList items;
+ items << newItem << m_model->fileItem(2) << m_model->fileItem(3);
+ m_model->slotItemsAdded(m_model->directory(), items);
+ m_model->slotCompleted();
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a"
+ << "b"
+ << "c1.txt"
+ << "c2.txt"
+ << "a2"
+ << "c1.txt"
+ << "c2.txt");
+
+ m_model->setExpanded(0, false);
+
+ // Test that the right items have been removed, see
+ // https://bugs.kde.org/show_bug.cgi?id=324371
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a"
+ << "a2"
+ << "c1.txt"
+ << "c2.txt");
+
+ // Test that resorting does not cause a crash, see
+ // https://bugs.kde.org/show_bug.cgi?id=325359
+ // The crash is not 100% reproducible, but Valgrind will report an invalid memory access.
+ m_model->resortAllItems();
+}
+
+void KFileItemModelTest::testChangeRolesForFilteredItems()
+{
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+
+ QSet<QByteArray> modelRoles = m_model->roles();
+ modelRoles << "owner";
+ m_model->setRoles(modelRoles);
+
+ m_testDir->createFiles({"a.txt", "aa.txt", "aaa.txt"});
+
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(),
+ QStringList() << "a.txt"
+ << "aa.txt"
+ << "aaa.txt");
+
+ for (int index = 0; index < m_model->count(); ++index) {
+ // All items should have the "text" and "owner" roles, but not "group".
+ QVERIFY(m_model->data(index).contains("text"));
+ QVERIFY(m_model->data(index).contains("owner"));
+ QVERIFY(!m_model->data(index).contains("group"));