diff --git a/src/presentation/artifacteditormodel.cpp b/src/presentation/artifacteditormodel.cpp index b8d683b1..ceea05a4 100644 --- a/src/presentation/artifacteditormodel.cpp +++ b/src/presentation/artifacteditormodel.cpp @@ -1,325 +1,402 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "artifacteditormodel.h" +#include +#include #include #include #include "domain/task.h" #include "errorhandler.h" +namespace Presentation { +class AttachmentModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit AttachmentModel(QObject *parent = nullptr) + : QAbstractListModel(parent) + { + } + + void setTask(const Domain::Task::Ptr &task) + { + if (m_task == task) + return; + + beginResetModel(); + if (m_task) { + disconnect(m_task.data(), &Domain::Task::attachmentsChanged, + this, &AttachmentModel::triggerReset); + } + m_task = task; + if (m_task) { + connect(m_task.data(), &Domain::Task::attachmentsChanged, + this, &AttachmentModel::triggerReset); + } + endResetModel(); + } + + int rowCount(const QModelIndex &parent) const override + { + if (parent.isValid()) + return 0; + + return m_task->attachments().size(); + } + + QVariant data(const QModelIndex &index, int role) const override + { + if (!index.isValid()) + return QVariant(); + + auto attachment = m_task->attachments().at(index.row()); + + switch (role) { + case Qt::DisplayRole: + return attachment.label(); + case Qt::DecorationRole: + return QVariant::fromValue(QIcon::fromTheme(attachment.iconName())); + default: + return QVariant(); + } + } + +private slots: + void triggerReset() + { + beginResetModel(); + endResetModel(); + } + +private: + Domain::Task::Ptr m_task; +}; +} + using namespace Presentation; ArtifactEditorModel::ArtifactEditorModel(QObject *parent) : QObject(parent), m_done(false), + m_attachmentModel(new AttachmentModel(this)), m_saveTimer(new QTimer(this)), m_saveNeeded(false), m_editingInProgress(false) { m_saveTimer->setSingleShot(true); m_saveTimer->setInterval(autoSaveDelay()); connect(m_saveTimer, &QTimer::timeout, this, &ArtifactEditorModel::save); } ArtifactEditorModel::~ArtifactEditorModel() { save(); } Domain::Artifact::Ptr ArtifactEditorModel::artifact() const { return m_artifact; } void ArtifactEditorModel::setArtifact(const Domain::Artifact::Ptr &artifact) { if (m_artifact == artifact) return; save(); m_text = QString(); m_title = QString(); m_done = false; m_start = QDateTime(); m_due = QDateTime(); + m_attachmentModel->setTask(Domain::Task::Ptr()); m_delegateText = QString(); if (m_artifact) disconnect(m_artifact.data(), Q_NULLPTR, this, Q_NULLPTR); m_artifact = artifact; if (m_artifact) { m_text = m_artifact->text(); m_title = m_artifact->title(); connect(m_artifact.data(), &Domain::Artifact::textChanged, this, &ArtifactEditorModel::onTextChanged); connect(m_artifact.data(), &Domain::Artifact::titleChanged, this, &ArtifactEditorModel::onTitleChanged); } if (auto task = artifact.objectCast()) { m_done = task->isDone(); m_start = task->startDate(); m_due = task->dueDate(); + m_attachmentModel->setTask(task); m_delegateText = task->delegate().display(); connect(task.data(), &Domain::Task::doneChanged, this, &ArtifactEditorModel::onDoneChanged); connect(task.data(), &Domain::Task::startDateChanged, this, &ArtifactEditorModel::onStartDateChanged); connect(task.data(), &Domain::Task::dueDateChanged, this, &ArtifactEditorModel::onDueDateChanged); connect(task.data(), &Domain::Task::delegateChanged, this, &ArtifactEditorModel::onDelegateChanged); } emit textChanged(m_text); emit titleChanged(m_title); emit doneChanged(m_done); emit startDateChanged(m_start); emit dueDateChanged(m_due); emit delegateTextChanged(m_delegateText); emit hasTaskPropertiesChanged(hasTaskProperties()); emit artifactChanged(m_artifact); } bool ArtifactEditorModel::hasSaveFunction() const { return bool(m_saveFunction); } void ArtifactEditorModel::setSaveFunction(const SaveFunction &function) { m_saveFunction = function; } bool ArtifactEditorModel::hasDelegateFunction() const { return bool(m_delegateFunction); } void ArtifactEditorModel::setDelegateFunction(const DelegateFunction &function) { m_delegateFunction = function; } bool ArtifactEditorModel::hasTaskProperties() const { return m_artifact.objectCast(); } QString ArtifactEditorModel::text() const { return m_text; } QString ArtifactEditorModel::title() const { return m_title; } bool ArtifactEditorModel::isDone() const { return m_done; } QDateTime ArtifactEditorModel::startDate() const { return m_start; } QDateTime ArtifactEditorModel::dueDate() const { return m_due; } +QAbstractItemModel *ArtifactEditorModel::attachmentModel() const +{ + return m_attachmentModel; +} + QString ArtifactEditorModel::delegateText() const { return m_delegateText; } int ArtifactEditorModel::autoSaveDelay() { return 500; } bool ArtifactEditorModel::editingInProgress() const { return m_editingInProgress; } void ArtifactEditorModel::setText(const QString &text) { if (m_text == text) return; applyNewText(text); setSaveNeeded(true); } void ArtifactEditorModel::setTitle(const QString &title) { if (m_title == title) return; applyNewTitle(title); setSaveNeeded(true); } void ArtifactEditorModel::setDone(bool done) { if (m_done == done) return; applyNewDone(done); setSaveNeeded(true); } void ArtifactEditorModel::setStartDate(const QDateTime &start) { if (m_start == start) return; applyNewStartDate(start); setSaveNeeded(true); } void ArtifactEditorModel::setDueDate(const QDateTime &due) { if (m_due == due) return; applyNewDueDate(due); setSaveNeeded(true); } void ArtifactEditorModel::delegate(const QString &name, const QString &email) { auto task = m_artifact.objectCast(); Q_ASSERT(task); auto delegate = Domain::Task::Delegate(name, email); m_delegateFunction(task, delegate); } void ArtifactEditorModel::setEditingInProgress(bool editing) { m_editingInProgress = editing; } void ArtifactEditorModel::onTextChanged(const QString &text) { if (!m_editingInProgress) applyNewText(text); } void ArtifactEditorModel::onTitleChanged(const QString &title) { if (!m_editingInProgress) applyNewTitle(title); } void ArtifactEditorModel::onDoneChanged(bool done) { if (!m_editingInProgress) applyNewDone(done); } void ArtifactEditorModel::onStartDateChanged(const QDateTime &start) { if (!m_editingInProgress) applyNewStartDate(start); } void ArtifactEditorModel::onDueDateChanged(const QDateTime &due) { if (!m_editingInProgress) applyNewDueDate(due); } void ArtifactEditorModel::onDelegateChanged(const Domain::Task::Delegate &delegate) { m_delegateText = delegate.display(); emit delegateTextChanged(m_delegateText); } void ArtifactEditorModel::save() { if (!isSaveNeeded()) return; Q_ASSERT(m_artifact); const auto currentTitle = m_artifact->title(); m_artifact->setTitle(m_title); m_artifact->setText(m_text); if (auto task = m_artifact.objectCast()) { task->setDone(m_done); task->setStartDate(m_start); task->setDueDate(m_due); } const auto job = m_saveFunction(m_artifact); installHandler(job, i18n("Cannot modify task %1", currentTitle)); setSaveNeeded(false); } void ArtifactEditorModel::setSaveNeeded(bool needed) { if (needed) m_saveTimer->start(); else m_saveTimer->stop(); m_saveNeeded = needed; } bool ArtifactEditorModel::isSaveNeeded() const { return m_saveNeeded; } void ArtifactEditorModel::applyNewText(const QString &text) { m_text = text; emit textChanged(m_text); } void ArtifactEditorModel::applyNewTitle(const QString &title) { m_title = title; emit titleChanged(m_title); } void ArtifactEditorModel::applyNewDone(bool done) { m_done = done; emit doneChanged(m_done); } void ArtifactEditorModel::applyNewStartDate(const QDateTime &start) { m_start = start; emit startDateChanged(m_start); } void ArtifactEditorModel::applyNewDueDate(const QDateTime &due) { m_due = due; emit dueDateChanged(m_due); } + +#include "artifacteditormodel.moc" diff --git a/src/presentation/artifacteditormodel.h b/src/presentation/artifacteditormodel.h index 9b117cbb..7fd21b38 100644 --- a/src/presentation/artifacteditormodel.h +++ b/src/presentation/artifacteditormodel.h @@ -1,140 +1,146 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef PRESENTATION_ARTIFACTEDITORMODEL_H #define PRESENTATION_ARTIFACTEDITORMODEL_H #include #include #include #include "domain/task.h" #include "presentation/errorhandlingmodelbase.h" +class QAbstractItemModel; class QTimer; namespace Presentation { +class AttachmentModel; + class ArtifactEditorModel : public QObject, public ErrorHandlingModelBase { Q_OBJECT Q_PROPERTY(Domain::Artifact::Ptr artifact READ artifact WRITE setArtifact NOTIFY artifactChanged) Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) Q_PROPERTY(bool done READ isDone WRITE setDone NOTIFY doneChanged) Q_PROPERTY(QDateTime startDate READ startDate WRITE setStartDate NOTIFY startDateChanged) Q_PROPERTY(QDateTime dueDate READ dueDate WRITE setDueDate NOTIFY dueDateChanged) + Q_PROPERTY(QAbstractItemModel* attachmentModel READ attachmentModel CONSTANT) Q_PROPERTY(QString delegateText READ delegateText NOTIFY delegateTextChanged) Q_PROPERTY(bool hasTaskProperties READ hasTaskProperties NOTIFY hasTaskPropertiesChanged) Q_PROPERTY(bool editingInProgress READ editingInProgress WRITE setEditingInProgress) public: typedef std::function SaveFunction; typedef std::function DelegateFunction; explicit ArtifactEditorModel(QObject *parent = Q_NULLPTR); ~ArtifactEditorModel(); Domain::Artifact::Ptr artifact() const; void setArtifact(const Domain::Artifact::Ptr &artifact); bool hasSaveFunction() const; void setSaveFunction(const SaveFunction &function); bool hasDelegateFunction() const; void setDelegateFunction(const DelegateFunction &function); bool hasTaskProperties() const; QString text() const; QString title() const; bool isDone() const; QDateTime startDate() const; QDateTime dueDate() const; + QAbstractItemModel *attachmentModel() const; QString delegateText() const; static int autoSaveDelay(); bool editingInProgress() const; public slots: void setText(const QString &text); void setTitle(const QString &title); void setDone(bool done); void setStartDate(const QDateTime &start); void setDueDate(const QDateTime &due); void delegate(const QString &name, const QString &email); void setEditingInProgress(bool editingInProgress); signals: void artifactChanged(const Domain::Artifact::Ptr &artifact); void hasTaskPropertiesChanged(bool hasTaskProperties); void textChanged(const QString &text); void titleChanged(const QString &title); void doneChanged(bool done); void startDateChanged(const QDateTime &date); void dueDateChanged(const QDateTime &due); void delegateTextChanged(const QString &delegateText); private slots: void onTextChanged(const QString &text); void onTitleChanged(const QString &title); void onDoneChanged(bool done); void onStartDateChanged(const QDateTime &start); void onDueDateChanged(const QDateTime &due); void onDelegateChanged(const Domain::Task::Delegate &delegate); void save(); private: void setSaveNeeded(bool needed); bool isSaveNeeded() const; void applyNewText(const QString &text); void applyNewTitle(const QString &title); void applyNewDone(bool done); void applyNewStartDate(const QDateTime &start); void applyNewDueDate(const QDateTime &due); Domain::Artifact::Ptr m_artifact; SaveFunction m_saveFunction; DelegateFunction m_delegateFunction; QString m_text; QString m_title; bool m_done; QDateTime m_start; QDateTime m_due; + AttachmentModel *m_attachmentModel; QString m_delegateText; QTimer *m_saveTimer; bool m_saveNeeded; bool m_editingInProgress; }; } #endif // PRESENTATION_ARTIFACTEDITORMODEL_H diff --git a/tests/units/presentation/artifacteditormodeltest.cpp b/tests/units/presentation/artifacteditormodeltest.cpp index 2b6573ae..4714f6d6 100644 --- a/tests/units/presentation/artifacteditormodeltest.cpp +++ b/tests/units/presentation/artifacteditormodeltest.cpp @@ -1,515 +1,542 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "utils/mockobject.h" #include #include "testlib/fakejob.h" #include "domain/task.h" #include "domain/note.h" #include "presentation/artifacteditormodel.h" #include "presentation/errorhandler.h" using namespace mockitopp; class FakeErrorHandler : public Presentation::ErrorHandler { public: void doDisplayMessage(const QString &message) { m_message = message; } QString m_message; }; class ArtifactEditorModelTest : public QObject { Q_OBJECT private slots: void shouldHaveEmptyDefaultState() { // GIVEN Presentation::ArtifactEditorModel model; // WHEN // Nothing // THEN QVERIFY(model.artifact().isNull()); QVERIFY(!model.hasTaskProperties()); QVERIFY(model.text().isEmpty()); QVERIFY(model.title().isEmpty()); QVERIFY(!model.isDone()); QVERIFY(model.startDate().isNull()); QVERIFY(model.dueDate().isNull()); + QVERIFY(model.attachmentModel() != nullptr); QVERIFY(model.delegateText().isNull()); QVERIFY(!model.hasSaveFunction()); QVERIFY(!model.hasDelegateFunction()); } void shouldHaveTaskProperties() { // GIVEN Presentation::ArtifactEditorModel model; QSignalSpy textSpy(&model, &Presentation::ArtifactEditorModel::textChanged); QSignalSpy titleSpy(&model, &Presentation::ArtifactEditorModel::titleChanged); QSignalSpy doneSpy(&model, &Presentation::ArtifactEditorModel::doneChanged); QSignalSpy startSpy(&model, &Presentation::ArtifactEditorModel::startDateChanged); QSignalSpy dueSpy(&model, &Presentation::ArtifactEditorModel::dueDateChanged); + QSignalSpy attachmentSpy(model.attachmentModel(), &QAbstractItemModel::modelReset); QSignalSpy delegateSpy(&model, &Presentation::ArtifactEditorModel::delegateTextChanged); + Domain::Task::Attachments attachments; + + Domain::Task::Attachment dataAttachment; + dataAttachment.setData("foo"); + dataAttachment.setLabel("dataAttachment"); + dataAttachment.setMimeType("text/plain"); + dataAttachment.setIconName("text-plain"); + attachments.append(dataAttachment); + + Domain::Task::Attachment uriAttachment; + uriAttachment.setUri(QUrl("https://www.kde.org")); + uriAttachment.setLabel("uriAttachment"); + uriAttachment.setMimeType("text/html"); + uriAttachment.setIconName("text-html"); + attachments.append(uriAttachment); + auto task = Domain::Task::Ptr::create(); task->setText(QStringLiteral("description")); task->setTitle(QStringLiteral("title")); task->setDone(true); task->setStartDate(QDateTime::currentDateTime()); task->setDueDate(QDateTime::currentDateTime().addDays(2)); + task->setAttachments(attachments); task->setDelegate(Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("john@doe.com"))); // WHEN model.setArtifact(task); // To make sure we don't signal too much model.setText(task->text()); model.setTitle(task->title()); model.setDone(task->isDone()); model.setStartDate(task->startDate()); model.setDueDate(task->dueDate()); // THEN QVERIFY(model.hasTaskProperties()); QCOMPARE(textSpy.size(), 1); QCOMPARE(textSpy.takeFirst().at(0).toString(), task->text()); QCOMPARE(model.property("text").toString(), task->text()); QCOMPARE(titleSpy.size(), 1); QCOMPARE(titleSpy.takeFirst().at(0).toString(), task->title()); QCOMPARE(model.property("title").toString(), task->title()); QCOMPARE(doneSpy.size(), 1); QCOMPARE(doneSpy.takeFirst().at(0).toBool(), task->isDone()); QCOMPARE(model.property("done").toBool(), task->isDone()); QCOMPARE(startSpy.size(), 1); QCOMPARE(startSpy.takeFirst().at(0).toDateTime(), task->startDate()); QCOMPARE(model.property("startDate").toDateTime(), task->startDate()); QCOMPARE(dueSpy.size(), 1); QCOMPARE(dueSpy.takeFirst().at(0).toDateTime(), task->dueDate()); QCOMPARE(model.property("dueDate").toDateTime(), task->dueDate()); QCOMPARE(delegateSpy.size(), 1); QCOMPARE(delegateSpy.takeFirst().at(0).toString(), task->delegate().display()); QCOMPARE(model.property("delegateText").toString(), task->delegate().display()); + + QCOMPARE(attachmentSpy.size(), 1); + auto am = model.attachmentModel(); + QCOMPARE(am->rowCount(), 2); + QCOMPARE(am->data(am->index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("dataAttachment")); + QCOMPARE(am->data(am->index(0, 0), Qt::DecorationRole).value(), QIcon::fromTheme("text-plain")); + QCOMPARE(am->data(am->index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("uriAttachment")); + QCOMPARE(am->data(am->index(1, 0), Qt::DecorationRole).value(), QIcon::fromTheme("text-html")); } void shouldHaveNoteProperties() { // GIVEN Presentation::ArtifactEditorModel model; QSignalSpy textSpy(&model, &Presentation::ArtifactEditorModel::textChanged); QSignalSpy titleSpy(&model, &Presentation::ArtifactEditorModel::titleChanged); QSignalSpy doneSpy(&model, &Presentation::ArtifactEditorModel::doneChanged); QSignalSpy startSpy(&model, &Presentation::ArtifactEditorModel::startDateChanged); QSignalSpy dueSpy(&model, &Presentation::ArtifactEditorModel::dueDateChanged); QSignalSpy delegateSpy(&model, &Presentation::ArtifactEditorModel::delegateTextChanged); auto note = Domain::Note::Ptr::create(); note->setText(QStringLiteral("description")); note->setTitle(QStringLiteral("title")); // WHEN model.setArtifact(note); // To make sure we don't signal too much model.setText(note->text()); model.setTitle(note->title()); // THEN QVERIFY(!model.hasTaskProperties()); QCOMPARE(textSpy.size(), 1); QCOMPARE(textSpy.takeFirst().at(0).toString(), note->text()); QCOMPARE(model.property("text").toString(), note->text()); QCOMPARE(titleSpy.size(), 1); QCOMPARE(titleSpy.takeFirst().at(0).toString(), note->title()); QCOMPARE(model.property("title").toString(), note->title()); QCOMPARE(doneSpy.size(), 1); QCOMPARE(doneSpy.takeFirst().at(0).toBool(), false); QCOMPARE(model.property("done").toBool(), false); QCOMPARE(startSpy.size(), 1); QVERIFY(startSpy.takeFirst().at(0).toDateTime().isNull()); QVERIFY(model.property("startDate").toDateTime().isNull()); QCOMPARE(dueSpy.size(), 1); QVERIFY(dueSpy.takeFirst().at(0).toDateTime().isNull()); QVERIFY(model.property("dueDate").toDateTime().isNull()); QCOMPARE(delegateSpy.size(), 1); QVERIFY(delegateSpy.takeFirst().at(0).toString().isEmpty()); QVERIFY(model.property("delegateText").toString().isEmpty()); } void shouldReactToArtifactPropertyChanges_data() { QTest::addColumn("artifact"); QTest::addColumn("propertyName"); QTest::addColumn("propertyValue"); QTest::addColumn("signal"); QTest::newRow("note text") << Domain::Artifact::Ptr(Domain::Note::Ptr::create()) << QByteArray("text") << QVariant("new text") << QByteArray(SIGNAL(textChanged(QString))); QTest::newRow("note title") << Domain::Artifact::Ptr(Domain::Note::Ptr::create()) << QByteArray("title") << QVariant("new title") << QByteArray(SIGNAL(titleChanged(QString))); QTest::newRow("task text") << Domain::Artifact::Ptr(Domain::Task::Ptr::create()) << QByteArray("text") << QVariant("new text") << QByteArray(SIGNAL(textChanged(QString))); QTest::newRow("task title") << Domain::Artifact::Ptr(Domain::Task::Ptr::create()) << QByteArray("title") << QVariant("new title") << QByteArray(SIGNAL(titleChanged(QString))); QTest::newRow("task done") << Domain::Artifact::Ptr(Domain::Task::Ptr::create()) << QByteArray("done") << QVariant(true) << QByteArray(SIGNAL(doneChanged(bool))); QTest::newRow("task start") << Domain::Artifact::Ptr(Domain::Task::Ptr::create()) << QByteArray("startDate") << QVariant(QDateTime::currentDateTime()) << QByteArray(SIGNAL(startDateChanged(QDateTime))); QTest::newRow("task due") << Domain::Artifact::Ptr(Domain::Task::Ptr::create()) << QByteArray("dueDate") << QVariant(QDateTime::currentDateTime().addDays(2)) << QByteArray(SIGNAL(dueDateChanged(QDateTime))); } void shouldReactToArtifactPropertyChanges() { // GIVEN QFETCH(Domain::Artifact::Ptr, artifact); QFETCH(QByteArray, propertyName); QFETCH(QVariant, propertyValue); QFETCH(QByteArray, signal); Presentation::ArtifactEditorModel model; model.setArtifact(artifact); QSignalSpy spy(&model, signal.constData()); // WHEN artifact->setProperty(propertyName, propertyValue); // THEN QCOMPARE(spy.size(), 1); QCOMPARE(spy.takeFirst().at(0), propertyValue); QCOMPARE(model.property(propertyName), propertyValue); } void shouldNotReactToArtifactPropertyChangesWhenEditing_data() { shouldReactToArtifactPropertyChanges_data(); } void shouldNotReactToArtifactPropertyChangesWhenEditing() { // GIVEN QFETCH(Domain::Artifact::Ptr, artifact); QFETCH(QByteArray, propertyName); QFETCH(QVariant, propertyValue); QFETCH(QByteArray, signal); Presentation::ArtifactEditorModel model; model.setArtifact(artifact); QSignalSpy spy(&model, signal.constData()); // WHEN const auto oldPropertyValue = artifact->property(propertyName); model.setEditingInProgress(true); artifact->setProperty(propertyName, propertyValue); // THEN QVERIFY(spy.isEmpty()); QCOMPARE(model.property(propertyName), oldPropertyValue); } void shouldReactToTaskDelegateChanges() { // GIVEN auto task = Domain::Task::Ptr::create(); Presentation::ArtifactEditorModel model; model.setArtifact(task); QSignalSpy spy(&model, &Presentation::ArtifactEditorModel::delegateTextChanged); // WHEN task->setDelegate(Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("john@doe.com"))); // THEN QCOMPARE(spy.size(), 1); QCOMPARE(spy.takeFirst().at(0).toString(), task->delegate().display()); QCOMPARE(model.property("delegateText").toString(), task->delegate().display()); } void shouldApplyChangesBackToArtifactAfterADelay_data() { shouldReactToArtifactPropertyChanges_data(); } void shouldApplyChangesBackToArtifactAfterADelay() { // GIVEN QFETCH(Domain::Artifact::Ptr, artifact); QFETCH(QByteArray, propertyName); QFETCH(QVariant, propertyValue); QFETCH(QByteArray, signal); auto savedArtifact = Domain::Artifact::Ptr(); auto save = [this, &savedArtifact] (const Domain::Artifact::Ptr &artifact) { savedArtifact = artifact; return new FakeJob(this); }; Presentation::ArtifactEditorModel model; model.setSaveFunction(save); model.setArtifact(artifact); QSignalSpy spy(&model, signal.constData()); // WHEN model.setProperty(propertyName, propertyValue); // THEN QCOMPARE(spy.size(), 1); QCOMPARE(spy.takeFirst().at(0), propertyValue); QCOMPARE(model.property(propertyName), propertyValue); QVERIFY(artifact->property(propertyName) != propertyValue); QVERIFY(!savedArtifact); // WHEN (apply after delay) QTest::qWait(model.autoSaveDelay() + 50); // THEN QCOMPARE(savedArtifact, artifact); QCOMPARE(artifact->property(propertyName), propertyValue); } void shouldApplyChangesImmediatelyIfANewArtifactIsSet_data() { shouldReactToArtifactPropertyChanges_data(); } void shouldApplyChangesImmediatelyIfANewArtifactIsSet() { // GIVEN QFETCH(Domain::Artifact::Ptr, artifact); QFETCH(QByteArray, propertyName); QFETCH(QVariant, propertyValue); QFETCH(QByteArray, signal); auto savedArtifact = Domain::Artifact::Ptr(); auto save = [this, &savedArtifact] (const Domain::Artifact::Ptr &artifact) { savedArtifact = artifact; return new FakeJob(this); }; Presentation::ArtifactEditorModel model; model.setSaveFunction(save); QVERIFY(model.hasSaveFunction()); model.setArtifact(artifact); QSignalSpy spy(&model, signal.constData()); // WHEN model.setProperty(propertyName, propertyValue); // THEN QCOMPARE(spy.size(), 1); QCOMPARE(spy.takeFirst().at(0), propertyValue); QCOMPARE(model.property(propertyName), propertyValue); QVERIFY(artifact->property(propertyName) != propertyValue); QVERIFY(!savedArtifact); // WHEN (apply immediately) model.setArtifact(Domain::Task::Ptr::create()); // THEN QCOMPARE(savedArtifact, artifact); QCOMPARE(artifact->property(propertyName), propertyValue); savedArtifact.clear(); // WHEN (nothing else happens after a delay) QTest::qWait(model.autoSaveDelay() + 50); // THEN QVERIFY(!savedArtifact); QCOMPARE(artifact->property(propertyName), propertyValue); } void shouldApplyChangesImmediatelyIfDeleted_data() { shouldReactToArtifactPropertyChanges_data(); } void shouldApplyChangesImmediatelyIfDeleted() { // GIVEN QFETCH(Domain::Artifact::Ptr, artifact); QFETCH(QByteArray, propertyName); QFETCH(QVariant, propertyValue); QFETCH(QByteArray, signal); auto savedArtifact = Domain::Artifact::Ptr(); auto save = [this, &savedArtifact] (const Domain::Artifact::Ptr &artifact) { savedArtifact = artifact; return new FakeJob(this); }; auto model = new Presentation::ArtifactEditorModel; model->setSaveFunction(save); QVERIFY(model->hasSaveFunction()); model->setArtifact(artifact); QSignalSpy spy(model, signal.constData()); // WHEN model->setProperty(propertyName, propertyValue); // THEN QCOMPARE(spy.size(), 1); QCOMPARE(spy.takeFirst().at(0), propertyValue); QCOMPARE(model->property(propertyName), propertyValue); QVERIFY(artifact->property(propertyName) != propertyValue); QVERIFY(!savedArtifact); // WHEN (apply immediately) delete model; // THEN QCOMPARE(savedArtifact, artifact); QCOMPARE(artifact->property(propertyName), propertyValue); } void shouldLaunchDelegation() { // GIVEN auto task = Domain::Task::Ptr::create(); auto expectedDelegate = Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("john@doe.com")); auto delegatedTask = Domain::Task::Ptr(); auto delegate = Domain::Task::Delegate(); auto delegateFunction = [this, &delegatedTask, &delegate] (const Domain::Task::Ptr &task, const Domain::Task::Delegate &d) { delegatedTask = task; delegate = d; return new FakeJob(this); }; Presentation::ArtifactEditorModel model; model.setDelegateFunction(delegateFunction); QVERIFY(model.hasDelegateFunction()); model.setArtifact(task); // WHEN model.delegate(QStringLiteral("John Doe"), QStringLiteral("john@doe.com")); // THEN QCOMPARE(delegatedTask, task); QCOMPARE(delegate, expectedDelegate); QVERIFY(!task->delegate().isValid()); } void shouldGetAnErrorMessageWhenSaveFailed() { // GIVEN auto task = Domain::Task::Ptr::create(); task->setTitle(QStringLiteral("Task 1")); auto savedArtifact = Domain::Artifact::Ptr(); auto save = [this, &savedArtifact] (const Domain::Artifact::Ptr &artifact) { savedArtifact = artifact; auto job = new FakeJob(this); job->setExpectedError(KJob::KilledJobError, QStringLiteral("Foo")); return job; }; auto model = new Presentation::ArtifactEditorModel; model->setSaveFunction(save); QVERIFY(model->hasSaveFunction()); FakeErrorHandler errorHandler; model->setErrorHandler(&errorHandler); model->setArtifact(task); // WHEN model->setProperty("title", "Foo"); delete model; // THEN QTest::qWait(150); QCOMPARE(errorHandler.m_message, QStringLiteral("Cannot modify task Task 1: Foo")); } void shouldDisconnectFromPreviousArtifact_data() { shouldReactToArtifactPropertyChanges_data(); } void shouldDisconnectFromPreviousArtifact() { // GIVEN QFETCH(Domain::Artifact::Ptr, artifact); QFETCH(QByteArray, propertyName); QFETCH(QVariant, propertyValue); QFETCH(QByteArray, signal); Presentation::ArtifactEditorModel model; model.setArtifact(artifact); QSignalSpy spy(&model, signal.constData()); Domain::Artifact::Ptr newArtifact = Domain::Task::Ptr::create(); // WHEN model.setArtifact(newArtifact); // modifying the *old* artifact should have no effect. artifact->setProperty(propertyName, propertyValue); // THEN QCOMPARE(spy.size(), 1); // emitted by setArtifact QVERIFY(model.property(propertyName) != artifact->property(propertyName)); } }; ZANSHIN_TEST_MAIN(ArtifactEditorModelTest) #include "artifacteditormodeltest.moc"