+ 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->setGroupRole("text");
+ 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->setGroupRole("text");
+
+ 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"));
+ }