File indexing completed on 2025-01-05 04:59:58

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Kevin Ottens <ervin@kde.org>
0003  SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 
0006 
0007 #include <testlib/qtest_zanshin.h>
0008 
0009 #include "utils/mockobject.h"
0010 
0011 #include <QSignalSpy>
0012 #include <QTemporaryFile>
0013 
0014 #include "testlib/fakejob.h"
0015 
0016 #include "domain/task.h"
0017 
0018 #include "presentation/editormodel.h"
0019 #include "presentation/errorhandler.h"
0020 
0021 using namespace mockitopp;
0022 
0023 class FakeErrorHandler : public Presentation::ErrorHandler
0024 {
0025 public:
0026     void doDisplayMessage(const QString &message) override
0027     {
0028         m_message = message;
0029     }
0030 
0031     QString m_message;
0032 };
0033 
0034 class EditorModelTest : public QObject
0035 {
0036     Q_OBJECT
0037 public:
0038     explicit EditorModelTest(QObject *parent = nullptr)
0039         : QObject(parent)
0040     {
0041         qRegisterMetaType<Domain::Task::Recurrence>();
0042         Presentation::EditorModel::setAutoSaveDelay(50);
0043     }
0044 
0045 private slots:
0046     void shouldHaveEmptyDefaultState()
0047     {
0048         // GIVEN
0049         Presentation::EditorModel model;
0050 
0051         // WHEN
0052         // Nothing
0053 
0054         // THEN
0055         QVERIFY(model.task().isNull());
0056         QVERIFY(model.text().isEmpty());
0057         QVERIFY(model.title().isEmpty());
0058         QVERIFY(!model.isDone());
0059         QVERIFY(model.startDate().isNull());
0060         QVERIFY(model.dueDate().isNull());
0061         QCOMPARE(model.recurrence(), Domain::Task::NoRecurrence);
0062         QVERIFY(model.attachmentModel() != nullptr);
0063         QVERIFY(!model.hasSaveFunction());
0064         auto am = model.attachmentModel();
0065         QCOMPARE(am->rowCount(), 0);
0066     }
0067 
0068     void shouldHaveTaskProperties()
0069     {
0070         // GIVEN
0071         Presentation::EditorModel model;
0072         QSignalSpy textSpy(&model, &Presentation::EditorModel::textChanged);
0073         QSignalSpy titleSpy(&model, &Presentation::EditorModel::titleChanged);
0074         QSignalSpy doneSpy(&model, &Presentation::EditorModel::doneChanged);
0075         QSignalSpy startSpy(&model, &Presentation::EditorModel::startDateChanged);
0076         QSignalSpy dueSpy(&model, &Presentation::EditorModel::dueDateChanged);
0077         QSignalSpy recurrenceSpy(&model, &Presentation::EditorModel::recurrenceChanged);
0078         QSignalSpy attachmentSpy(model.attachmentModel(), &QAbstractItemModel::modelReset);
0079 
0080         Domain::Task::Attachments attachments;
0081 
0082         Domain::Task::Attachment dataAttachment;
0083         dataAttachment.setData("foo");
0084         dataAttachment.setLabel("dataAttachment");
0085         dataAttachment.setMimeType("text/plain");
0086         dataAttachment.setIconName("text-plain");
0087         attachments.append(dataAttachment);
0088 
0089         Domain::Task::Attachment uriAttachment;
0090         uriAttachment.setUri(QUrl("https://www.kde.org"));
0091         uriAttachment.setLabel("uriAttachment");
0092         uriAttachment.setMimeType("text/html");
0093         uriAttachment.setIconName("text-html");
0094         attachments.append(uriAttachment);
0095 
0096         auto task = Domain::Task::Ptr::create();
0097         task->setText(QStringLiteral("description"));
0098         task->setTitle(QStringLiteral("title"));
0099         task->setDone(true);
0100         task->setStartDate(QDate::currentDate());
0101         task->setDueDate(QDate::currentDate().addDays(2));
0102         task->setRecurrence(Domain::Task::RecursDaily);
0103         task->setAttachments(attachments);
0104 
0105         // WHEN
0106         model.setTask(task);
0107         // To make sure we don't signal too much
0108         model.setText(task->text());
0109         model.setTitle(task->title());
0110         model.setDone(task->isDone());
0111         model.setStartDate(task->startDate());
0112         model.setDueDate(task->dueDate());
0113         model.setRecurrence(task->recurrence());
0114 
0115         // THEN
0116         QCOMPARE(textSpy.size(), 1);
0117         QCOMPARE(textSpy.takeFirst().at(0).toString(), task->text());
0118         QCOMPARE(model.property("text").toString(), task->text());
0119 
0120         QCOMPARE(titleSpy.size(), 1);
0121         QCOMPARE(titleSpy.takeFirst().at(0).toString(), task->title());
0122         QCOMPARE(model.property("title").toString(), task->title());
0123 
0124         QCOMPARE(doneSpy.size(), 1);
0125         QCOMPARE(doneSpy.takeFirst().at(0).toBool(), task->isDone());
0126         QCOMPARE(model.property("done").toBool(), task->isDone());
0127 
0128         QCOMPARE(startSpy.size(), 1);
0129         QCOMPARE(startSpy.takeFirst().at(0).toDate(), task->startDate());
0130         QCOMPARE(model.property("startDate").toDate(), task->startDate());
0131 
0132         QCOMPARE(dueSpy.size(), 1);
0133         QCOMPARE(dueSpy.takeFirst().at(0).toDate(), task->dueDate());
0134         QCOMPARE(model.property("dueDate").toDate(), task->dueDate());
0135 
0136         QCOMPARE(recurrenceSpy.size(), 1);
0137         QCOMPARE(recurrenceSpy.takeFirst().at(0).value<Domain::Task::Recurrence>(), task->recurrence());
0138         QCOMPARE(model.property("recurrence").value<Domain::Task::Recurrence>(), task->recurrence());
0139 
0140         QCOMPARE(attachmentSpy.size(), 1);
0141         auto am = model.attachmentModel();
0142         QCOMPARE(am->rowCount(), 2);
0143         QCOMPARE(am->data(am->index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("dataAttachment"));
0144         QCOMPARE(am->data(am->index(0, 0), Qt::DecorationRole).value<QIcon>(), QIcon::fromTheme("text-plain"));
0145         QCOMPARE(am->data(am->index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("uriAttachment"));
0146         QCOMPARE(am->data(am->index(1, 0), Qt::DecorationRole).value<QIcon>(), QIcon::fromTheme("text-html"));
0147     }
0148 
0149     void shouldReactToTaskPropertyChanges_data()
0150     {
0151         QTest::addColumn<Domain::Task::Ptr>("task");
0152         QTest::addColumn<QByteArray>("propertyName");
0153         QTest::addColumn<QVariant>("propertyValue");
0154         QTest::addColumn<QByteArray>("signal");
0155 
0156         QTest::newRow("task text") << Domain::Task::Ptr(Domain::Task::Ptr::create())
0157                                    << QByteArray("text")
0158                                    << QVariant("new text")
0159                                    << QByteArray(SIGNAL(textChanged(QString)));
0160 
0161         QTest::newRow("task title") << Domain::Task::Ptr(Domain::Task::Ptr::create())
0162                                     << QByteArray("title")
0163                                     << QVariant("new title")
0164                                     << QByteArray(SIGNAL(titleChanged(QString)));
0165 
0166         QTest::newRow("task done") << Domain::Task::Ptr(Domain::Task::Ptr::create())
0167                                    << QByteArray("done")
0168                                    << QVariant(true)
0169                                    << QByteArray(SIGNAL(doneChanged(bool)));
0170 
0171         QTest::newRow("task start") << Domain::Task::Ptr(Domain::Task::Ptr::create())
0172                                     << QByteArray("startDate")
0173                                     << QVariant(QDate::currentDate())
0174                                     << QByteArray(SIGNAL(startDateChanged(QDate)));
0175 
0176         QTest::newRow("task due") << Domain::Task::Ptr(Domain::Task::Ptr::create())
0177                                   << QByteArray("dueDate")
0178                                   << QVariant(QDate::currentDate().addDays(2))
0179                                   << QByteArray(SIGNAL(dueDateChanged(QDate)));
0180 
0181         QTest::newRow("task recurrence") << Domain::Task::Ptr(Domain::Task::Ptr::create())
0182                                   << QByteArray("recurrence")
0183                                   << QVariant::fromValue(Domain::Task::RecursDaily)
0184                                   << QByteArray(SIGNAL(recurrenceChanged(Domain::Task::Recurrence)));
0185     }
0186 
0187     void shouldReactToTaskPropertyChanges()
0188     {
0189         // GIVEN
0190         QFETCH(Domain::Task::Ptr, task);
0191         QFETCH(QByteArray, propertyName);
0192         QFETCH(QVariant, propertyValue);
0193         QFETCH(QByteArray, signal);
0194 
0195         Presentation::EditorModel model;
0196         model.setTask(task);
0197         QSignalSpy spy(&model, signal.constData());
0198 
0199         // WHEN
0200         task->setProperty(propertyName, propertyValue);
0201 
0202         // THEN
0203         QCOMPARE(spy.size(), 1);
0204         QCOMPARE(spy.takeFirst().at(0), propertyValue);
0205         QCOMPARE(model.property(propertyName), propertyValue);
0206     }
0207 
0208     void shouldNotReactToTaskPropertyChangesWhenEditing_data()
0209     {
0210         shouldReactToTaskPropertyChanges_data();
0211     }
0212 
0213     void shouldNotReactToTaskPropertyChangesWhenEditing()
0214     {
0215         // GIVEN
0216         QFETCH(Domain::Task::Ptr, task);
0217         QFETCH(QByteArray, propertyName);
0218         QFETCH(QVariant, propertyValue);
0219         QFETCH(QByteArray, signal);
0220 
0221         Presentation::EditorModel model;
0222         model.setTask(task);
0223         QSignalSpy spy(&model, signal.constData());
0224 
0225         // WHEN
0226         const auto oldPropertyValue = task->property(propertyName);
0227         model.setEditingInProgress(true);
0228         task->setProperty(propertyName, propertyValue);
0229 
0230         // THEN
0231         QVERIFY(spy.isEmpty());
0232         QCOMPARE(model.property(propertyName), oldPropertyValue);
0233     }
0234 
0235     void shouldApplyChangesBackToTaskAfterADelay_data()
0236     {
0237         shouldReactToTaskPropertyChanges_data();
0238     }
0239 
0240     void shouldApplyChangesBackToTaskAfterADelay()
0241     {
0242         // GIVEN
0243         QFETCH(Domain::Task::Ptr, task);
0244         QFETCH(QByteArray, propertyName);
0245         QFETCH(QVariant, propertyValue);
0246         QFETCH(QByteArray, signal);
0247 
0248         auto savedTask = Domain::Task::Ptr();
0249         auto save = [this, &savedTask] (const Domain::Task::Ptr &task) {
0250             savedTask = task;
0251             return new FakeJob(this);
0252         };
0253 
0254         Presentation::EditorModel model;
0255         model.setSaveFunction(save);
0256         model.setTask(task);
0257         QSignalSpy spy(&model, signal.constData());
0258 
0259         // WHEN
0260         model.setProperty(propertyName, propertyValue);
0261 
0262         // THEN
0263         QCOMPARE(spy.size(), 1);
0264         QCOMPARE(spy.takeFirst().at(0), propertyValue);
0265         QCOMPARE(model.property(propertyName), propertyValue);
0266         QVERIFY(task->property(propertyName) != propertyValue);
0267         QVERIFY(!savedTask);
0268 
0269         // WHEN (apply after delay)
0270         QTest::qWait(model.autoSaveDelay() + 50);
0271 
0272         // THEN
0273         QCOMPARE(savedTask, task);
0274         QCOMPARE(task->property(propertyName), propertyValue);
0275     }
0276 
0277     void shouldApplyChangesImmediatelyIfANewTaskIsSet_data()
0278     {
0279         shouldReactToTaskPropertyChanges_data();
0280     }
0281 
0282     void shouldApplyChangesImmediatelyIfANewTaskIsSet()
0283     {
0284         // GIVEN
0285         QFETCH(Domain::Task::Ptr, task);
0286         QFETCH(QByteArray, propertyName);
0287         QFETCH(QVariant, propertyValue);
0288         QFETCH(QByteArray, signal);
0289 
0290         auto savedTask = Domain::Task::Ptr();
0291         auto save = [this, &savedTask] (const Domain::Task::Ptr &task) {
0292             savedTask = task;
0293             return new FakeJob(this);
0294         };
0295 
0296         Presentation::EditorModel model;
0297         model.setSaveFunction(save);
0298         QVERIFY(model.hasSaveFunction());
0299         model.setTask(task);
0300         QSignalSpy spy(&model, signal.constData());
0301 
0302         // WHEN
0303         model.setProperty(propertyName, propertyValue);
0304 
0305         // THEN
0306         QCOMPARE(spy.size(), 1);
0307         QCOMPARE(spy.takeFirst().at(0), propertyValue);
0308         QCOMPARE(model.property(propertyName), propertyValue);
0309         QVERIFY(task->property(propertyName) != propertyValue);
0310         QVERIFY(!savedTask);
0311 
0312         // WHEN (apply immediately)
0313         model.setTask(Domain::Task::Ptr::create());
0314 
0315         // THEN
0316         QCOMPARE(savedTask, task);
0317         QCOMPARE(task->property(propertyName), propertyValue);
0318         savedTask.clear();
0319 
0320         // WHEN (nothing else happens after a delay)
0321         QTest::qWait(model.autoSaveDelay() + 50);
0322 
0323         // THEN
0324         QVERIFY(!savedTask);
0325         QCOMPARE(task->property(propertyName), propertyValue);
0326     }
0327 
0328     void shouldApplyChangesImmediatelyIfDeleted_data()
0329     {
0330         shouldReactToTaskPropertyChanges_data();
0331     }
0332 
0333     void shouldApplyChangesImmediatelyIfDeleted()
0334     {
0335         // GIVEN
0336         QFETCH(Domain::Task::Ptr, task);
0337         QFETCH(QByteArray, propertyName);
0338         QFETCH(QVariant, propertyValue);
0339         QFETCH(QByteArray, signal);
0340 
0341         auto savedTask = Domain::Task::Ptr();
0342         auto save = [this, &savedTask] (const Domain::Task::Ptr &task) {
0343             savedTask = task;
0344             return new FakeJob(this);
0345         };
0346 
0347         auto model = new Presentation::EditorModel;
0348         model->setSaveFunction(save);
0349         QVERIFY(model->hasSaveFunction());
0350         model->setTask(task);
0351         QSignalSpy spy(model, signal.constData());
0352 
0353         // WHEN
0354         model->setProperty(propertyName, propertyValue);
0355 
0356         // THEN
0357         QCOMPARE(spy.size(), 1);
0358         QCOMPARE(spy.takeFirst().at(0), propertyValue);
0359         QCOMPARE(model->property(propertyName), propertyValue);
0360         QVERIFY(task->property(propertyName) != propertyValue);
0361         QVERIFY(!savedTask);
0362 
0363         // WHEN (apply immediately)
0364         delete model;
0365 
0366         // THEN
0367         QCOMPARE(savedTask, task);
0368         QCOMPARE(task->property(propertyName), propertyValue);
0369     }
0370 
0371     void shouldGetAnErrorMessageWhenSaveFailed()
0372     {
0373         // GIVEN
0374         auto task = Domain::Task::Ptr::create();
0375         task->setTitle(QStringLiteral("Task 1"));
0376 
0377         auto savedTask = Domain::Task::Ptr();
0378         auto save = [this, &savedTask] (const Domain::Task::Ptr &task) {
0379             savedTask = task;
0380             auto job = new FakeJob(this);
0381             job->setExpectedError(KJob::KilledJobError, QStringLiteral("Foo"));
0382             return job;
0383         };
0384 
0385         auto model = new Presentation::EditorModel;
0386         model->setSaveFunction(save);
0387         QVERIFY(model->hasSaveFunction());
0388         FakeErrorHandler errorHandler;
0389         model->setErrorHandler(&errorHandler);
0390         model->setTask(task);
0391 
0392         // WHEN
0393         model->setProperty("title", "Foo");
0394         delete model;
0395 
0396         // THEN
0397         QTest::qWait(150);
0398         QCOMPARE(errorHandler.m_message, QStringLiteral("Cannot modify task Task 1: Foo"));
0399     }
0400 
0401     void shouldDisconnectFromPreviousTask_data()
0402     {
0403         shouldReactToTaskPropertyChanges_data();
0404     }
0405 
0406     void shouldDisconnectFromPreviousTask()
0407     {
0408         // GIVEN
0409         QFETCH(Domain::Task::Ptr, task);
0410         QFETCH(QByteArray, propertyName);
0411         QFETCH(QVariant, propertyValue);
0412         QFETCH(QByteArray, signal);
0413 
0414         Presentation::EditorModel model;
0415         model.setTask(task);
0416         QSignalSpy spy(&model, signal.constData());
0417 
0418         Domain::Task::Ptr newTask = Domain::Task::Ptr::create();
0419 
0420         // WHEN
0421         model.setTask(newTask);
0422         // modifying the *old* task should have no effect.
0423         task->setProperty(propertyName, propertyValue);
0424 
0425         // THEN
0426         QCOMPARE(spy.size(), 1); // emitted by setTask
0427         QVERIFY(model.property(propertyName) != task->property(propertyName));
0428     }
0429 
0430     void shouldAddAttachments()
0431     {
0432         // GIVEN
0433         QTemporaryFile temporaryFile(QDir::tempPath() + "/taskeditormodeltest_XXXXXX.txt");
0434         temporaryFile.open();
0435         temporaryFile.write("foo bar");
0436         temporaryFile.close();
0437         auto fileName = temporaryFile.fileName().mid(QDir::tempPath().size() + 1);
0438 
0439         auto task = Domain::Task::Ptr::create();
0440 
0441         auto savedTask = Domain::Task::Ptr();
0442         auto save = [this, &savedTask] (const Domain::Task::Ptr &task) {
0443             savedTask = task;
0444             return new FakeJob(this);
0445         };
0446 
0447         Presentation::EditorModel model;
0448         model.setSaveFunction(save);
0449         model.setTask(task);
0450 
0451         QSignalSpy spy(model.attachmentModel(), &QAbstractItemModel::modelReset);
0452 
0453         // WHEN
0454         model.addAttachment(temporaryFile.fileName());
0455 
0456         // THEN
0457         QCOMPARE(spy.size(), 1);
0458         QCOMPARE(model.attachmentModel()->rowCount(), 1);
0459         QVERIFY(!savedTask);
0460 
0461         // WHEN (nothing else happens after a delay)
0462         QTest::qWait(model.autoSaveDelay() + 50);
0463 
0464         // THEN
0465         QCOMPARE(savedTask, task);
0466         QCOMPARE(task->attachments().size(), 1);
0467         QCOMPARE(task->attachments().first().label(), fileName);
0468         QCOMPARE(task->attachments().first().mimeType(), QStringLiteral("text/plain"));
0469         QCOMPARE(task->attachments().first().iconName(), QStringLiteral("text-plain"));
0470         QCOMPARE(task->attachments().first().data(), QByteArrayLiteral("foo bar"));
0471     }
0472 
0473     void shouldRemoveAttachments()
0474     {
0475         // GIVEN
0476         auto task = Domain::Task::Ptr::create();
0477         task->setAttachments(Domain::Task::Attachments() << Domain::Task::Attachment("foo")
0478                                                          << Domain::Task::Attachment("bar"));
0479 
0480         auto savedTask = Domain::Task::Ptr();
0481         auto save = [this, &savedTask] (const Domain::Task::Ptr &task) {
0482             savedTask = task;
0483             return new FakeJob(this);
0484         };
0485 
0486         Presentation::EditorModel model;
0487         model.setSaveFunction(save);
0488         model.setTask(task);
0489 
0490         QSignalSpy spy(model.attachmentModel(), &QAbstractItemModel::modelReset);
0491 
0492         // WHEN
0493         model.removeAttachment(model.attachmentModel()->index(0, 0));
0494 
0495         // THEN
0496         QCOMPARE(spy.size(), 1);
0497         QCOMPARE(model.attachmentModel()->rowCount(), 1);
0498         QVERIFY(!savedTask);
0499 
0500         // WHEN (nothing else happens after a delay)
0501         QTest::qWait(model.autoSaveDelay() + 50);
0502 
0503         // THEN
0504         QCOMPARE(savedTask, task);
0505         QCOMPARE(task->attachments().size(), 1);
0506         QCOMPARE(task->attachments().first().data(), QByteArrayLiteral("bar"));
0507     }
0508 };
0509 
0510 ZANSHIN_TEST_MAIN(EditorModelTest)
0511 
0512 #include "editormodeltest.moc"