diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -55,6 +55,35 @@ ENDFOREACH(_testname) ENDMACRO(ZANSHIN_AUTO_TESTS) +MACRO(ZANSHIN_FEATURE_TESTS) + set(prefix "${CMAKE_CURRENT_SOURCE_DIR}") + string(REPLACE "${CMAKE_SOURCE_DIR}" "" prefix "${prefix}") + string(REPLACE "/" "-" prefix "${prefix}") + string(REPLACE "\\" "-" prefix "${prefix}") + string(LENGTH "${prefix}" prefix_length) + string(SUBSTRING "${prefix}" 1 ${prefix_length}-1 prefix) + + FOREACH(_testname ${ARGN}) + set(_prefixed_testname "${prefix}-${_testname}") + add_executable(${_prefixed_testname} ${_testname}.cpp) + add_test(NAME ${_prefixed_testname} COMMAND ${_prefixed_testname}) + kde_enable_exceptions() + target_link_libraries(${_prefixed_testname} + Qt5::Test + + featurelib + testlib + akonadi + domain + presentation + utils + widgets + KF5::AkonadiXml + ) + ENDFOREACH(_testname) +ENDMACRO(ZANSHIN_FEATURE_TESTS) + +add_subdirectory(featurelib) add_subdirectory(features) add_subdirectory(manual) add_subdirectory(benchmarks) diff --git a/tests/featurelib/CMakeLists.txt b/tests/featurelib/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/tests/featurelib/CMakeLists.txt @@ -0,0 +1,15 @@ +set(featurelib_SRCS + zanshincontext.cpp + "${CMAKE_SOURCE_DIR}/src/zanshin/app/dependencies.cpp" +) + +add_definitions(-DZANSHIN_USER_XMLDATA="${CMAKE_SOURCE_DIR}/tests/features/testenv/data/testdata.xml") + +include_directories(${CMAKE_SOURCE_DIR}/tests ${CMAKE_SOURCE_DIR}/src) +add_library(featurelib STATIC ${featurelib_SRCS}) +target_link_libraries(featurelib + Qt5::Test + KF5::ConfigCore + presentation +) + diff --git a/tests/featurelib/zanshincontext.h b/tests/featurelib/zanshincontext.h new file mode 100644 --- /dev/null +++ b/tests/featurelib/zanshincontext.h @@ -0,0 +1,135 @@ +/* This file is part of Zanshin + + Copyright 2014-2019 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 +#include +#include + +#include "domain/task.h" +#include "presentation/errorhandler.h" + +#include "testlib/akonadifakedata.h" +#include "testlib/fakejob.h" + +class QAbstractItemModel; +class QSortFilterProxyModel; +class MonitorSpy; + +class FakeErrorHandler : public Presentation::ErrorHandler +{ +public: + void doDisplayMessage(const QString &) override; +}; + +#define ZANSHIN_CONTEXT ZanshinContext context +#define Given(a) QVERIFY(context.a) +#define When(a) QVERIFY(context.a) +#define Then(a) QVERIFY(context.a) +#define And(a) QVERIFY(context.a) + +class ZanshinContext : public QObject +{ + Q_OBJECT +public: + struct TableData + { + QVector roles; + QVector> rows; + }; + + explicit ZanshinContext(QObject *parent = nullptr); + + // GIVEN + Q_REQUIRED_RESULT bool I_display_the_available_data_sources(); + Q_REQUIRED_RESULT bool I_display_the_available_pages(); + Q_REQUIRED_RESULT bool I_display_the_page(const QString &pageName); + Q_REQUIRED_RESULT bool there_is_an_item_in_the_central_list(const QString &taskName); + Q_REQUIRED_RESULT bool there_is_an_item_in_the_available_data_sources(const QString &sourceName); + Q_REQUIRED_RESULT bool the_central_list_contains_items_named(const QStringList &taskNames); + + // WHEN + Q_REQUIRED_RESULT bool I_look_at_the_central_list(); + Q_REQUIRED_RESULT bool I_check_the_item(); + Q_REQUIRED_RESULT bool I_uncheck_the_item(); + Q_REQUIRED_RESULT bool I_remove_the_item(); + Q_REQUIRED_RESULT bool I_promote_the_item(); + Q_REQUIRED_RESULT bool I_add_a_project(const QString &projectName, const QString &parentSourceName); + Q_REQUIRED_RESULT bool I_add_a_context(const QString &contextName); + Q_REQUIRED_RESULT bool I_add_a_task(const QString &taskName); + Q_REQUIRED_RESULT bool I_rename_a_page(const QString &path, const QString &oldName, const QString &newName); + Q_REQUIRED_RESULT bool I_remove_a_page(const QString &path, const QString &pageName); + Q_REQUIRED_RESULT bool I_add_a_task_child(const QString &childName, const QString &parentName); + Q_REQUIRED_RESULT bool I_list_the_items(); + Q_REQUIRED_RESULT bool I_open_the_item_in_the_editor(); + Q_REQUIRED_RESULT bool I_mark_the_item_done_in_the_editor(); + Q_REQUIRED_RESULT bool I_change_the_editor_field(const QString &field, const QVariant &value); + Q_REQUIRED_RESULT bool I_rename_the_item(const QString &taskName); + Q_REQUIRED_RESULT bool I_open_the_item_in_the_editor_again(); + Q_REQUIRED_RESULT bool I_drop_the_item_on_the_central_list(const QString &dropSiteName); + Q_REQUIRED_RESULT bool I_drop_the_item_on_the_blank_area_of_the_central_list(); + Q_REQUIRED_RESULT bool I_drop_items_on_the_central_list(const QString &dropSiteName); + Q_REQUIRED_RESULT bool I_drop_the_item_on_the_page_list(const QString &pageName); + Q_REQUIRED_RESULT bool I_drop_items_on_the_page_list(const QString &pageName); + Q_REQUIRED_RESULT bool I_change_the_setting(const QString &key, qint64 id); + Q_REQUIRED_RESULT bool I_change_the_default_data_source(const QString &sourceName); + + // THEN + Q_REQUIRED_RESULT bool the_list_is(const TableData &data); + Q_REQUIRED_RESULT bool the_list_contains(const QString &itemName); + Q_REQUIRED_RESULT bool the_list_does_not_contain(const QString &itemName); + Q_REQUIRED_RESULT bool the_task_corresponding_to_the_item_is_done(); + Q_REQUIRED_RESULT bool the_editor_shows_the_task_as_done(); + Q_REQUIRED_RESULT bool the_editor_shows_the_field(const QString &field, const QVariant &expectedValue); + Q_REQUIRED_RESULT bool the_default_data_source_is(const QString &sourceName); + Q_REQUIRED_RESULT bool the_setting_is(const QString &key, qint64 expectedId); + +private: + void setModel(QAbstractItemModel *model); + QAbstractItemModel *sourceModel() const; + QAbstractItemModel *model() const; + + Domain::Task::Ptr currentTask() const; + + void waitForEmptyJobQueue(); + void waitForStableState(); + + void collectIndicesImpl(const QModelIndex &root = QModelIndex()); + void collectIndices(); + + QSharedPointer m_appModel; + + QList m_indices; + QPersistentModelIndex m_index; + QObject *m_presentation; + QObject *m_editor; + QList m_dragIndices; + + Testlib::AkonadiFakeData m_data; + + QSortFilterProxyModel *m_proxyModel; + QAbstractItemModel *m_model; + QAbstractItemModel *m_sourceModel; + MonitorSpy *m_monitorSpy; + FakeErrorHandler m_errorHandler; +}; diff --git a/tests/featurelib/zanshincontext.cpp b/tests/featurelib/zanshincontext.cpp new file mode 100644 --- /dev/null +++ b/tests/featurelib/zanshincontext.cpp @@ -0,0 +1,844 @@ +/* This file is part of Zanshin + + Copyright 2014-2019 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 "zanshincontext.h" + +#include "akonadi/akonadiapplicationselectedattribute.h" +#include "akonadi/akonadicachingstorage.h" +#include "akonadi/akonadistorageinterface.h" +#include "akonadi/akonaditimestampattribute.h" + +#include "presentation/applicationmodel.h" +#include "presentation/errorhandler.h" +#include "presentation/querytreemodelbase.h" + +#include "testlib/akonadifakedataxmlloader.h" +#include "testlib/monitorspy.h" + +#include "utils/dependencymanager.h" +#include "utils/jobhandler.h" + +#include + +#include +#include + +#include +#include +#include + +namespace App +{ + void initializeDependencies(); +} + +void FakeErrorHandler::doDisplayMessage(const QString &) +{ +} + +ZanshinContext::ZanshinContext(QObject *parent) + : QObject(parent), + m_presentation(nullptr), + m_editor(nullptr), + m_proxyModel(new QSortFilterProxyModel(this)), + m_model(nullptr), + m_sourceModel(nullptr), + m_monitorSpy(nullptr) +{ + qputenv("ZANSHIN_OVERRIDE_DATE", "2015-03-10"); + + static bool initializedDependencies = false; + + if (!initializedDependencies) { + App::initializeDependencies(); + MonitorSpy::setExpirationDelay(200); + initializedDependencies = true; + } + + Akonadi::AttributeFactory::registerAttribute(); + Akonadi::AttributeFactory::registerAttribute(); + + const auto xmlFile = QString::fromLocal8Bit(ZANSHIN_USER_XMLDATA); + if (xmlFile.isEmpty()) { + qDebug() << "FATAL ERROR! ZANSHIN_USER_XMLDATA WAS NOT PROVIDED\n\n"; + exit(1); + } + + auto searchCollection = Akonadi::Collection(1); + searchCollection.setParentCollection(Akonadi::Collection::root()); + searchCollection.setName(QStringLiteral("Search")); + m_data.createCollection(searchCollection); + + auto loader = Testlib::AkonadiFakeDataXmlLoader(&m_data); + loader.load(xmlFile); + + // Swap regular dependencies for the fake data ones + auto &deps = Utils::DependencyManager::globalInstance(); + deps.add( + [this] (Utils::DependencyManager *) { + return m_data.createMonitor(); + } + ); + deps.add( + [this] (Utils::DependencyManager *deps) { + return new Akonadi::CachingStorage(deps->create(), + Akonadi::StorageInterface::Ptr(m_data.createStorage())); + } + ); + + using namespace Presentation; + m_proxyModel->setDynamicSortFilter(true); + + auto appModel = ApplicationModel::Ptr::create(); + + appModel->setErrorHandler(&m_errorHandler); + + m_appModel = appModel; + + auto monitor = Utils::DependencyManager::globalInstance().create(); + m_monitorSpy = new MonitorSpy(monitor.data(), this); +} + +// Note that setModel might invalidate the 'index' member variable, due to proxyModel->setSourceModel. +void ZanshinContext::setModel(QAbstractItemModel *model) +{ + if (m_sourceModel == model) + return; + m_sourceModel = model; + if (!qobject_cast(model)) { + m_proxyModel->setObjectName(QStringLiteral("m_proxyModel_in_ZanshinContext")); + m_proxyModel->setSourceModel(model); + m_proxyModel->setSortRole(Qt::DisplayRole); + m_proxyModel->sort(0); + m_model = m_proxyModel; + } else { + m_model = model; + } +} + +QAbstractItemModel *ZanshinContext::sourceModel() const +{ + return m_sourceModel; +} + +QAbstractItemModel *ZanshinContext::model() const +{ + return m_model; +} + +Domain::Task::Ptr ZanshinContext::currentTask() const +{ + return m_index.data(Presentation::QueryTreeModelBase::ObjectRole) + .value(); +} + +void ZanshinContext::waitForEmptyJobQueue() +{ + while (Utils::JobHandler::jobCount() != 0) { + QTest::qWait(20); + } +} + +void ZanshinContext::waitForStableState() +{ + waitForEmptyJobQueue(); + m_monitorSpy->waitForStableState(); +} + +void ZanshinContext::collectIndicesImpl(const QModelIndex &root) +{ + QAbstractItemModel *model = m_model; + for (int row = 0; row < model->rowCount(root); row++) { + const QModelIndex index = model->index(row, 0, root); + m_indices << index; + if (model->rowCount(index) > 0) + collectIndicesImpl(index); + } +} + +void ZanshinContext::collectIndices() +{ + m_indices.clear(); + collectIndicesImpl(); +} + +namespace Zanshin { + +QString indexString(const QModelIndex &index, int role = Qt::DisplayRole) +{ + if (role != Qt::DisplayRole) + return index.data(role).toString(); + + QString data = index.data(role).toString(); + + if (index.parent().isValid()) + return indexString(index.parent(), role) + " / " + data; + else + return data; +} + +QModelIndex findIndex(QAbstractItemModel *model, + const QString &string, + int role = Qt::DisplayRole, + const QModelIndex &root = QModelIndex()) +{ + for (int row = 0; row < model->rowCount(root); row++) { + const QModelIndex index = model->index(row, 0, root); + if (indexString(index, role) == string) + return index; + + if (model->rowCount(index) > 0) { + const QModelIndex found = findIndex(model, string, role, index); + if (found.isValid()) + return found; + } + } + + return QModelIndex(); +} + +void dumpIndices(const QList &indices) +{ + qDebug() << "Dumping list of size:" << indices.size(); + for (int row = 0; row < indices.size(); row++) { + qDebug() << row << indexString(indices.at(row)); + } +} + +inline bool verify(bool statement, const char *str, + const char *file, int line) +{ + if (statement) + return true; + + qDebug() << "Statement" << str << "returned FALSE"; + qDebug() << "Loc:" << file << line; + return false; +} + +template +inline bool compare(T const &t1, T const &t2, + const char *actual, const char *expected, + const char *file, int line) +{ + if (t1 == t2) + return true; + + qDebug() << "Compared values are not the same"; + qDebug() << "Actual (" << actual << ") :" << QTest::toString(t1); + qDebug() << "Expected (" << expected << ") :" << QTest::toString(t2); + qDebug() << "Loc:" << file << line; + return false; +} + +} // namespace Zanshin + +#define COMPARE(actual, expected) \ +do {\ + if (!Zanshin::compare(actual, expected, #actual, #expected, __FILE__, __LINE__))\ + return false;\ +} while (0) + +// Note: you should make sure that m_indices is filled in before calling this, +// e.g. calling Zanshin::collectIndices(context.get()) if not already done. +#define COMPARE_OR_DUMP(actual, expected) \ +do {\ + if (!Zanshin::compare(actual, expected, #actual, #expected, __FILE__, __LINE__)) {\ + Zanshin::dumpIndices(m_indices); \ + return false;\ + }\ +} while (0) + +#define VERIFY(statement) \ +do {\ + if (!Zanshin::verify((statement), #statement, __FILE__, __LINE__))\ + return false;\ +} while (0) + +// Note: you should make sure that m_indices is filled in before calling this, +// e.g. calling Zanshin::collectIndices(context.get()) if not already done. +#define VERIFY_OR_DUMP(statement) \ +do {\ + if (!Zanshin::verify((statement), #statement, __FILE__, __LINE__)) {\ + Zanshin::dumpIndices(m_indices); \ + return false;\ + }\ +} while (0) + +#define VERIFY_OR_DO(statement, whatToDo) \ +do {\ + if (!Zanshin::verify((statement), #statement, __FILE__, __LINE__)) {\ + whatToDo; \ + return false;\ + }\ +} while (0) + + +bool ZanshinContext::I_display_the_available_data_sources() +{ + auto availableSources = m_appModel->property("availableSources").value(); + VERIFY(availableSources); + + auto sourceListModel = availableSources->property("sourceListModel").value(); + VERIFY(sourceListModel); + + m_presentation = availableSources; + setModel(sourceListModel); + + return true; +} + +bool ZanshinContext::I_display_the_available_pages() +{ + m_presentation = m_appModel->property("availablePages").value(); + setModel(m_presentation->property("pageListModel").value()); + return true; +} + +bool ZanshinContext::I_display_the_page(const QString &pageName) +{ + auto availablePages = m_appModel->property("availablePages").value(); + VERIFY(availablePages); + + auto pageListModel = availablePages->property("pageListModel").value(); + VERIFY(pageListModel); + waitForEmptyJobQueue(); + + QModelIndex pageIndex = Zanshin::findIndex(pageListModel, pageName); + VERIFY(pageIndex.isValid()); + + QObject *page = nullptr; + QMetaObject::invokeMethod(availablePages, "createPageForIndex", + Q_RETURN_ARG(QObject*, page), + Q_ARG(QModelIndex, pageIndex)); + VERIFY(page); + + VERIFY(m_appModel->setProperty("currentPage", QVariant::fromValue(page))); + m_presentation = m_appModel->property("currentPage").value(); + + return true; +} + +bool ZanshinContext::there_is_an_item_in_the_central_list(const QString &taskName) +{ + auto m = m_presentation->property("centralListModel").value(); + setModel(m); + waitForEmptyJobQueue(); + + collectIndices(); + m_index = Zanshin::findIndex(model(), taskName); + VERIFY_OR_DUMP(m_index.isValid()); + + return true; +} + +bool ZanshinContext::there_is_an_item_in_the_available_data_sources(const QString &sourceName) +{ + auto availableSources = m_appModel->property("availableSources").value(); + VERIFY(availableSources); + auto m = availableSources->property("sourceListModel").value(); + VERIFY(m); + waitForEmptyJobQueue(); + setModel(m); + + collectIndices(); + m_index = Zanshin::findIndex(model(), sourceName); + VERIFY_OR_DUMP(m_index.isValid()); + + return true; +} + +bool ZanshinContext::the_central_list_contains_items_named(const QStringList &taskNames) +{ + m_dragIndices.clear(); + + auto m = m_presentation->property("centralListModel").value(); + waitForEmptyJobQueue(); + setModel(m); + + for (const auto &taskName : taskNames) { + QModelIndex index = Zanshin::findIndex(model(), taskName); + VERIFY_OR_DO(index.isValid(), Zanshin::dumpIndices(m_dragIndices)); + m_dragIndices << index; + } + + return true; +} + +bool ZanshinContext::I_look_at_the_central_list() +{ + auto m = m_presentation->property("centralListModel").value(); + setModel(m); + waitForStableState(); + return true; +} + +bool ZanshinContext::I_check_the_item() +{ + VERIFY(model()->setData(m_index, Qt::Checked, Qt::CheckStateRole)); + waitForStableState(); + return true; +} + +bool ZanshinContext::I_uncheck_the_item() +{ + VERIFY(model()->setData(m_index, Qt::Unchecked, Qt::CheckStateRole)); + waitForStableState(); + return true; +} + +bool ZanshinContext::I_remove_the_item() +{ + VERIFY(QMetaObject::invokeMethod(m_presentation, "removeItem", Q_ARG(QModelIndex, m_index))); + waitForStableState(); + return true; +} + +bool ZanshinContext::I_promote_the_item() +{ + VERIFY(QMetaObject::invokeMethod(m_presentation, "promoteItem", Q_ARG(QModelIndex, m_index))); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_add_a_project(const QString &projectName, const QString &parentSourceName) +{ + auto availableSources = m_appModel->property("availableSources").value(); + VERIFY(availableSources); + auto sourceList = availableSources->property("sourceListModel").value(); + VERIFY(sourceList); + waitForStableState(); + QModelIndex index = Zanshin::findIndex(sourceList, parentSourceName); + VERIFY(index.isValid()); + auto source = index.data(Presentation::QueryTreeModelBase::ObjectRole) + .value(); + VERIFY(source); + + VERIFY(QMetaObject::invokeMethod(m_presentation, "addProject", + Q_ARG(QString, projectName), + Q_ARG(Domain::DataSource::Ptr, source))); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_add_a_context(const QString &contextName) +{ + waitForStableState(); + + VERIFY(QMetaObject::invokeMethod(m_presentation, + "addContext", + Q_ARG(QString, contextName))); + waitForStableState(); + + return true; + +} + +bool ZanshinContext::I_add_a_task(const QString &taskName) +{ + waitForStableState(); + + VERIFY(QMetaObject::invokeMethod(m_presentation, + "addItem", + Q_ARG(QString, taskName))); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_rename_a_page(const QString &path, const QString &oldName, const QString &newName) +{ + const QString pageNodeName = path + " / "; + + VERIFY(!pageNodeName.isEmpty()); + + auto availablePages = m_appModel->property("availablePages").value(); + VERIFY(availablePages); + + auto pageListModel = availablePages->property("pageListModel").value(); + VERIFY(pageListModel); + waitForStableState(); + + QModelIndex pageIndex = Zanshin::findIndex(pageListModel, pageNodeName + oldName); + VERIFY(pageIndex.isValid()); + + pageListModel->setData(pageIndex, newName); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_remove_a_page(const QString &path, const QString &pageName) +{ + const QString pageNodeName = path + " / "; + + VERIFY(!pageNodeName.isEmpty()); + + auto availablePages = m_appModel->property("availablePages").value(); + VERIFY(availablePages); + + auto pageListModel = availablePages->property("pageListModel").value(); + VERIFY(pageListModel); + waitForStableState(); + + QModelIndex pageIndex = Zanshin::findIndex(pageListModel, pageNodeName + pageName); + VERIFY(pageIndex.isValid()); + + VERIFY(QMetaObject::invokeMethod(availablePages, "removeItem", + Q_ARG(QModelIndex, pageIndex))); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_add_a_task_child(const QString &childName, const QString &parentName) +{ + waitForStableState(); + + auto parentIndex = QModelIndex(); + for (int row = 0; row < m_indices.size(); row++) { + auto index = m_indices.at(row); + if (Zanshin::indexString(index) == parentName) { + parentIndex = index; + break; + } + } + + VERIFY_OR_DUMP(parentIndex.isValid()); + + VERIFY(QMetaObject::invokeMethod(m_presentation, + "addItem", + Q_ARG(QString, childName), + Q_ARG(QModelIndex, parentIndex))); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_list_the_items() +{ + waitForStableState(); + collectIndices(); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_open_the_item_in_the_editor() +{ + auto task = currentTask(); + VERIFY(task); + m_editor = m_appModel->property("editor").value(); + VERIFY(m_editor); + VERIFY(m_editor->setProperty("task", QVariant::fromValue(task))); + + return true; +} + +bool ZanshinContext::I_mark_the_item_done_in_the_editor() +{ + VERIFY(m_editor->setProperty("done", true)); + return true; +} + +bool ZanshinContext::I_change_the_editor_field(const QString &field, const QVariant &value) +{ + const QByteArray property = (field == QStringLiteral("text")) ? field.toUtf8() + : (field == QStringLiteral("title")) ? field.toUtf8() + : (field == QStringLiteral("start date")) ? "startDate" + : (field == QStringLiteral("due date")) ? "dueDate" + : QByteArray(); + + VERIFY(value.isValid()); + VERIFY(!property.isEmpty()); + + VERIFY(m_editor->setProperty("editingInProgress", true)); + VERIFY(m_editor->setProperty(property, value)); + + return true; +} + +bool ZanshinContext::I_rename_the_item(const QString &taskName) +{ + VERIFY(m_editor->setProperty("editingInProgress", false)); + VERIFY(model()->setData(m_index, taskName, Qt::EditRole)); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_open_the_item_in_the_editor_again() +{ + auto task = currentTask(); + VERIFY(task); + VERIFY(m_editor->setProperty("task", QVariant::fromValue(Domain::Task::Ptr()))); + VERIFY(m_editor->setProperty("task", QVariant::fromValue(task))); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_drop_the_item_on_the_central_list(const QString &dropSiteName) +{ + VERIFY(m_index.isValid()); + const QMimeData *data = model()->mimeData(QModelIndexList() << m_index); + + QAbstractItemModel *destModel = model(); + QModelIndex dropIndex = Zanshin::findIndex(destModel, dropSiteName); + VERIFY(dropIndex.isValid()); + VERIFY(destModel->dropMimeData(data, Qt::MoveAction, -1, -1, dropIndex)); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_drop_the_item_on_the_blank_area_of_the_central_list() +{ + VERIFY(m_index.isValid()); + const QMimeData *data = model()->mimeData(QModelIndexList() << m_index); + + QAbstractItemModel *destModel = model(); + VERIFY(destModel->dropMimeData(data, Qt::MoveAction, -1, -1, QModelIndex())); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_drop_items_on_the_central_list(const QString &dropSiteName) +{ + VERIFY(!m_dragIndices.isEmpty()); + QModelIndexList indexes; + bool allValid = true; + std::transform(m_dragIndices.constBegin(), m_dragIndices.constEnd(), + std::back_inserter(indexes), + [&allValid] (const QPersistentModelIndex &index) { + allValid &= index.isValid(); + return index; + }); + VERIFY(allValid); + + const QMimeData *data = model()->mimeData(indexes); + + QAbstractItemModel *destModel = model(); + QModelIndex dropIndex = Zanshin::findIndex(destModel, dropSiteName); + VERIFY(dropIndex.isValid()); + VERIFY(destModel->dropMimeData(data, Qt::MoveAction, -1, -1, dropIndex)); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_drop_the_item_on_the_page_list(const QString &pageName) +{ + VERIFY(m_index.isValid()); + const QMimeData *data = model()->mimeData(QModelIndexList() << m_index); + + auto availablePages = m_appModel->property("availablePages").value(); + VERIFY(availablePages); + + auto destModel = availablePages->property("pageListModel").value(); + VERIFY(destModel); + waitForStableState(); + + QModelIndex dropIndex = Zanshin::findIndex(destModel, pageName); + VERIFY(dropIndex.isValid()); + VERIFY(destModel->dropMimeData(data, Qt::MoveAction, -1, -1, dropIndex)); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_drop_items_on_the_page_list(const QString &pageName) +{ + VERIFY(!m_dragIndices.isEmpty()); + QModelIndexList indexes; + bool allValid = true; + std::transform(m_dragIndices.constBegin(), m_dragIndices.constEnd(), + std::back_inserter(indexes), + [&allValid] (const QPersistentModelIndex &index) { + allValid &= index.isValid(); + return index; + }); + VERIFY(allValid); + + const QMimeData *data = model()->mimeData(indexes); + + auto availablePages = m_appModel->property("availablePages").value(); + VERIFY(availablePages); + + auto destModel = availablePages->property("pageListModel").value(); + VERIFY(destModel); + waitForStableState(); + + QModelIndex dropIndex = Zanshin::findIndex(destModel, pageName); + VERIFY(dropIndex.isValid()); + VERIFY(destModel->dropMimeData(data, Qt::MoveAction, -1, -1, dropIndex)); + waitForStableState(); + + return true; +} + +bool ZanshinContext::I_change_the_setting(const QString &key, qint64 id) +{ + KConfigGroup config(KSharedConfig::openConfig(), "General"); + config.writeEntry(key, id); + return true; +} + +bool ZanshinContext::I_change_the_default_data_source(const QString &sourceName) +{ + waitForStableState(); + auto sourceIndex = Zanshin::findIndex(model(), sourceName); + auto availableSources = m_appModel->property("availableSources").value(); + VERIFY(availableSources); + VERIFY(QMetaObject::invokeMethod(availableSources, "setDefaultItem", Q_ARG(QModelIndex, sourceIndex))); + waitForStableState(); + + return true; +} + +bool ZanshinContext::the_list_is(const TableData &data) +{ + auto roleNames = model()->roleNames(); + QVector usedRoles; + + for (const auto &roleName : data.roles) { + const int role = roleNames.key(roleName, -1); + VERIFY_OR_DUMP(role != -1 && !usedRoles.contains(role)); + usedRoles << role; + } + + QStandardItemModel inputModel; + for (const auto &row : data.rows) { + VERIFY_OR_DUMP(usedRoles.size() == row.size()); + + QStandardItem *item = new QStandardItem; + for (int i = 0; i < row.size(); ++i) { + const auto role = usedRoles.at(i); + const auto value = row.at(i); + item->setData(value, role); + } + inputModel.appendRow(item); + } + + QSortFilterProxyModel proxy; + + QAbstractItemModel *referenceModel; + if (!qobject_cast(sourceModel())) { + referenceModel = &proxy; + proxy.setSourceModel(&inputModel); + proxy.setSortRole(Qt::DisplayRole); + proxy.sort(0); + proxy.setObjectName(QStringLiteral("the_list_is_proxy")); + } else { + referenceModel = &inputModel; + } + + for (int row = 0; row < m_indices.size(); row++) { + QModelIndex expectedIndex = referenceModel->index(row, 0); + QModelIndex resultIndex = m_indices.at(row); + + foreach (const auto &role, usedRoles) { + COMPARE_OR_DUMP(Zanshin::indexString(resultIndex, role), + Zanshin::indexString(expectedIndex, role)); + } + } + COMPARE_OR_DUMP(m_indices.size(), referenceModel->rowCount()); + + return true; +} + +bool ZanshinContext::the_list_contains(const QString &itemName) +{ + for (int row = 0; row < m_indices.size(); row++) { + if (Zanshin::indexString(m_indices.at(row)) == itemName) + return true; + } + + VERIFY_OR_DUMP(false); + return false; +} + +bool ZanshinContext::the_list_does_not_contain(const QString &itemName) +{ + for (int row = 0; row < m_indices.size(); row++) { + VERIFY_OR_DUMP(Zanshin::indexString(m_indices.at(row)) != itemName); + } + + return true; +} + +bool ZanshinContext::the_task_corresponding_to_the_item_is_done() +{ + auto task = currentTask(); + VERIFY(task); + VERIFY(task->isDone()); + + return true; +} + +bool ZanshinContext::the_editor_shows_the_task_as_done() +{ + VERIFY(m_editor->property("done").toBool()); + return true; +} + +bool ZanshinContext::the_editor_shows_the_field(const QString &field, const QVariant &expectedValue) +{ + const QByteArray property = (field == QStringLiteral("text")) ? field.toUtf8() + : (field == QStringLiteral("title")) ? field.toUtf8() + : (field == QStringLiteral("start date")) ? "startDate" + : (field == QStringLiteral("due date")) ? "dueDate" + : QByteArray(); + + VERIFY(expectedValue.isValid()); + VERIFY(!property.isEmpty()); + + COMPARE(m_editor->property(property), expectedValue); + + return true; +} + +bool ZanshinContext::the_default_data_source_is(const QString &expectedName) +{ + waitForStableState(); + auto expectedIndex = Zanshin::findIndex(model(), expectedName); + VERIFY(expectedIndex.isValid()); + auto defaultRole = model()->roleNames().key("default", -1); + VERIFY(expectedIndex.data(defaultRole).toBool()); + + return true; +} + +bool ZanshinContext::the_setting_is(const QString &key, qint64 expectedId) +{ + KConfigGroup config(KSharedConfig::openConfig(), "General"); + const qint64 id = config.readEntry(key, -1); + COMPARE(id, expectedId); + + return true; +} diff --git a/tests/features/CMakeLists.txt b/tests/features/CMakeLists.txt --- a/tests/features/CMakeLists.txt +++ b/tests/features/CMakeLists.txt @@ -1,6 +1 @@ -find_package(Boost "1.40" COMPONENTS system regex unit_test_framework) -if(Boost_UNIT_TEST_FRAMEWORK_FOUND) - - add_subdirectory(zanshin) - -endif() +add_subdirectory(inbox) diff --git a/tests/features/README b/tests/features/README deleted file mode 100644 --- a/tests/features/README +++ /dev/null @@ -1,18 +0,0 @@ -These tests use the ruby "cucumber" application. - -You can install it with distro packages or with `gem install cucumber` as root. - -The tests are run as part of `ctest` (or `make test`), but when debugging -you can also run them separately, e.g. - -$ cd /zanshin/tests/features/zanshin - -Run all zanshin tests -- stop at first failure: -$ ./tests-features-zanshin - -Run all zanshin tests -- keep going in case of failure: -$ ./tests-features-zanshin -k - -Run only specific tests, useful when debugging or developing new tests: -$ ./tests-features-zanshin features/editing/adding-task.feature features/editing/editing-task.feature - diff --git a/tests/features/cuke-steps.cpp b/tests/features/cuke-steps.cpp deleted file mode 100644 --- a/tests/features/cuke-steps.cpp +++ /dev/null @@ -1,895 +0,0 @@ -/* This file is part of Zanshin - - Copyright 2014-2015 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 - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include "presentation/applicationmodel.h" -#include "presentation/errorhandler.h" -#include "presentation/querytreemodelbase.h" - -#include "akonadi/akonadiapplicationselectedattribute.h" -#include "akonadi/akonadicache.h" -#include "akonadi/akonadicachingstorage.h" -#include "akonadi/akonadimonitorimpl.h" -#include "akonadi/akonaditimestampattribute.h" - -#include "utils/dependencymanager.h" -#include "utils/jobhandler.h" - -#include "testlib/akonadifakedata.h" -#include "testlib/akonadifakedataxmlloader.h" -#include "testlib/monitorspy.h" -#include "testlib/testsafety.h" - -static int argc = 1; -static char *argv0 = "cuke-steps"; -static QApplication app(argc, &argv0); - -namespace CukeSteps -{ - void initializeAppDependencies(); -} - -namespace cucumber { - namespace internal { - template<> - inline QString fromString(const std::string& s) { - return QString::fromUtf8(s.data()); - } - } -} - -using namespace cucumber; - -class FakeErrorHandler : public Presentation::ErrorHandler -{ -public: - void doDisplayMessage(const QString &) - { - } -}; - -class ZanshinContext : public QObject -{ - Q_OBJECT -public: - explicit ZanshinContext(QObject *parent = nullptr) - : QObject(parent), - app(), - presentation(nullptr), - editor(nullptr), - proxyModel(new QSortFilterProxyModel(this)), - m_model(nullptr), - m_sourceModel(nullptr), - monitorSpy(nullptr) - { - qputenv("ZANSHIN_OVERRIDE_DATE", "2015-03-10"); - - static bool initializedDependencies = false; - - if (!initializedDependencies) { - CukeSteps::initializeAppDependencies(); - MonitorSpy::setExpirationDelay(200); - initializedDependencies = true; - } - - Akonadi::AttributeFactory::registerAttribute(); - Akonadi::AttributeFactory::registerAttribute(); - - const auto xmlFile = QString::fromLocal8Bit(qgetenv("ZANSHIN_USER_XMLDATA")); - if (xmlFile.isEmpty()) { - qDebug() << "FATAL ERROR! ZANSHIN_USER_XMLDATA WAS NOT PROVIDED\n\n"; - exit(1); - } - - auto searchCollection = Akonadi::Collection(1); - searchCollection.setParentCollection(Akonadi::Collection::root()); - searchCollection.setName(QStringLiteral("Search")); - m_data.createCollection(searchCollection); - - auto loader = Testlib::AkonadiFakeDataXmlLoader(&m_data); - loader.load(xmlFile); - - // Swap regular dependencies for the fake data ones - auto &deps = Utils::DependencyManager::globalInstance(); - deps.add( - [this] (Utils::DependencyManager *) { - return m_data.createMonitor(); - } - ); - deps.add( - [this] (Utils::DependencyManager *deps) { - return new Akonadi::CachingStorage(deps->create(), - Akonadi::StorageInterface::Ptr(m_data.createStorage())); - } - ); - - using namespace Presentation; - proxyModel->setDynamicSortFilter(true); - - auto appModel = ApplicationModel::Ptr::create(); - - appModel->setErrorHandler(&m_errorHandler); - - app = appModel; - - auto monitor = Utils::DependencyManager::globalInstance().create(); - monitorSpy = new MonitorSpy(monitor.data(), this); - } - - ~ZanshinContext() - { - } - - // Note that setModel might invalidate the 'index' member variable, due to proxyModel->setSourceModel. - void setModel(QAbstractItemModel *model) - { - if (m_sourceModel == model) - return; - m_sourceModel = model; - if (!qobject_cast(model)) { - proxyModel->setObjectName(QStringLiteral("m_proxyModel_in_ZanshinContext")); - proxyModel->setSourceModel(model); - proxyModel->setSortRole(Qt::DisplayRole); - proxyModel->sort(0); - m_model = proxyModel; - } else { - m_model = model; - } - } - - QAbstractItemModel *sourceModel() - { - return m_sourceModel; - } - - QAbstractItemModel *model() - { - return m_model; - } - - Domain::Task::Ptr currentTask() const - { - return index.data(Presentation::QueryTreeModelBase::ObjectRole) - .value(); - } - - void waitForEmptyJobQueue() - { - while (Utils::JobHandler::jobCount() != 0) { - QTest::qWait(20); - } - } - - void waitForStableState() - { - waitForEmptyJobQueue(); - monitorSpy->waitForStableState(); - } - - QObjectPtr app; - - QList indices; - QPersistentModelIndex index; - QObject *presentation; - QObject *editor; - QList dragIndices; - -private: - Testlib::AkonadiFakeData m_data; - - QSortFilterProxyModel *proxyModel; - QAbstractItemModel *m_model; - QAbstractItemModel *m_sourceModel; - MonitorSpy *monitorSpy; - FakeErrorHandler m_errorHandler; -}; - -namespace Zanshin { - -QString indexString(const QModelIndex &index, int role = Qt::DisplayRole) -{ - if (role != Qt::DisplayRole) - return index.data(role).toString(); - - QString data = index.data(role).toString(); - - if (index.parent().isValid()) - return indexString(index.parent(), role) + " / " + data; - else - return data; -} - -QModelIndex findIndex(QAbstractItemModel *model, - const QString &string, - int role = Qt::DisplayRole, - const QModelIndex &root = QModelIndex()) -{ - for (int row = 0; row < model->rowCount(root); row++) { - const QModelIndex index = model->index(row, 0, root); - if (indexString(index, role) == string) - return index; - - if (model->rowCount(index) > 0) { - const QModelIndex found = findIndex(model, string, role, index); - if (found.isValid()) - return found; - } - } - - return QModelIndex(); -} - -static void collectIndicesImpl(ZanshinContext *context, const QModelIndex &root = QModelIndex()) -{ - QAbstractItemModel *model = context->model(); - for (int row = 0; row < model->rowCount(root); row++) { - const QModelIndex index = model->index(row, 0, root); - context->indices << index; - if (model->rowCount(index) > 0) - collectIndicesImpl(context, index); - } -} - -static void collectIndices(ZanshinContext *context) -{ - context->indices.clear(); - collectIndicesImpl(context); -} - -void dumpIndices(const QList &indices) -{ - qDebug() << "Dumping list of size:" << indices.size(); - for (int row = 0; row < indices.size(); row++) { - qDebug() << row << indexString(indices.at(row)); - } -} - -inline bool verify(bool statement, const char *str, - const char *file, int line) -{ - if (statement) - return true; - - qDebug() << "Statement" << str << "returned FALSE"; - qDebug() << "Loc:" << file << line; - return false; -} - -template -inline bool compare(T const &t1, T const &t2, - const char *actual, const char *expected, - const char *file, int line) -{ - if (t1 == t2) - return true; - - qDebug() << "Compared values are not the same"; - qDebug() << "Actual (" << actual << ") :" << QTest::toString(t1); - qDebug() << "Expected (" << expected << ") :" << QTest::toString(t2); - qDebug() << "Loc:" << file << line; - return false; -} - -} // namespace Zanshin - -#define COMPARE(actual, expected) \ -do {\ - if (!Zanshin::compare(actual, expected, #actual, #expected, __FILE__, __LINE__))\ - BOOST_REQUIRE(false);\ -} while (0) - -// Note: you should make sure that context->indices is filled in before calling this, -// e.g. calling Zanshin::collectIndices(context.get()) if not already done. -#define COMPARE_OR_DUMP(actual, expected) \ -do {\ - if (!Zanshin::compare(actual, expected, #actual, #expected, __FILE__, __LINE__)) {\ - Zanshin::dumpIndices(context->indices); \ - BOOST_REQUIRE(false);\ - }\ -} while (0) - -#define VERIFY(statement) \ -do {\ - if (!Zanshin::verify((statement), #statement, __FILE__, __LINE__))\ - BOOST_REQUIRE(false);\ -} while (0) - -// Note: you should make sure that context->indices is filled in before calling this, -// e.g. calling Zanshin::collectIndices(context.get()) if not already done. -#define VERIFY_OR_DUMP(statement) \ -do {\ - if (!Zanshin::verify((statement), #statement, __FILE__, __LINE__)) {\ - Zanshin::dumpIndices(context->indices); \ - BOOST_REQUIRE(false);\ - }\ -} while (0) - -#define VERIFY_OR_DO(statement, whatToDo) \ -do {\ - if (!Zanshin::verify((statement), #statement, __FILE__, __LINE__)) {\ - whatToDo; \ - BOOST_REQUIRE(false);\ - }\ -} while (0) - - -GIVEN("^I display the available data sources$") { - ScenarioScope context; - auto availableSources = context->app->property("availableSources").value(); - VERIFY(availableSources); - - auto sourceListModel = availableSources->property("sourceListModel").value(); - VERIFY(sourceListModel); - - context->presentation = availableSources; - context->setModel(sourceListModel); -} - -GIVEN("^I display the available pages$") { - ScenarioScope context; - context->presentation = context->app->property("availablePages").value(); - context->setModel(context->presentation->property("pageListModel").value()); -} - -GIVEN("^I display the \"(.*)\" page$") { - REGEX_PARAM(QString, pageName); - - ScenarioScope context; - auto availablePages = context->app->property("availablePages").value(); - VERIFY(availablePages); - - auto pageListModel = availablePages->property("pageListModel").value(); - VERIFY(pageListModel); - context->waitForEmptyJobQueue(); - - QModelIndex pageIndex = Zanshin::findIndex(pageListModel, pageName); - VERIFY(pageIndex.isValid()); - - QObject *page = nullptr; - QMetaObject::invokeMethod(availablePages, "createPageForIndex", - Q_RETURN_ARG(QObject*, page), - Q_ARG(QModelIndex, pageIndex)); - VERIFY(page); - - VERIFY(context->app->setProperty("currentPage", QVariant::fromValue(page))); - context->presentation = context->app->property("currentPage").value(); -} - -GIVEN("^there is an item named \"(.+)\" in the central list$") { - REGEX_PARAM(QString, itemName); - - ScenarioScope context; - - auto model = context->presentation->property("centralListModel").value(); - context->setModel(model); - context->waitForEmptyJobQueue(); - - Zanshin::collectIndices(context.get()); - context->index = Zanshin::findIndex(context->model(), itemName); - VERIFY_OR_DUMP(context->index.isValid()); -} - -GIVEN("^there is an item named \"(.+)\" in the available data sources$") { - REGEX_PARAM(QString, itemName); - - ScenarioScope context; - - auto availableSources = context->app->property("availableSources").value(); - VERIFY(availableSources); - auto model = availableSources->property("sourceListModel").value(); - VERIFY(model); - context->waitForEmptyJobQueue(); - context->setModel(model); - - Zanshin::collectIndices(context.get()); - context->index = Zanshin::findIndex(context->model(), itemName); - VERIFY_OR_DUMP(context->index.isValid()); -} - -GIVEN("^the central list contains items named:") { - TABLE_PARAM(tableParam); - - ScenarioScope context; - context->dragIndices.clear(); - - auto model = context->presentation->property("centralListModel").value(); - context->waitForEmptyJobQueue(); - context->setModel(model); - - for (const auto &row : tableParam.hashes()) { - for (const auto &it : row) { - const QString itemName = QString::fromUtf8(it.second.data()); - QModelIndex index = Zanshin::findIndex(context->model(), itemName); - VERIFY_OR_DO(index.isValid(), Zanshin::dumpIndices(context->dragIndices)); - context->dragIndices << index; - } - } -} - -WHEN("^I look at the central list$") { - ScenarioScope context; - - auto model = context->presentation->property("centralListModel").value(); - context->setModel(model); - context->waitForStableState(); -} - -WHEN("^I check the item$") { - ScenarioScope context; - VERIFY(context->model()->setData(context->index, Qt::Checked, Qt::CheckStateRole)); - context->waitForStableState(); -} - -WHEN("^I uncheck the item$") { - ScenarioScope context; - VERIFY(context->model()->setData(context->index, Qt::Unchecked, Qt::CheckStateRole)); - context->waitForStableState(); -} - -WHEN("^I remove the item$") { - ScenarioScope context; - VERIFY(QMetaObject::invokeMethod(context->presentation, "removeItem", Q_ARG(QModelIndex, context->index))); - context->waitForStableState(); -} - -WHEN("^I promote the item$") { - ScenarioScope context; - VERIFY(QMetaObject::invokeMethod(context->presentation, "promoteItem", Q_ARG(QModelIndex, context->index))); - context->waitForStableState(); -} - -WHEN("^I add a project named \"(.*)\" in the source named \"(.*)\"$") { - REGEX_PARAM(QString, projectName); - REGEX_PARAM(QString, sourceName); - - ScenarioScope context; - - auto availableSources = context->app->property("availableSources").value(); - VERIFY(availableSources); - auto sourceList = availableSources->property("sourceListModel").value(); - VERIFY(sourceList); - context->waitForStableState(); - QModelIndex index = Zanshin::findIndex(sourceList, sourceName); - VERIFY(index.isValid()); - auto source = index.data(Presentation::QueryTreeModelBase::ObjectRole) - .value(); - VERIFY(source); - - VERIFY(QMetaObject::invokeMethod(context->presentation, "addProject", - Q_ARG(QString, projectName), - Q_ARG(Domain::DataSource::Ptr, source))); - context->waitForStableState(); -} - -WHEN("^I rename the page named \"(.*)\" under \"(.*)\" to \"(.*)\"$") { - REGEX_PARAM(QString, oldName); - REGEX_PARAM(QString, path); - REGEX_PARAM(QString, newName); - - const QString pageNodeName = path + " / "; - - VERIFY(!pageNodeName.isEmpty()); - - ScenarioScope context; - auto availablePages = context->app->property("availablePages").value(); - VERIFY(availablePages); - - auto pageListModel = availablePages->property("pageListModel").value(); - VERIFY(pageListModel); - context->waitForStableState(); - - QModelIndex pageIndex = Zanshin::findIndex(pageListModel, pageNodeName + oldName); - VERIFY(pageIndex.isValid()); - - pageListModel->setData(pageIndex, newName); - context->waitForStableState(); -} - -WHEN("^I remove the page named \"(.*)\" under \"(.*)\"$") { - REGEX_PARAM(QString, name); - REGEX_PARAM(QString, path); - - const QString pageNodeName = path + " / "; - - VERIFY(!pageNodeName.isEmpty()); - - ScenarioScope context; - auto availablePages = context->app->property("availablePages").value(); - VERIFY(availablePages); - - auto pageListModel = availablePages->property("pageListModel").value(); - VERIFY(pageListModel); - context->waitForStableState(); - - QModelIndex pageIndex = Zanshin::findIndex(pageListModel, pageNodeName + name); - VERIFY(pageIndex.isValid()); - - VERIFY(QMetaObject::invokeMethod(availablePages, "removeItem", - Q_ARG(QModelIndex, pageIndex))); - context->waitForStableState(); -} - -WHEN("^I add a \"(.*)\" named \"(.+)\"$") { - REGEX_PARAM(QString, objectType); - REGEX_PARAM(QString, objectName); - - QByteArray actionName = (objectType == QStringLiteral("context")) ? "addContext" - : (objectType == QStringLiteral("note")) ? "addItem" - : (objectType == QStringLiteral("task")) ? "addItem" - : (objectType == QStringLiteral("tag")) ? "addTag" - : QByteArray(); - - VERIFY(!actionName.isEmpty()); - - ScenarioScope context; - context->waitForStableState(); - - VERIFY(QMetaObject::invokeMethod(context->presentation, - actionName.data(), - Q_ARG(QString, objectName))); - context->waitForStableState(); -} - -WHEN("^I add a child named \"(.+)\" under the task named \"(.+)\"$") { - REGEX_PARAM(QString, childName); - REGEX_PARAM(QString, parentName); - - ScenarioScope context; - context->waitForStableState(); - - auto parentIndex = QModelIndex(); - for (int row = 0; row < context->indices.size(); row++) { - auto index = context->indices.at(row); - if (Zanshin::indexString(index) == parentName) { - parentIndex = index; - break; - } - } - - VERIFY_OR_DUMP(parentIndex.isValid()); - - VERIFY(QMetaObject::invokeMethod(context->presentation, - "addItem", - Q_ARG(QString, childName), - Q_ARG(QModelIndex, parentIndex))); - context->waitForStableState(); -} - -WHEN("^I list the items$") { - ScenarioScope context; - context->waitForStableState(); - Zanshin::collectIndices(context.get()); - context->waitForStableState(); -} - -WHEN("^I open the item in the editor$") { - ScenarioScope context; - auto task = context->currentTask(); - VERIFY(task); - context->editor = context->app->property("editor").value(); - VERIFY(context->editor); - VERIFY(context->editor->setProperty("task", QVariant::fromValue(task))); -} - -WHEN("^I mark it done in the editor$") { - ScenarioScope context; - VERIFY(context->editor->setProperty("done", true)); -} - -WHEN("^I change the editor (.*) to \"(.*)\"$") { - REGEX_PARAM(QString, field); - REGEX_PARAM(QString, string); - - const QVariant value = (field == QStringLiteral("text")) ? string - : (field == QStringLiteral("title")) ? string - : (field == QStringLiteral("start date")) ? QDateTime::fromString(string, Qt::ISODate) - : (field == QStringLiteral("due date")) ? QDateTime::fromString(string, Qt::ISODate) - : QVariant(); - - const QByteArray property = (field == QStringLiteral("text")) ? field.toUtf8() - : (field == QStringLiteral("title")) ? field.toUtf8() - : (field == QStringLiteral("start date")) ? "startDate" - : (field == QStringLiteral("due date")) ? "dueDate" - : QByteArray(); - - VERIFY(value.isValid()); - VERIFY(!property.isEmpty()); - - ScenarioScope context; - VERIFY(context->editor->setProperty("editingInProgress", true)); - VERIFY(context->editor->setProperty(property, value)); -} - -WHEN("^I rename the item to \"(.+)\"$") { - REGEX_PARAM(QString, title); - ScenarioScope context; - VERIFY(context->editor->setProperty("editingInProgress", false)); - VERIFY(context->model()->setData(context->index, title, Qt::EditRole)); - context->waitForStableState(); -} - -WHEN("^I open the item in the editor again$") { - ScenarioScope context; - auto task = context->currentTask(); - VERIFY(task); - VERIFY(context->editor->setProperty("task", QVariant::fromValue(Domain::Task::Ptr()))); - VERIFY(context->editor->setProperty("task", QVariant::fromValue(task))); - context->waitForStableState(); -} - -WHEN("^I drop the item on \"(.*)\" in the central list") { - REGEX_PARAM(QString, itemName); - - ScenarioScope context; - VERIFY(context->index.isValid()); - const QMimeData *data = context->model()->mimeData(QModelIndexList() << context->index); - - QAbstractItemModel *destModel = context->model(); - QModelIndex dropIndex = Zanshin::findIndex(destModel, itemName); - VERIFY(dropIndex.isValid()); - VERIFY(destModel->dropMimeData(data, Qt::MoveAction, -1, -1, dropIndex)); - context->waitForStableState(); -} - -WHEN("^I drop the item on the blank area of the central list") { - - ScenarioScope context; - VERIFY(context->index.isValid()); - const QMimeData *data = context->model()->mimeData(QModelIndexList() << context->index); - - QAbstractItemModel *destModel = context->model(); - VERIFY(destModel->dropMimeData(data, Qt::MoveAction, -1, -1, QModelIndex())); - context->waitForStableState(); -} - -WHEN("^I drop items on \"(.*)\" in the central list") { - REGEX_PARAM(QString, itemName); - - ScenarioScope context; - VERIFY(!context->dragIndices.isEmpty()); - QModelIndexList indexes; - std::transform(context->dragIndices.constBegin(), context->dragIndices.constEnd(), - std::back_inserter(indexes), - [] (const QPersistentModelIndex &index) { - VERIFY(index.isValid()); - return index; - }); - - const QMimeData *data = context->model()->mimeData(indexes); - - QAbstractItemModel *destModel = context->model(); - QModelIndex dropIndex = Zanshin::findIndex(destModel, itemName); - VERIFY(dropIndex.isValid()); - VERIFY(destModel->dropMimeData(data, Qt::MoveAction, -1, -1, dropIndex)); - context->waitForStableState(); -} - -WHEN("^I drop the item on \"(.*)\" in the page list") { - REGEX_PARAM(QString, itemName); - - ScenarioScope context; - VERIFY(context->index.isValid()); - const QMimeData *data = context->model()->mimeData(QModelIndexList() << context->index); - - auto availablePages = context->app->property("availablePages").value(); - VERIFY(availablePages); - - auto destModel = availablePages->property("pageListModel").value(); - VERIFY(destModel); - context->waitForStableState(); - - QModelIndex dropIndex = Zanshin::findIndex(destModel, itemName); - VERIFY(dropIndex.isValid()); - VERIFY(destModel->dropMimeData(data, Qt::MoveAction, -1, -1, dropIndex)); - context->waitForStableState(); -} - -WHEN("^I drop items on \"(.*)\" in the page list") { - REGEX_PARAM(QString, itemName); - - ScenarioScope context; - VERIFY(!context->dragIndices.isEmpty()); - QModelIndexList indexes; - std::transform(context->dragIndices.constBegin(), context->dragIndices.constEnd(), - std::back_inserter(indexes), - [] (const QPersistentModelIndex &index) { - VERIFY(index.isValid()); - return index; - }); - - const QMimeData *data = context->model()->mimeData(indexes); - - auto availablePages = context->app->property("availablePages").value(); - VERIFY(availablePages); - - auto destModel = availablePages->property("pageListModel").value(); - VERIFY(destModel); - context->waitForStableState(); - - QModelIndex dropIndex = Zanshin::findIndex(destModel, itemName); - VERIFY(dropIndex.isValid()); - VERIFY(destModel->dropMimeData(data, Qt::MoveAction, -1, -1, dropIndex)); - context->waitForStableState(); -} - -WHEN("^the setting key (\\S+) changes to (\\d+)$") { - REGEX_PARAM(QString, keyName); - REGEX_PARAM(qint64, id); - - ScenarioScope context; - KConfigGroup config(KSharedConfig::openConfig(), "General"); - config.writeEntry(keyName, id); -} - -WHEN("^the user changes the default data source to \"(.*)\"$") { - REGEX_PARAM(QString, sourceName); - - ScenarioScope context; - context->waitForStableState(); - auto sourceIndex = Zanshin::findIndex(context->model(), sourceName); - auto availableSources = context->app->property("availableSources").value(); - VERIFY(availableSources); - VERIFY(QMetaObject::invokeMethod(availableSources, "setDefaultItem", Q_ARG(QModelIndex, sourceIndex))); - context->waitForStableState(); -} - - -THEN("^the list is") { - TABLE_PARAM(tableParam); - - ScenarioScope context; - auto roleNames = context->model()->roleNames(); - QSet usedRoles; - - QStandardItemModel inputModel; - for (const auto &row : tableParam.hashes()) { - QStandardItem *item = new QStandardItem; - for (const auto &it : row) { - const QByteArray roleName = it.first.data(); - const QString value = QString::fromUtf8(it.second.data()); - const int role = roleNames.key(roleName, -1); - VERIFY_OR_DUMP(role != -1); - item->setData(value, role); - usedRoles.insert(role); - } - inputModel.appendRow(item); - } - - QSortFilterProxyModel proxy; - - QAbstractItemModel *referenceModel; - if (!qobject_cast(context->sourceModel())) { - referenceModel = &proxy; - proxy.setSourceModel(&inputModel); - proxy.setSortRole(Qt::DisplayRole); - proxy.sort(0); - proxy.setObjectName(QStringLiteral("the_list_is_proxy")); - } else { - referenceModel = &inputModel; - } - - for (int row = 0; row < context->indices.size(); row++) { - QModelIndex expectedIndex = referenceModel->index(row, 0); - QModelIndex resultIndex = context->indices.at(row); - - foreach (const auto &role, usedRoles) { - COMPARE_OR_DUMP(Zanshin::indexString(resultIndex, role), - Zanshin::indexString(expectedIndex, role)); - } - } - COMPARE_OR_DUMP(context->indices.size(), referenceModel->rowCount()); -} - -THEN("^the list contains \"(.+)\"$") { - REGEX_PARAM(QString, itemName); - - ScenarioScope context; - for (int row = 0; row < context->indices.size(); row++) { - if (Zanshin::indexString(context->indices.at(row)) == itemName) - return; - } - - VERIFY_OR_DUMP(false); -} - -THEN("^the list does not contain \"(.+)\"$") { - REGEX_PARAM(QString, itemName); - - ScenarioScope context; - for (int row = 0; row < context->indices.size(); row++) { - VERIFY_OR_DUMP(Zanshin::indexString(context->indices.at(row)) != itemName); - } -} - -THEN("^the task corresponding to the item is done$") { - ScenarioScope context; - auto task = context->currentTask(); - VERIFY(task); - VERIFY(task->isDone()); -} - -THEN("^the editor shows the task as done$") { - ScenarioScope context; - VERIFY(context->editor->property("done").toBool()); -} - -THEN("^the editor shows \"(.*)\" as (.*)$") { - REGEX_PARAM(QString, string); - REGEX_PARAM(QString, field); - - const QVariant value = (field == QStringLiteral("text")) ? string - : (field == QStringLiteral("title")) ? string - : (field == QStringLiteral("start date")) ? QDateTime::fromString(string, Qt::ISODate) - : (field == QStringLiteral("due date")) ? QDateTime::fromString(string, Qt::ISODate) - : QVariant(); - - const QByteArray property = (field == QStringLiteral("text")) ? field.toUtf8() - : (field == QStringLiteral("title")) ? field.toUtf8() - : (field == QStringLiteral("start date")) ? "startDate" - : (field == QStringLiteral("due date")) ? "dueDate" - : QByteArray(); - - VERIFY(value.isValid()); - VERIFY(!property.isEmpty()); - - ScenarioScope context; - COMPARE(context->editor->property(property), value); -} - -THEN("^the default data source is \"(.*)\"$") { - REGEX_PARAM(QString, expectedName); - - ScenarioScope context; - context->waitForStableState(); - auto expectedIndex = Zanshin::findIndex(context->model(), expectedName); - VERIFY(expectedIndex.isValid()); - auto defaultRole = context->model()->roleNames().key("default", -1); - VERIFY(expectedIndex.data(defaultRole).toBool()); -} - -THEN("^the setting key (\\S+) is (\\d+)$") { - REGEX_PARAM(QString, keyName); - REGEX_PARAM(qint64, expectedId); - - KConfigGroup config(KSharedConfig::openConfig(), "General"); - const qint64 id = config.readEntry(keyName, -1); - COMPARE(id, expectedId); -} - -#include "cuke-steps.moc" diff --git a/tests/features/features-run.cpp b/tests/features/features-run.cpp deleted file mode 100644 --- a/tests/features/features-run.cpp +++ /dev/null @@ -1,120 +0,0 @@ -/* This file is part of Zanshin - - Copyright 2016 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 -#include -#include -#include -#include -#include -#include -#include - -class ProcessKiller -{ -public: - explicit ProcessKiller(QProcess *process) - : m_process(process) - { - } - - ~ProcessKiller() - { - m_process->kill(); - m_process->waitForFinished(); - } - -private: - ProcessKiller(const ProcessKiller &other); - ProcessKiller &operator=(const ProcessKiller &other); - - QProcess *m_process; -}; - -bool waitForCukeSteps(qint64 timeout) -{ - QElapsedTimer timer; - timer.start(); - QTcpSocket socket; - - socket.connectToHost(QHostAddress::LocalHost, 3902); - while (!socket.waitForConnected() && !timer.hasExpired(timeout)) { - socket.connectToHost(QHostAddress::LocalHost, 3902); - } - - return socket.state() == QTcpSocket::ConnectedState; -} - -int main(int argc, char **argv) -{ - qputenv("ZANSHIN_USER_XMLDATA", USER_XMLDATA); - - QCoreApplication app(argc, argv); - - QCommandLineParser parser; - QCommandLineOption includeWIP("wip", "Run all scenarios including WIP scenarios (those with \"@wip\" above Scenario, on a separate line)"); - parser.addOption(includeWIP); - QCommandLineOption keepGoing(QStringList{"k", "keep-going"}, "Keep going after errors"); - parser.addOption(keepGoing); - parser.addPositionalArgument("filesOrDirs", "Files or directories for selecting a subset of the features to test. Example: features/editing", "[ [FILE|DIR][:LINE[:LINE]*] ]*"); - parser.addHelpOption(); - - parser.process(app); - - QDir::setCurrent(QStringLiteral(FEATURES_DIR)); - - QProcess cukeSteps; - cukeSteps.setProcessChannelMode(QProcess::ForwardedChannels); - cukeSteps.start(QStringLiteral(CUKE_STEPS), QStringList()); - - if (!cukeSteps.waitForStarted()) { - qWarning() << "Couldn't start the cuke steps server, exiting..."; - return 1; - } - - ProcessKiller cukeStepsKiller(&cukeSteps); - - if (!waitForCukeSteps(10000)) { - qWarning() << "The cuke steps server didn't show up as expected, exiting..."; - return 1; - } - - const QStringList appArgs = parser.positionalArguments(); - - QStringList args; - if (!parser.isSet(includeWIP)) - args += QStringList{"--tags", "~@wip"}; - if (!parser.isSet(keepGoing)) - args += QStringLiteral("--fail-fast"); - if (!appArgs.isEmpty()) - args += appArgs; // files or dirs - - const QString cucumber = QStringLiteral("cucumber"); - if (QStandardPaths::findExecutable(cucumber).isEmpty()) { - qWarning() << "cucumber not found"; - return 1; - } - - return QProcess::execute(cucumber, args); -} diff --git a/tests/features/inbox/CMakeLists.txt b/tests/features/inbox/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/tests/features/inbox/CMakeLists.txt @@ -0,0 +1,4 @@ +zanshin_feature_tests( + inboxdisplayfeature + inboxdraganddropfeature +) diff --git a/tests/features/inbox/inboxdisplayfeature.cpp b/tests/features/inbox/inboxdisplayfeature.cpp new file mode 100644 --- /dev/null +++ b/tests/features/inbox/inboxdisplayfeature.cpp @@ -0,0 +1,61 @@ +/* This file is part of Zanshin + + Copyright 2019 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 + +using namespace Testlib; + +// Feature: Inbox content +// As someone collecting tasks +// I can display the Inbox +// In order to see the tasks which need to be organized (e.g. any task not associated to any project or context) +class InboxDisplayFeature : public QObject +{ + Q_OBJECT +private slots: + void Unorganized_tasks_appear_in_the_inbox() + { + ZANSHIN_CONTEXT; + Given(I_display_the_page("Inbox")); + And(I_look_at_the_central_list()); + When(I_list_the_items()); + Then(the_list_is({ + { "display" }, + { + { "Errands" }, + { "\"Capital in the Twenty-First Century\" by Thomas Piketty" }, + { "\"The Pragmatic Programmer\" by Hunt and Thomas" }, + { "Buy cheese" }, + { "Buy kiwis" }, + { "Buy apples" }, + { "Buy pears" }, + { "Buy rutabagas" } + } + })); + } +}; + +ZANSHIN_TEST_MAIN(InboxDisplayFeature) + +#include "inboxdisplayfeature.moc" diff --git a/tests/features/inbox/inboxdraganddropfeature.cpp b/tests/features/inbox/inboxdraganddropfeature.cpp new file mode 100644 --- /dev/null +++ b/tests/features/inbox/inboxdraganddropfeature.cpp @@ -0,0 +1,194 @@ +/* This file is part of Zanshin + + Copyright 2019 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 + +using namespace Testlib; + +// Feature: Inbox task association +// As someone collecting tasks +// I can associate a task to another one +// In order to deal with complex tasks requiring several steps +class InboxDragAndDropFeature : public QObject +{ + Q_OBJECT +private slots: + void Dropping_a_task_on_another_one_makes_it_a_child() + { + ZANSHIN_CONTEXT; + Given(I_display_the_page("Inbox")); + And(there_is_an_item_in_the_central_list("Buy apples")); + When(I_drop_the_item_on_the_central_list("Errands")); + And(I_list_the_items()); + Then(the_list_is({ + { "display" }, + { + { "Errands" }, + { "Errands / Buy apples" }, + { "\"Capital in the Twenty-First Century\" by Thomas Piketty" }, + { "\"The Pragmatic Programmer\" by Hunt and Thomas" }, + { "Buy cheese" }, + { "Buy kiwis" }, + { "Buy pears" }, + { "Buy rutabagas" } + } + })); + } + + void Dropping_a_child_task_on_the_inbox_makes_it_top_level() + { + ZANSHIN_CONTEXT; + Given(I_display_the_page("Inbox")); + And(there_is_an_item_in_the_central_list("Buy apples")); + And(I_drop_the_item_on_the_central_list("Errands")); + And(there_is_an_item_in_the_central_list("Errands / Buy apples")); + When(I_drop_the_item_on_the_page_list("Inbox")); + And(I_list_the_items()); + Then(the_list_is({ + { "display" }, + { + { "Errands" }, + { "Buy apples" }, + { "\"Capital in the Twenty-First Century\" by Thomas Piketty" }, + { "\"The Pragmatic Programmer\" by Hunt and Thomas" }, + { "Buy cheese" }, + { "Buy kiwis" }, + { "Buy pears" }, + { "Buy rutabagas" } + } + })); + } + + void Dropping_two_tasks_on_another_one_makes_them_children() + { + ZANSHIN_CONTEXT; + Given(I_display_the_page("Inbox")); + And(the_central_list_contains_items_named({"Buy apples", "Buy pears"})); + When(I_drop_items_on_the_central_list("Errands")); + And(I_list_the_items()); + Then(the_list_is({ + { "display" }, + { + { "Errands" }, + { "Errands / Buy apples" }, + { "Errands / Buy pears" }, + { "\"Capital in the Twenty-First Century\" by Thomas Piketty" }, + { "\"The Pragmatic Programmer\" by Hunt and Thomas" }, + { "Buy cheese" }, + { "Buy kiwis" }, + { "Buy rutabagas" } + } + })); + } + + void Dropping_two_child_tasks_on_the_inbox_makes_them_top_level() + { + ZANSHIN_CONTEXT; + Given(I_display_the_page("Inbox")); + And(the_central_list_contains_items_named({"Buy apples", "Buy pears"})); + And(I_drop_items_on_the_central_list("Errands")); + And(the_central_list_contains_items_named({"Errands / Buy apples", "Errands / Buy pears"})); + When(I_drop_items_on_the_page_list("Inbox")); + And(I_list_the_items()); + Then(the_list_is({ + { "display" }, + { + { "Errands" }, + { "Buy apples" }, + { "\"Capital in the Twenty-First Century\" by Thomas Piketty" }, + { "\"The Pragmatic Programmer\" by Hunt and Thomas" }, + { "Buy cheese" }, + { "Buy kiwis" }, + { "Buy pears" }, + { "Buy rutabagas" } + } + })); + } + + void Dropping_a_task_on_the_inbox_removes_it_from_its_associated_project() + { + ZANSHIN_CONTEXT; + Given(I_display_the_page("Projects / TestData » Calendar1 / Prepare talk about TDD")); + And(there_is_an_item_in_the_central_list("Create Sozi SVG")); + When(I_drop_the_item_on_the_page_list("Inbox")); + And(I_display_the_page("Inbox")); + And(I_look_at_the_central_list()); + And(I_list_the_items()); + Then(the_list_is({ + { "display" }, + { + { "Errands" }, + { "Buy apples" }, + { "\"Capital in the Twenty-First Century\" by Thomas Piketty" }, + { "\"The Pragmatic Programmer\" by Hunt and Thomas" }, + { "Buy cheese" }, + { "Buy kiwis" }, + { "Buy pears" }, + { "Buy rutabagas" }, + { "Create Sozi SVG" } + } + })); + } + + void Deparenting_a_task_by_dropping_on_the_central_list_blank_area() + { + ZANSHIN_CONTEXT; + Given(I_display_the_page("Inbox")); + And(I_look_at_the_central_list()); + And(there_is_an_item_in_the_central_list("Buy apples")); + And(I_drop_the_item_on_the_central_list("Errands")); + And(I_look_at_the_central_list()); + And(there_is_an_item_in_the_central_list("Errands / Buy apples")); + When(I_drop_the_item_on_the_blank_area_of_the_central_list()); + And(I_list_the_items()); + Then(the_list_is({ + { "display" }, + { + { "Errands" }, + { "Buy apples" }, + { "\"Capital in the Twenty-First Century\" by Thomas Piketty" }, + { "\"The Pragmatic Programmer\" by Hunt and Thomas" }, + { "Buy cheese" }, + { "Buy kiwis" }, + { "Buy pears" }, + { "Buy rutabagas" } + } + })); + } + + void Dropping_a_task_on_the_inbox_removes_it_from_all_its_contexts() + { + ZANSHIN_CONTEXT; + Given(I_display_the_page("Contexts / Errands")); + And(there_is_an_item_in_the_central_list("Buy kiwis")); + When(I_drop_the_item_on_the_page_list("Inbox")); + And(I_display_the_page("Contexts / Errands")); + And(I_look_at_the_central_list()); + Then(the_list_does_not_contain("Buy kiwis")); + } +}; + +ZANSHIN_TEST_MAIN(InboxDragAndDropFeature) + +#include "inboxdraganddropfeature.moc" diff --git a/tests/features/zanshin/CMakeLists.txt b/tests/features/zanshin/CMakeLists.txt deleted file mode 100644 --- a/tests/features/zanshin/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -add_subdirectory(features) - -add_executable(tests-features-zanshin ../features-run.cpp) -add_test(NAME tests-features-zanshin COMMAND tests-features-zanshin) -add_definitions(-DUSER_XMLDATA="${CMAKE_CURRENT_SOURCE_DIR}/../testenv/data/testdata.xml" - -DCUKE_STEPS="${CMAKE_CURRENT_BINARY_DIR}/features/step_definitions/zanshin-cuke-steps" - -DFEATURES_DIR="${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(tests-features-zanshin - Qt5::Core - Qt5::Network -) diff --git a/tests/features/zanshin/features/CMakeLists.txt b/tests/features/zanshin/features/CMakeLists.txt deleted file mode 100644 --- a/tests/features/zanshin/features/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -add_subdirectory(step_definitions) diff --git a/tests/features/zanshin/features/inbox/inbox-display.feature b/tests/features/zanshin/features/inbox/inbox-display.feature deleted file mode 100644 --- a/tests/features/zanshin/features/inbox/inbox-display.feature +++ /dev/null @@ -1,19 +0,0 @@ -Feature: Inbox content - As someone collecting tasks - I can display the Inbox - In order to see the tasks which need to be organized (e.g. any task not associated to any project or context) - - Scenario: Unorganized tasks appear in the inbox - Given I display the "Inbox" page - And I look at the central list - When I list the items - Then the list is: - | display | - | Errands | - | "Capital in the Twenty-First Century" by Thomas Piketty | - | "The Pragmatic Programmer" by Hunt and Thomas | - | Buy cheese | - | Buy kiwis | - | Buy apples | - | Buy pears | - | Buy rutabagas | diff --git a/tests/features/zanshin/features/inbox/inbox-drag-and-drop.feature b/tests/features/zanshin/features/inbox/inbox-drag-and-drop.feature deleted file mode 100644 --- a/tests/features/zanshin/features/inbox/inbox-drag-and-drop.feature +++ /dev/null @@ -1,129 +0,0 @@ -Feature: Inbox task association - As someone collecting tasks - I can associate a task to another one - In order to deal with complex tasks requiring several steps - - Scenario: Dropping a task on another one makes it a child - Given I display the "Inbox" page - And there is an item named "Buy apples" in the central list - When I drop the item on "Errands" in the central list - And I list the items - Then the list is: - | display | - | Errands | - | Errands / Buy apples | - | "Capital in the Twenty-First Century" by Thomas Piketty | - | "The Pragmatic Programmer" by Hunt and Thomas | - | Buy cheese | - | Buy kiwis | - | Buy pears | - | Buy rutabagas | - - Scenario: Dropping a child task on the inbox makes it top-level - Given I display the "Inbox" page - And there is an item named "Buy apples" in the central list - And I drop the item on "Errands" in the central list - And there is an item named "Errands / Buy apples" in the central list - When I drop the item on "Inbox" in the page list - And I list the items - Then the list is: - | display | - | Errands | - | Buy apples | - | "Capital in the Twenty-First Century" by Thomas Piketty | - | "The Pragmatic Programmer" by Hunt and Thomas | - | Buy cheese | - | Buy kiwis | - | Buy pears | - | Buy rutabagas | - - Scenario: Dropping two tasks on another one makes them children - Given I display the "Inbox" page - And the central list contains items named: - | display | - | Buy apples | - | Buy pears | - When I drop items on "Errands" in the central list - And I list the items - Then the list is: - | display | - | Errands | - | Errands / Buy apples | - | Errands / Buy pears | - | "Capital in the Twenty-First Century" by Thomas Piketty | - | "The Pragmatic Programmer" by Hunt and Thomas | - | Buy cheese | - | Buy kiwis | - | Buy rutabagas | - - Scenario: Dropping two child tasks on the inbox makes them top-level - Given I display the "Inbox" page - And the central list contains items named: - | display | - | Buy apples | - | Buy pears | - And I drop items on "Errands" in the central list - And the central list contains items named: - | display | - | Errands / Buy apples | - | Errands / Buy pears | - When I drop items on "Inbox" in the page list - And I list the items - Then the list is: - | display | - | Errands | - | Buy apples | - | "Capital in the Twenty-First Century" by Thomas Piketty | - | "The Pragmatic Programmer" by Hunt and Thomas | - | Buy cheese | - | Buy kiwis | - | Buy pears | - | Buy rutabagas | - - Scenario: Dropping a task on the inbox removes it from it's associated project - Given I display the "Projects / TestData » Calendar1 / Prepare talk about TDD" page - And there is an item named "Create Sozi SVG" in the central list - When I drop the item on "Inbox" in the page list - And I display the "Inbox" page - And I look at the central list - And I list the items - Then the list is: - | display | - | Errands | - | Buy apples | - | "Capital in the Twenty-First Century" by Thomas Piketty | - | "The Pragmatic Programmer" by Hunt and Thomas | - | Buy cheese | - | Buy kiwis | - | Buy pears | - | Buy rutabagas | - | Create Sozi SVG | - - Scenario: Deparenting a task by dropping on the central list's blank area - Given I display the "Inbox" page - And I look at the central list - And there is an item named "Buy apples" in the central list - And I drop the item on "Errands" in the central list - And I look at the central list - And there is an item named "Errands / Buy apples" in the central list - When I drop the item on the blank area of the central list - And I list the items - Then the list is: - | display | - | Errands | - | Buy apples | - | "Capital in the Twenty-First Century" by Thomas Piketty | - | "The Pragmatic Programmer" by Hunt and Thomas | - | Buy cheese | - | Buy kiwis | - | Buy pears | - | Buy rutabagas | - - Scenario: Dropping a task on the inbox removes it from all it's contexts - Given I display the "Contexts / Errands" page - And there is an item named "Buy kiwis" in the central list - When I drop the item on "Inbox" in the page list - And I display the "Contexts / Errands" page - And I look at the central list - Then the list does not contain "Buy kiwis" - diff --git a/tests/features/zanshin/features/step_definitions/CMakeLists.txt b/tests/features/zanshin/features/step_definitions/CMakeLists.txt deleted file mode 100644 --- a/tests/features/zanshin/features/step_definitions/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -include_directories(${Boost_INCLUDE_DIRS}) -include_directories(${CMAKE_SOURCE_DIR}/3rdparty/cucumber-cpp/include) - -# Because cucumber-cpp exhibits plenty of warnings -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w") - -set(steps_SRCS - main.cpp - ../../../cuke-steps.cpp - ${CMAKE_SOURCE_DIR}/src/zanshin/app/dependencies.cpp -) - -add_executable(zanshin-cuke-steps ${steps_SRCS}) -target_link_libraries(zanshin-cuke-steps - cucumber-cpp - Threads::Threads - ${Boost_REGEX_LIBRARY} - ${Boost_SYSTEM_LIBRARY} - ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} - Qt5::Test - testlib - akonadi - domain - presentation - utils -) diff --git a/tests/features/zanshin/features/step_definitions/cucumber.wire b/tests/features/zanshin/features/step_definitions/cucumber.wire deleted file mode 100644 --- a/tests/features/zanshin/features/step_definitions/cucumber.wire +++ /dev/null @@ -1,2 +0,0 @@ -host: localhost -port: 3902 diff --git a/tests/features/zanshin/features/step_definitions/main.cpp b/tests/features/zanshin/features/step_definitions/main.cpp deleted file mode 100644 --- a/tests/features/zanshin/features/step_definitions/main.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/* This file is part of Zanshin - - Copyright 2014-2015 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 "zanshin/app/dependencies.h" - -namespace CukeSteps -{ - void initializeAppDependencies() - { - App::initializeDependencies(); - } -} -