+ QSet<QByteArray> originalModelRoles = m_model->roles();
+ QSet<QByteArray> modelRoles = originalModelRoles;
+ modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount";
+ m_model->setRoles(modelRoles);
+
+ m_testDir->createFiles({"folder/child", "file"});
+
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+
+ // So far, the model contains only "folder/" and "file".
+ QCOMPARE(m_model->count(), 2);
+ QVERIFY(m_model->isExpandable(0));
+ QVERIFY(!m_model->isExpandable(1));
+ QVERIFY(!m_model->isExpanded(0));
+ QVERIFY(!m_model->isExpanded(1));
+ QCOMPARE(itemsInModel(), QStringList() << "folder" << "file");
+
+ // Expand "folder" -> "folder/child" becomes visible.
+ m_model->setExpanded(0, true);
+ QVERIFY(m_model->isExpanded(0));
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInModel(), QStringList() << "folder" << "child" << "file");
+
+ // Add a name filter.
+ m_model->setNameFilter("f");
+ QCOMPARE(itemsInModel(), QStringList() << "folder" << "file");
+
+ m_model->setNameFilter("fo");
+ QCOMPARE(itemsInModel(), QStringList() << "folder");
+
+ // Remove all expanded items by changing the roles
+ m_model->setRoles(originalModelRoles);
+ QVERIFY(!m_model->isExpanded(0));
+ QCOMPARE(itemsInModel(), QStringList() << "folder");
+
+ // Remove the name filter and verify that "folder/child" does not reappear.
+ m_model->setNameFilter(QString());
+ QCOMPARE(itemsInModel(), QStringList() << "folder" << "file");
+}
+
+void KFileItemModelTest::testSorting()
+{
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+ QSignalSpy itemsMovedSpy(m_model, &KFileItemModel::itemsMoved);
+ QVERIFY(itemsMovedSpy.isValid());
+
+ // Create some files with different sizes and modification times to check the different sorting options
+ QDateTime now = QDateTime::currentDateTime();
+
+ QSet<QByteArray> roles;
+ roles.insert("text");
+ roles.insert("isExpanded");
+ roles.insert("isExpandable");
+ roles.insert("expandedParentsCount");
+ m_model->setRoles(roles);
+
+ m_testDir->createDir("c/c-2");
+ m_testDir->createFile("c/c-2/c-3");
+ m_testDir->createFile("c/c-1");
+
+ m_testDir->createFile("a", "A file", now.addDays(-3));
+ m_testDir->createFile("b", "A larger file", now.addDays(0));
+ m_testDir->createDir("c", now.addDays(-2));
+ m_testDir->createFile("d", "The largest file in this directory", now.addDays(-1));
+ m_testDir->createFile("e", "An even larger file", now.addDays(-4));
+ m_testDir->createFile(".f");
+
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+
+ int index = m_model->index(QUrl(m_testDir->url().url() + "/c"));
+ m_model->setExpanded(index, true);
+ QVERIFY(itemsInsertedSpy.wait());
+
+ index = m_model->index(QUrl(m_testDir->url().url() + "/c/c-2"));
+ m_model->setExpanded(index, true);
+ QVERIFY(itemsInsertedSpy.wait());
+
+ // Default: Sort by Name, ascending
+ QCOMPARE(m_model->sortRole(), QByteArray("text"));
+ QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder);
+ QVERIFY(m_model->sortDirectoriesFirst());
+ QVERIFY(!m_model->showHiddenFiles());
+ QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "a" << "b" << "d" << "e");
+
+ // Sort by Name, ascending, 'Sort Folders First' disabled
+ m_model->setSortDirectoriesFirst(false);
+ QCOMPARE(m_model->sortRole(), QByteArray("text"));
+ QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder);
+ QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c" << "c-1" << "c-2" << "c-3" << "d" << "e");
+ QCOMPARE(itemsMovedSpy.count(), 1);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(0, 6));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 2 << 4 << 5 << 3 << 0 << 1);
+
+ // Sort by Name, descending
+ m_model->setSortDirectoriesFirst(true);
+ m_model->setSortOrder(Qt::DescendingOrder);
+ QCOMPARE(m_model->sortRole(), QByteArray("text"));
+ QCOMPARE(m_model->sortOrder(), Qt::DescendingOrder);
+ QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "e" << "d" << "b" << "a");
+ QCOMPARE(itemsMovedSpy.count(), 2);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(0, 6));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 4 << 5 << 0 << 3 << 1 << 2);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(4, 4));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 7 << 6 << 5 << 4);
+
+ // Sort by Date, descending
+ m_model->setSortDirectoriesFirst(true);
+ m_model->setSortRole("modificationtime");
+ QCOMPARE(m_model->sortRole(), QByteArray("modificationtime"));
+ QCOMPARE(m_model->sortOrder(), Qt::DescendingOrder);
+ QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "b" << "d" << "a" << "e");
+ QCOMPARE(itemsMovedSpy.count(), 1);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(4, 4));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 7 << 5 << 4 << 6);
+
+ // Sort by Date, ascending
+ m_model->setSortOrder(Qt::AscendingOrder);
+ QCOMPARE(m_model->sortRole(), QByteArray("modificationtime"));
+ QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder);
+ QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "e" << "a" << "d" << "b");
+ QCOMPARE(itemsMovedSpy.count(), 1);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(4, 4));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 7 << 6 << 5 << 4);
+
+ // Sort by Date, ascending, 'Sort Folders First' disabled
+ m_model->setSortDirectoriesFirst(false);
+ QCOMPARE(m_model->sortRole(), QByteArray("modificationtime"));
+ QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder);
+ QVERIFY(!m_model->sortDirectoriesFirst());
+ QCOMPARE(itemsInModel(), QStringList() << "e" << "a" << "c" << "c-1" << "c-2" << "c-3" << "d" << "b");
+ QCOMPARE(itemsMovedSpy.count(), 1);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(0, 6));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 2 << 4 << 5 << 3 << 0 << 1);
+
+ // Sort by Name, ascending, 'Sort Folders First' disabled
+ m_model->setSortRole("text");
+ QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder);
+ QVERIFY(!m_model->sortDirectoriesFirst());
+ QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c" << "c-1" << "c-2" << "c-3" << "d" << "e");
+ QCOMPARE(itemsMovedSpy.count(), 1);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(0, 8));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 7 << 0 << 2 << 3 << 4 << 5 << 6 << 1);
+
+ // Sort by Size, ascending, 'Sort Folders First' disabled
+ m_model->setSortRole("size");
+ QCOMPARE(m_model->sortRole(), QByteArray("size"));
+ QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder);
+ QVERIFY(!m_model->sortDirectoriesFirst());
+ QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "a" << "b" << "e" << "d");
+ QCOMPARE(itemsMovedSpy.count(), 1);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(0, 8));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 4 << 5 << 0 << 3 << 1 << 2 << 7 << 6);
+
+ // In 'Sort by Size' mode, folders are always first -> changing 'Sort Folders First' does not resort the model
+ m_model->setSortDirectoriesFirst(true);
+ QCOMPARE(m_model->sortRole(), QByteArray("size"));
+ QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder);
+ QVERIFY(m_model->sortDirectoriesFirst());
+ QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "a" << "b" << "e" << "d");
+ QCOMPARE(itemsMovedSpy.count(), 0);
+
+ // Sort by Size, descending, 'Sort Folders First' enabled
+ m_model->setSortOrder(Qt::DescendingOrder);
+ QCOMPARE(m_model->sortRole(), QByteArray("size"));
+ QCOMPARE(m_model->sortOrder(), Qt::DescendingOrder);
+ QVERIFY(m_model->sortDirectoriesFirst());
+ QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "d" << "e" << "b" << "a");
+ QCOMPARE(itemsMovedSpy.count(), 1);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(4, 4));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 7 << 6 << 5 << 4);
+
+ // TODO: Sort by other roles; show/hide hidden files
+}
+
+void KFileItemModelTest::testIndexForKeyboardSearch()
+{
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+
+ m_testDir->createFiles({"a", "aa", "Image.jpg", "Image.png", "Text", "Text1", "Text2", "Text11"});
+
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+
+ // Search from index 0
+ QCOMPARE(m_model->indexForKeyboardSearch("a", 0), 0);
+ QCOMPARE(m_model->indexForKeyboardSearch("aa", 0), 1);
+ QCOMPARE(m_model->indexForKeyboardSearch("i", 0), 2);
+ QCOMPARE(m_model->indexForKeyboardSearch("image", 0), 2);
+ QCOMPARE(m_model->indexForKeyboardSearch("image.jpg", 0), 2);
+ QCOMPARE(m_model->indexForKeyboardSearch("image.png", 0), 3);
+ QCOMPARE(m_model->indexForKeyboardSearch("t", 0), 4);
+ QCOMPARE(m_model->indexForKeyboardSearch("text", 0), 4);
+ QCOMPARE(m_model->indexForKeyboardSearch("text1", 0), 5);
+ QCOMPARE(m_model->indexForKeyboardSearch("text2", 0), 6);
+ QCOMPARE(m_model->indexForKeyboardSearch("text11", 0), 7);
+
+ // Start a search somewhere in the middle
+ QCOMPARE(m_model->indexForKeyboardSearch("a", 1), 1);
+ QCOMPARE(m_model->indexForKeyboardSearch("i", 3), 3);
+ QCOMPARE(m_model->indexForKeyboardSearch("t", 5), 5);
+ QCOMPARE(m_model->indexForKeyboardSearch("text1", 6), 7);
+
+ // Test searches that go past the last item back to index 0
+ QCOMPARE(m_model->indexForKeyboardSearch("a", 2), 0);
+ QCOMPARE(m_model->indexForKeyboardSearch("i", 7), 2);
+ QCOMPARE(m_model->indexForKeyboardSearch("image.jpg", 3), 2);
+ QCOMPARE(m_model->indexForKeyboardSearch("text2", 7), 6);
+
+ // Test searches that yield no result
+ QCOMPARE(m_model->indexForKeyboardSearch("aaa", 0), -1);
+ QCOMPARE(m_model->indexForKeyboardSearch("b", 0), -1);
+ QCOMPARE(m_model->indexForKeyboardSearch("image.svg", 0), -1);
+ QCOMPARE(m_model->indexForKeyboardSearch("text3", 0), -1);
+ QCOMPARE(m_model->indexForKeyboardSearch("text3", 5), -1);
+
+ // Test upper case searches (note that search is case insensitive)
+ QCOMPARE(m_model->indexForKeyboardSearch("A", 0), 0);
+ QCOMPARE(m_model->indexForKeyboardSearch("aA", 0), 1);
+ QCOMPARE(m_model->indexForKeyboardSearch("TexT", 5), 5);
+ QCOMPARE(m_model->indexForKeyboardSearch("IMAGE", 4), 2);
+
+ // TODO: Maybe we should also test keyboard searches in directories which are not sorted by Name?
+}
+
+void KFileItemModelTest::testNameFilter()
+{
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+
+ m_testDir->createFiles({"A1", "A2", "Abc", "Bcd", "Cde"});
+
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+
+ m_model->setNameFilter("A"); // Shows A1, A2 and Abc
+ QCOMPARE(m_model->count(), 3);
+
+ m_model->setNameFilter("A2"); // Shows only A2
+ QCOMPARE(m_model->count(), 1);
+
+ m_model->setNameFilter("A2"); // Shows only A1
+ QCOMPARE(m_model->count(), 1);
+
+ m_model->setNameFilter("Bc"); // Shows "Abc" and "Bcd"
+ QCOMPARE(m_model->count(), 2);
+
+ m_model->setNameFilter("bC"); // Shows "Abc" and "Bcd"
+ QCOMPARE(m_model->count(), 2);
+
+ m_model->setNameFilter(QString()); // Shows again all items
+ QCOMPARE(m_model->count(), 5);
+}
+
+/**
+ * Verifies that we do not crash when adding a KFileItem with an empty path.
+ * Before this issue was fixed, KFileItemModel::expandedParentsCountCompare()
+ * tried to always read the first character of the path, even if the path is empty.
+ */
+void KFileItemModelTest::testEmptyPath()
+{
+ QSet<QByteArray> roles;
+ roles.insert("text");
+ roles.insert("isExpanded");
+ roles.insert("isExpandable");
+ roles.insert("expandedParentsCount");
+ m_model->setRoles(roles);
+
+ const QUrl emptyUrl;
+ QVERIFY(emptyUrl.path().isEmpty());
+
+ const QUrl url("file:///test/");
+
+ KFileItemList items;
+ items << KFileItem(emptyUrl, QString(), KFileItem::Unknown) << KFileItem(url, QString(), KFileItem::Unknown);
+ m_model->slotItemsAdded(emptyUrl, items);
+ m_model->slotCompleted();
+}
+
+/**
+ * Verifies that the 'isExpanded' state of folders does not change when the
+ * 'refreshItems' signal is received, see https://bugs.kde.org/show_bug.cgi?id=299675.
+ */
+void KFileItemModelTest::testRefreshExpandedItem()
+{
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+ QSignalSpy itemsChangedSpy(m_model, &KFileItemModel::itemsChanged);
+ QVERIFY(itemsChangedSpy.isValid());
+
+ QSet<QByteArray> modelRoles = m_model->roles();
+ modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount";
+ m_model->setRoles(modelRoles);
+
+ m_testDir->createFiles({"a/1", "a/2", "3", "4"});
+
+ m_model->loadDirectory(m_testDir->url());
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), 3); // "a/", "3", "4"
+
+ m_model->setExpanded(0, true);
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), 5); // "a/", "a/1", "a/2", "3", "4"
+ QVERIFY(m_model->isExpanded(0));
+
+ const KFileItem item = m_model->fileItem(0);
+ m_model->slotRefreshItems({qMakePair(item, item)});
+ QVERIFY(!itemsChangedSpy.isEmpty());
+
+ QCOMPARE(m_model->count(), 5); // "a/", "a/1", "a/2", "3", "4"
+ QVERIFY(m_model->isExpanded(0));
+}
+
+/**
+ * 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 -> only the expanded folders remain.
+ m_model->setNameFilter("xyz");
+ QCOMPARE(itemsRemovedSpy.count(), 1);
+ QCOMPARE(m_model->count(), 3);
+ QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c");
+
+ // 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 -> no files should appear (and we should not get a crash).
+ m_model->setNameFilter(QString());
+ QCOMPARE(m_model->count(), 1);
+}
+
+/**
+ * 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 -> only the expanded folders remain.
+ m_model->setNameFilter("xyz");
+ QCOMPARE(itemsRemovedSpy.count(), 1);
+ 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(), 1);
+ QCOMPARE(itemsInModel(), QStringList() << "a");
+
+ // 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(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 -> only expanded folders remain.
+ m_model->setNameFilter("xyz");
+ QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "parent2" << "realChild2");
+ QCOMPARE(itemsRemovedSpy.count(), 1);
+ QList<QVariant> arguments = itemsRemovedSpy.takeFirst();
+ KItemRangeList itemRangeList = arguments.at(0).value<KItemRangeList>();
+ QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(2, 3) << KItemRange(7, 3));
+
+ // 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"));
+ }
+
+ // Add a filter, such that only "aaa.txt" remains in the model.
+ m_model->setNameFilter("aaa");
+ QCOMPARE(itemsInModel(), QStringList() << "aaa.txt");
+
+ // Add the "group" role.
+ modelRoles << "group";
+ m_model->setRoles(modelRoles);
+
+ // Modify the filter, such that "aa.txt" reappears, and verify that all items have the expected roles.
+ m_model->setNameFilter("aa");
+ QCOMPARE(itemsInModel(), QStringList() << "aa.txt" << "aaa.txt");
+
+ for (int index = 0; index < m_model->count(); ++index) {
+ // All items should have the "text", "owner", and "group" roles.
+ QVERIFY(m_model->data(index).contains("text"));
+ QVERIFY(m_model->data(index).contains("owner"));
+ QVERIFY(m_model->data(index).contains("group"));
+ }
+
+ // Remove the "owner" role.
+ modelRoles.remove("owner");
+ m_model->setRoles(modelRoles);
+
+ // Clear the filter, and verify that all items have the expected roles
+ m_model->setNameFilter(QString());
+ 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 "group" roles, but now "owner".
+ QVERIFY(m_model->data(index).contains("text"));
+ QVERIFY(!m_model->data(index).contains("owner"));
+ QVERIFY(m_model->data(index).contains("group"));