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"