File indexing completed on 2024-11-10 04:40:22

0001 /*
0002  * SPDX-FileCopyrightText: 2020 Daniel Vrátil <dvratil@kde.org>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  *
0006  */
0007 
0008 #include "qtest_akonadi.h"
0009 #include "shared/aktest.h"
0010 
0011 #include "monitor.h"
0012 #include "tag.h"
0013 #include "tagcreatejob.h"
0014 #include "tagdeletejob.h"
0015 #include "tageditwidget.h"
0016 #include "tagmodel.h"
0017 
0018 #include <QDialog>
0019 #include <QDialogButtonBox>
0020 #include <QLabel>
0021 #include <QLineEdit>
0022 #include <QListView>
0023 #include <QPushButton>
0024 #include <QSignalSpy>
0025 #include <QTest>
0026 
0027 #include <memory>
0028 using namespace std::chrono_literals;
0029 using namespace Akonadi;
0030 
0031 /***
0032  * This test also covers TagManagementDialog and TagSelectionDialog, which
0033  * both wrap TagEditWidget and provide their own own Monitor and TagModel,
0034  * just one allows selection and the other does not
0035  */
0036 class TagEditWidgetTest : public QObject
0037 {
0038     Q_OBJECT
0039 
0040     struct TestSetup {
0041         TestSetup()
0042             : monitor(std::make_unique<Monitor>())
0043         {
0044             monitor->setTypeMonitored(Monitor::Tags);
0045 
0046             model = std::make_unique<TagModel>(monitor.get());
0047             QSignalSpy modelSpy(model.get(), &TagModel::populated);
0048             QVERIFY(modelSpy.wait());
0049             QCOMPARE(model->rowCount(), 1); // there's one existing tag
0050 
0051             widget = std::make_unique<TagEditWidget>();
0052             widget->setModel(model.get());
0053             widget->show();
0054             QVERIFY(QTest::qWaitForWindowActive(widget.get()));
0055 
0056             newTagEdit = widget->findChild<QLineEdit *>(QStringLiteral("newTagEdit"));
0057             QVERIFY(newTagEdit);
0058             newTagButton = widget->findChild<QPushButton *>(QStringLiteral("newTagButton"));
0059             QVERIFY(newTagButton);
0060             QVERIFY(!newTagButton->isEnabled());
0061             tagsView = widget->findChild<QListView *>(QStringLiteral("tagsView"));
0062             QVERIFY(tagsView);
0063             tagDeleteButton = widget->findChild<QPushButton *>(QStringLiteral("tagDeleteButton"));
0064             QVERIFY(tagDeleteButton);
0065             QVERIFY(!tagDeleteButton->isVisible());
0066 
0067             valid = true;
0068         }
0069 
0070         ~TestSetup()
0071         {
0072             if (!createdTags.empty()) {
0073                 auto deleteJob = new TagDeleteJob(createdTags);
0074                 AKVERIFYEXEC(deleteJob);
0075             }
0076         }
0077 
0078         bool createTags(int count)
0079         {
0080             const auto doCreateTags = [this, count]() {
0081                 QSignalSpy monitorSpy(monitor.get(), &Monitor::tagAdded);
0082                 for (int i = 0; i < count; ++i) {
0083                     auto job = new TagCreateJob(Tag(QStringLiteral("TestTag-%1").arg(i)));
0084                     AKVERIFYEXEC(job);
0085                     createdTags.push_back(job->tag());
0086                 }
0087                 QTRY_COMPARE(monitorSpy.count(), count);
0088             };
0089             doCreateTags();
0090             return createdTags.size() == count;
0091         }
0092 
0093         bool checkSelectionIsEmpty() const
0094         {
0095             auto *const tagViewModel = tagsView->model();
0096             for (int i = 0; i < tagViewModel->rowCount(); ++i) {
0097                 if (tagViewModel->data(tagViewModel->index(i, 0), Qt::CheckStateRole).value<Qt::CheckState>() != Qt::Unchecked) {
0098                     return false;
0099                 }
0100             }
0101             return true;
0102         }
0103 
0104         QModelIndex indexForTag(const Tag &tag) const
0105         {
0106             for (int i = 0; i < tagsView->model()->rowCount(); ++i) {
0107                 const auto index = tagsView->model()->index(i, 0);
0108                 if (tagsView->model()->data(index, TagModel::TagRole).value<Tag>() == tag) {
0109                     return index;
0110                 }
0111             }
0112             return {};
0113         }
0114 
0115         bool deleteTag(const Tag &tag, bool confirmDeletion)
0116         {
0117             const auto index = indexForTag(tag);
0118             AKVERIFY(index.isValid());
0119             const auto itemRect = tagsView->visualRect(index);
0120             // Hover over the item and confirm the button is there
0121             QTest::mouseMove(tagsView->viewport(), itemRect.center());
0122             AKVERIFY(QTest::qWaitFor(std::bind(&QWidget::isVisible, tagDeleteButton)));
0123             AKVERIFY(tagDeleteButton->geometry().intersects(itemRect));
0124 
0125             // Clicking the button blocks (QDialog::exec), so we need to confirm the
0126             // dialog from event loop
0127             bool confirmed = false;
0128             QTimer::singleShot(100ms, [this, confirmDeletion, &confirmed]() {
0129                 confirmed = confirmDialog(confirmDeletion);
0130                 QVERIFY(confirmed);
0131             });
0132             QTest::mouseClick(tagDeleteButton, Qt::LeftButton);
0133 
0134             // Check that the confirmation was successful
0135             AKVERIFY(confirmed);
0136 
0137             return true;
0138         }
0139 
0140         bool confirmDialog(bool confirmDeletion)
0141         {
0142             const auto windows = QApplication::topLevelWidgets();
0143             for (const auto *window : windows) {
0144                 // We are using KMessageBox, which is not a QMessageBox but rather a custom QDialog
0145                 if (window->objectName() == QLatin1StringView("questionYesNo")) {
0146                     const auto *const msgbox = qobject_cast<const QDialog *>(window);
0147                     AKVERIFY(msgbox);
0148 
0149                     const auto *const buttonBox = msgbox->findChild<const QDialogButtonBox *>();
0150                     AKVERIFY(buttonBox);
0151                     auto *const button = buttonBox->button(confirmDeletion ? QDialogButtonBox::Yes : QDialogButtonBox::No);
0152                     AKVERIFY(button);
0153                     QTest::mouseClick(button, Qt::LeftButton);
0154                     return true;
0155                 }
0156             }
0157 
0158             return false;
0159         }
0160 
0161         std::unique_ptr<Monitor> monitor;
0162         std::unique_ptr<TagModel> model;
0163         std::unique_ptr<TagEditWidget> widget;
0164 
0165         QLineEdit *newTagEdit = nullptr;
0166         QPushButton *newTagButton = nullptr;
0167         QListView *tagsView = nullptr;
0168         QPushButton *tagDeleteButton = nullptr;
0169 
0170         Tag::List createdTags;
0171 
0172         bool valid = false;
0173     };
0174 
0175 private Q_SLOTS:
0176     void initTestCase()
0177     {
0178         AkonadiTest::checkTestIsIsolated();
0179     }
0180 
0181     void testTagCreationWithEnter()
0182     {
0183         const auto tagName = QStringLiteral("TagEditWidgetTestTag");
0184 
0185         TestSetup test;
0186         QVERIFY(test.valid);
0187 
0188         QSignalSpy monitorSpy(test.monitor.get(), &Monitor::tagAdded);
0189 
0190         QTest::keyClicks(test.newTagEdit, tagName);
0191         QVERIFY(test.newTagButton->isEnabled());
0192         QTest::keyClick(test.newTagEdit, Qt::Key_Return);
0193         QVERIFY(!test.newTagButton->isEnabled());
0194         QVERIFY(!test.newTagEdit->isEnabled());
0195 
0196         QTRY_COMPARE(monitorSpy.size(), 1);
0197         test.createdTags.push_back(monitorSpy.at(0).at(0).value<Akonadi::Tag>());
0198         QCOMPARE(test.model->rowCount(), 2);
0199         QCOMPARE(test.model->data(test.model->index(1, 0), Qt::DisplayRole).toString(), tagName);
0200     }
0201 
0202     void testTagCreationWithButton()
0203     {
0204         const auto tagName = QStringLiteral("TagEditWidgetTestTag");
0205 
0206         TestSetup test;
0207         QVERIFY(test.valid);
0208 
0209         QSignalSpy monitorSpy(test.monitor.get(), &Monitor::tagAdded);
0210 
0211         QTest::keyClicks(test.newTagEdit, tagName);
0212         QVERIFY(test.newTagButton->isEnabled());
0213         QTest::mouseClick(test.newTagButton, Qt::LeftButton);
0214         QVERIFY(!test.newTagButton->isEnabled());
0215         QVERIFY(!test.newTagEdit->isEnabled());
0216 
0217         QTRY_COMPARE(monitorSpy.size(), 1);
0218         test.createdTags.push_back(monitorSpy.at(0).at(0).value<Akonadi::Tag>());
0219         QCOMPARE(test.model->rowCount(), 2);
0220         QCOMPARE(test.model->data(test.model->index(1, 0), Qt::DisplayRole).toString(), tagName);
0221     }
0222 
0223     void testDuplicatedTagCannotBeCreated()
0224     {
0225         TestSetup test;
0226         QVERIFY(test.valid);
0227 
0228         // Create a tag
0229         QVERIFY(test.createTags(1));
0230 
0231         // Wait for the tag to appear in the model
0232         QTRY_COMPARE(test.model->rowCount(), 2);
0233 
0234         // Type the entire string char-by-char - once the name is full the button
0235         // should be disabled because we don't allow creating duplicated tags
0236         const auto tagName = test.createdTags.front().name();
0237         for (int i = 0; i < tagName.size(); ++i) {
0238             QTest::keyClicks(test.newTagEdit, tagName[i]);
0239             QCOMPARE(test.newTagButton->isEnabled(), i != tagName.size() - 1);
0240         }
0241     }
0242 
0243     void testSettingSelectionFromCode()
0244     {
0245         TestSetup test;
0246         QVERIFY(test.valid);
0247         QVERIFY(test.createTags(10));
0248 
0249         test.widget->setSelectionEnabled(true);
0250 
0251         // Nothing should be checked
0252         QVERIFY(test.checkSelectionIsEmpty());
0253 
0254         // Set selection
0255         auto *model = test.tagsView->model();
0256         Tag::List selectTags;
0257         for (int i = 0; i < model->rowCount(); i += 2) {
0258             selectTags.push_back(model->data(model->index(i, 0), TagModel::TagRole).value<Tag>());
0259         }
0260         QVERIFY(!selectTags.empty());
0261         test.widget->setSelection(selectTags);
0262         QCOMPARE(test.widget->selection(), selectTags);
0263 
0264         // Confirm that the items are visually selected
0265         for (int i = 0; i < model->rowCount(); ++i) {
0266             const auto tag = model->data(model->index(i, 0), TagModel::TagRole).value<Tag>();
0267             const auto expectedState = selectTags.contains(tag) ? Qt::Checked : Qt::Unchecked;
0268             QCOMPARE(model->data(model->index(i, 0), Qt::CheckStateRole).value<Qt::CheckState>(), expectedState);
0269         }
0270     }
0271 
0272     void testSelectingTagsByMouse()
0273     {
0274         TestSetup test;
0275         QVERIFY(test.valid);
0276         QVERIFY(test.createTags(10));
0277 
0278         test.widget->setSelectionEnabled(true);
0279 
0280         // Nothing should be checked
0281         QVERIFY(test.checkSelectionIsEmpty());
0282 
0283         // Check several tags
0284         Tag::List selectedTags;
0285         auto *model = test.tagsView->model();
0286         for (int i = 0; i < model->rowCount(); i += 2) {
0287             const auto index = model->index(i, 0);
0288             selectedTags.push_back(model->data(index, TagModel::TagRole).value<Tag>());
0289             // Select the row
0290             QTest::mouseClick(test.tagsView->viewport(), Qt::LeftButton, {}, test.tagsView->visualRect(index).topLeft() + QPoint(5, 5));
0291             // Use spacebar to toggle selection, we can't possibly hit the checkbox with mouse in a
0292             // reliable manner.
0293             QTest::keyClick(test.tagsView, Qt::Key_Space);
0294         }
0295 
0296         // Confirm that the selection occurred
0297         for (int i = 0; i < model->rowCount(); ++i) {
0298             const auto expectedState = i % 2 == 0 ? Qt::Checked : Qt::Unchecked;
0299             QCOMPARE(model->data(model->index(i, 0), Qt::CheckStateRole).value<Qt::CheckState>(), expectedState);
0300         }
0301 
0302         // Compare the selectede tags
0303         auto currentSelection = test.widget->selection();
0304         const auto sortTag = [](const Tag &l, const Tag &r) {
0305             return l.id() < r.id();
0306         };
0307         std::sort(currentSelection.begin(), currentSelection.end(), sortTag);
0308         std::sort(selectedTags.begin(), selectedTags.end(), sortTag);
0309         QCOMPARE(currentSelection, selectedTags);
0310     }
0311 
0312     void testDeletingTags()
0313     {
0314         TestSetup test;
0315         QVERIFY(test.valid);
0316         QVERIFY(test.createTags(4));
0317 
0318         while (!test.createdTags.empty()) {
0319             QSignalSpy monitorSpy(test.monitor.get(), &Monitor::tagRemoved);
0320             // Get the last tag in the list and delete it
0321             const auto tag = test.createdTags.last();
0322             QVERIFY(test.deleteTag(tag, true));
0323 
0324             // Wait for confirmation
0325             QTRY_COMPARE(monitorSpy.size(), 1);
0326             QCOMPARE(monitorSpy.at(0).at(0).value<Tag>(), tag);
0327 
0328             test.createdTags.pop_back(); // remove the tag from the list
0329         }
0330 
0331         // Verify that we've deleted everything
0332         QVERIFY(test.createdTags.empty());
0333         QCOMPARE(test.model->rowCount(), 1); // only the default tag remains
0334     }
0335 
0336     void testRejectingDeleteDialogDoesntDeleteTheTAg()
0337     {
0338         TestSetup test;
0339         QVERIFY(test.valid);
0340 
0341         QSignalSpy monitorSpy(test.monitor.get(), &Monitor::tagRemoved);
0342         const auto index = test.model->index(0, 0);
0343         QVERIFY(index.isValid());
0344 
0345         const auto tag = test.model->data(index, TagModel::TagRole).value<Tag>();
0346         QVERIFY(test.deleteTag(tag, false));
0347 
0348         QTest::qWait(500); // wait some amount of time to see that nothing has changed...
0349         QVERIFY(monitorSpy.empty());
0350         QCOMPARE(test.model->rowCount(), 1);
0351     }
0352 };
0353 
0354 QTEST_AKONADIMAIN(TagEditWidgetTest)
0355 
0356 #include "tageditwidgettest.moc"