+/**
+ * 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()