Verified Commit 44163cf9 authored by Daniel Vrátil's avatar Daniel Vrátil 🤖
Browse files

Implement Akonadi unittests for Tag widgets

parent 8e7be806
find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Test DBus)
if(${EXECUTABLE_OUTPUT_PATH})
set(PREVIOUS_EXEC_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH})
else()
set(PREVIOUS_EXEC_OUTPUT_PATH .)
endif()
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR})
set(TEST_RESULT_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}/testresults)
file(MAKE_DIRECTORY ${TEST_RESULT_OUTPUT_PATH})
option(AKONADI_TESTS_XML "Use XML files for the test results, instead of plain text." FALSE)
option(AKONADI_RUN_SQLITE_ISOLATED_TESTS "Run isolated tests with sqlite3 as backend" TRUE)
option(AKONADI_RUN_MYSQL_ISOLATED_TESTS "Run isolated tests with MySQL as backend" TRUE)
option(AKONADI_RUN_PGSQL_ISOLATED_TESTS "Run isolated tests with PostgreSQL as backend" TRUE)
kde_enable_exceptions()
include_directories(
${Boost_INCLUDE_DIR}
)
# convenience macro to add akonadi qtestlib unit-tests
macro(add_akonadi_test _source)
set(_test ${_source} ${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp)
get_filename_component(_name ${_source} NAME_WE)
ecm_add_test(TEST_NAME ${_name} ${_test})
set_tests_properties(${_name} PROPERTIES ENVIRONMENT "QT_HASH_SEED=1;QT_NO_CPU_FEATURE=sse4.2")
target_link_libraries(${_name} akonaditestfake Qt5::Test KF5::AkonadiPrivate KF5::I18n)
endmacro()
# convenience macro to add akonadi qtestlib unit-tests
macro(add_akonadi_test_widgets _source)
set(_test
${_source}
${CMAKE_BINARY_DIR}/src/widgets/akonadiwidgets_debug.cpp
${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp
)
get_filename_component(_name ${_source} NAME_WE)
ecm_add_test(TEST_NAME ${_name} ${_test})
set_tests_properties(${_name} PROPERTIES ENVIRONMENT "QT_HASH_SEED=1;QT_NO_CPU_FEATURE=sse4.2")
target_link_libraries(${_name} akonaditestfake Qt5::Test KF5::AkonadiWidgets KF5::AkonadiPrivate)
endmacro()
include(../KF5AkonadiMacros.cmake)
add_subdirectory(private)
add_subdirectory(server)
add_subdirectory(libs)
add_subdirectory(akonadicontrol)
add_subdirectory(shared)
add_subdirectory(widgets)
set(QT_REQUIRED_VERSION "5.11.0")
find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Test DBus)
if(${EXECUTABLE_OUTPUT_PATH})
set( PREVIOUS_EXEC_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH} )
else()
set( PREVIOUS_EXEC_OUTPUT_PATH . )
endif()
set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} )
set( TEST_RESULT_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}/testresults )
file(MAKE_DIRECTORY ${TEST_RESULT_OUTPUT_PATH})
option(AKONADI_TESTS_XML "Use XML files for the test results, instead of plain text." FALSE)
option(AKONADI_RUN_SQLITE_ISOLATED_TESTS "Run isolated tests with sqlite3 as backend" TRUE)
option(AKONADI_RUN_MYSQL_ISOLATED_TESTS "Run isolated tests with MySQL as backend" TRUE)
option(AKONADI_RUN_PGSQL_ISOLATED_TESTS "Run isolated tests with PostgreSQL as backend" TRUE)
kde_enable_exceptions()
include_directories(
${Boost_INCLUDE_DIR}
)
# convenience macro to add akonadi qtestlib unit-tests
macro(add_akonadi_test _source)
set(_test ${_source} ${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp)
get_filename_component(_name ${_source} NAME_WE)
ecm_add_test(TEST_NAME ${_name} ${_test})
set_tests_properties(${_name} PROPERTIES ENVIRONMENT "QT_HASH_SEED=1;QT_NO_CPU_FEATURE=sse4.2")
target_link_libraries(${_name} akonaditestfake Qt5::Test KF5::AkonadiPrivate KF5::I18n)
endmacro()
# convenience macro to add akonadi qtestlib unit-tests
macro(add_akonadi_test_widgets _source)
set(_test
${_source}
${CMAKE_BINARY_DIR}/src/widgets/akonadiwidgets_debug.cpp
${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp
)
get_filename_component(_name ${_source} NAME_WE)
ecm_add_test(TEST_NAME ${_name} ${_test})
set_tests_properties(${_name} PROPERTIES ENVIRONMENT "QT_HASH_SEED=1;QT_NO_CPU_FEATURE=sse4.2")
target_link_libraries(${_name} akonaditestfake Qt5::Test KF5::AkonadiWidgets KF5::AkonadiPrivate)
endmacro()
include(../../KF5AkonadiMacros.cmake)
# akonadi test fake library
set(akonaditestfake_xml ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml)
set_source_files_properties(${akonaditestfake_xml} PROPERTIES INCLUDE "protocol_p.h")
......
macro(add_akonadi_isolated_widget_test source)
add_akonadi_isolated_test(SOURCE ${source} LINK_LIBRARIES KF5::AkonadiWidgets KF5::Crash)
endmacro()
add_akonadi_isolated_widget_test(tageditwidgettest.cpp)
add_akonadi_isolated_widget_test(tagwidgettest.cpp)
/*
* Copyright 2020 Daniel Vrátil <dvratil@kde.org>
*
* 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, see <https://www.gnu.org/licenses/>.
*
*/
#include "qtest_akonadi.h"
#include <shared/aktest.h>
#include "tageditwidget.h"
#include "tagmodel.h"
#include "monitor.h"
#include "tag.h"
#include "tagdeletejob.h"
#include "tagcreatejob.h"
#include <QSignalSpy>
#include <QTest>
#include <QLineEdit>
#include <QPushButton>
#include <QListView>
#include <QDialogButtonBox>
#include <QDialog>
#include <QLabel>
#include <memory>
using namespace Akonadi;
/***
* This test also covers TagManagementDialog and TagSelectionDialog, which
* both wrap TagEditWidget and provide their own own Monitor and TagModel,
* just one allows selection and the other does not
*/
class TagEditWidgetTest: public QObject
{
Q_OBJECT
struct TestSetup {
TestSetup()
{
monitor = std::make_unique<Monitor>();
monitor->setTypeMonitored(Monitor::Tags);
model = std::make_unique<TagModel>(monitor.get());
QSignalSpy modelSpy(model.get(), &TagModel::populated);
QVERIFY(modelSpy.wait());
QCOMPARE(model->rowCount(), 1); // there's one existing tag
widget = std::make_unique<TagEditWidget>();
widget->setModel(model.get());
widget->show();
QVERIFY(QTest::qWaitForWindowActive(widget.get()));
newTagEdit = widget->findChild<QLineEdit*>(QStringLiteral("newTagEdit"));
QVERIFY(newTagEdit);
newTagButton = widget->findChild<QPushButton*>(QStringLiteral("newTagButton"));
QVERIFY(newTagButton);
QVERIFY(!newTagButton->isEnabled());
tagsView = widget->findChild<QListView*>(QStringLiteral("tagsView"));
QVERIFY(tagsView);
tagDeleteButton = widget->findChild<QPushButton*>(QStringLiteral("tagDeleteButton"));
QVERIFY(tagDeleteButton);
QVERIFY(!tagDeleteButton->isVisible());
valid = true;
}
~TestSetup()
{
if (!createdTags.empty()) {
auto deleteJob = new TagDeleteJob(createdTags);
AKVERIFYEXEC(deleteJob);
}
}
bool createTags(int count)
{
const auto doCreateTags = [this, count]() {
QSignalSpy monitorSpy(monitor.get(), &Monitor::tagAdded);
for (int i = 0; i < count; ++i) {
auto job = new TagCreateJob(Tag(QStringLiteral("TestTag-%1").arg(i)));
AKVERIFYEXEC(job);
createdTags.push_back(job->tag());
}
QTRY_COMPARE(monitorSpy.count(), count);
};
doCreateTags();
return createdTags.size() == count;
}
bool checkSelectionIsEmpty() const
{
const auto model = tagsView->model();
for (int i = 0; i < model->rowCount(); ++i) {
if (model->data(model->index(i, 0), Qt::CheckStateRole).value<Qt::CheckState>() != Qt::Unchecked) {
return false;
}
}
return true;
}
QModelIndex indexForTag(const Tag &tag) const
{
for (int i = 0; i < tagsView->model()->rowCount(); ++i) {
const auto index = tagsView->model()->index(i, 0);
if (tagsView->model()->data(index, TagModel::TagRole).value<Tag>() == tag) {
return index;
}
}
return {};
}
bool deleteTag(const Tag &tag, bool confirmDeletion)
{
const auto index = indexForTag(tag);
AKVERIFY(index.isValid());
const auto itemRect = tagsView->visualRect(index);
// Hover over the item and confirm the button is there
QTest::mouseMove(tagsView->viewport(), itemRect.center());
AKVERIFY(QTest::qWaitFor(std::bind(&QWidget::isVisible, tagDeleteButton)));
AKVERIFY(tagDeleteButton->geometry().intersects(itemRect));
// Clicking the button blocks (QDialog::exec), so we need to confirm the
// dialog from event loop
bool confirmed =false;
QTimer::singleShot(100, [this, confirmDeletion, &confirmed]() {
confirmed = confirmDialog(confirmDeletion);
QVERIFY(confirmed);
});
QTest::mouseClick(tagDeleteButton, Qt::LeftButton);
// Check that the confirmation was succesful
AKVERIFY(confirmed);
return true;
}
bool confirmDialog(bool confirmDeletion)
{
const auto windows = QApplication::topLevelWidgets();
for (const auto *window : windows) {
// We are using KMessageBox, which is not a QMessageBox but rather a custom QDialog
if (window->objectName() == QLatin1String("questionYesNo")) {
const auto msgbox = qobject_cast<const QDialog*>(window);
AKVERIFY(msgbox);
const auto buttonBox = msgbox->findChild<const QDialogButtonBox*>();
AKVERIFY(buttonBox);
const auto button = buttonBox->button(confirmDeletion ? QDialogButtonBox::Yes : QDialogButtonBox::No);
AKVERIFY(button);
QTest::mouseClick(button, Qt::LeftButton);
return true;
}
}
return false;
}
std::unique_ptr<Monitor> monitor;
std::unique_ptr<TagModel> model;
std::unique_ptr<TagEditWidget> widget;
QLineEdit *newTagEdit = nullptr;
QPushButton *newTagButton = nullptr;
QListView *tagsView = nullptr;
QPushButton *tagDeleteButton = nullptr;
Tag::List createdTags;
bool valid = false;
};
private Q_SLOTS:
void initTestCase()
{
AkonadiTest::checkTestIsIsolated();
}
void testTagCreationWithEnter()
{
const auto tagName = QStringLiteral("TagEditWidgetTestTag");
TestSetup test;
QVERIFY(test.valid);
QSignalSpy monitorSpy(test.monitor.get(), &Monitor::tagAdded);
QTest::keyClicks(test.newTagEdit, tagName);
QVERIFY(test.newTagButton->isEnabled());
QTest::keyClick(test.newTagEdit, Qt::Key_Return);
QVERIFY(!test.newTagButton->isEnabled());
QVERIFY(!test.newTagEdit->isEnabled());
QVERIFY(monitorSpy.wait());
test.createdTags.push_back(monitorSpy.at(0).at(0).value<Akonadi::Tag>());
QCOMPARE(test.model->rowCount(), 2);
QCOMPARE(test.model->data(test.model->index(1, 0), Qt::DisplayRole).toString(), tagName);
}
void testTagCreationWithButton()
{
const auto tagName = QStringLiteral("TagEditWidgetTestTag");
TestSetup test;
QVERIFY(test.valid);
QSignalSpy monitorSpy(test.monitor.get(), &Monitor::tagAdded);
QTest::keyClicks(test.newTagEdit, tagName);
QVERIFY(test.newTagButton->isEnabled());
QTest::mouseClick(test.newTagButton, Qt::LeftButton);
QVERIFY(!test.newTagButton->isEnabled());
QVERIFY(!test.newTagEdit->isEnabled());
QVERIFY(monitorSpy.wait());
test.createdTags.push_back(monitorSpy.at(0).at(0).value<Akonadi::Tag>());
QCOMPARE(test.model->rowCount(), 2);
QCOMPARE(test.model->data(test.model->index(1, 0), Qt::DisplayRole).toString(), tagName);
}
void testDuplicatedTagCannotBeCreated()
{
TestSetup test;
QVERIFY(test.valid);
// Create a tag
QVERIFY(test.createTags(1));
// Wait for the tag to appear in the model
QTRY_COMPARE(test.model->rowCount(), 2);
// Type the entire string char-by-char - once the name is full the button
// should be disabled because we don't allow creating duplicated tags
const auto tagName = test.createdTags.front().name();
for (int i = 0; i < tagName.size(); ++i) {
QTest::keyClicks(test.newTagEdit, tagName[i]);
QCOMPARE(test.newTagButton->isEnabled(), i != tagName.size() - 1);
}
}
void testSettingSelectionFromCode()
{
TestSetup test;
QVERIFY(test.valid);
QVERIFY(test.createTags(10));
test.widget->setSelectionEnabled(true);
// Nothing should be checked
QVERIFY(test.checkSelectionIsEmpty());
// Set selection
auto model = test.tagsView->model();
Tag::List selectTags;
for (int i = 0; i < model->rowCount(); i += 2) {
selectTags.push_back(model->data(model->index(i, 0), TagModel::TagRole).value<Tag>());
}
QVERIFY(!selectTags.empty());
test.widget->setSelection(selectTags);
QCOMPARE(test.widget->selection(), selectTags);
// Confirm that the items are visually selected
for (int i = 0; i < model->rowCount(); ++i) {
const auto tag = model->data(model->index(i, 0), TagModel::TagRole).value<Tag>();
const auto expectedState = selectTags.contains(tag) ? Qt::Checked : Qt::Unchecked;
QCOMPARE(model->data(model->index(i, 0), Qt::CheckStateRole).value<Qt::CheckState>(), expectedState);
}
}
void testSelectingTagsByMouse()
{
TestSetup test;
QVERIFY(test.valid);
QVERIFY(test.createTags(10));
test.widget->setSelectionEnabled(true);
// Nothing should be checked
QVERIFY(test.checkSelectionIsEmpty());
// Check several tags
Tag::List selectedTags;
auto model = test.tagsView->model();
for (int i = 0; i < model->rowCount(); i += 2) {
const auto index = model->index(i, 0);
selectedTags.push_back(model->data(index, TagModel::TagRole).value<Tag>());
// Select the row
QTest::mouseClick(test.tagsView->viewport(), Qt::LeftButton, {}, test.tagsView->visualRect(index).topLeft() + QPoint(5, 5));
// Use spacebar to toggle selection, we can't possibly hit the checkbox with mouse in a
// reliable manner.
QTest::keyClick(test.tagsView, Qt::Key_Space);
}
// Confirm that the selection occured
for (int i = 0; i < model->rowCount(); ++i) {
const auto expectedState = i % 2 == 0 ? Qt::Checked : Qt::Unchecked;
QCOMPARE(model->data(model->index(i, 0), Qt::CheckStateRole).value<Qt::CheckState>(), expectedState);
}
// Compare the selectede tags
auto currentSelection = test.widget->selection();
const auto sortTag = [](const Tag &l, const Tag &r) { return l.id() < r.id(); };
std::sort(currentSelection.begin(), currentSelection.end(), sortTag);
std::sort(selectedTags.begin(), selectedTags.end(), sortTag);
QCOMPARE(currentSelection, selectedTags);
}
void testDeletingTags()
{
TestSetup test;
QVERIFY(test.valid);
QVERIFY(test.createTags(4));
while (!test.createdTags.empty()) {
QSignalSpy monitorSpy(test.monitor.get(), &Monitor::tagRemoved);
// Get the last tag in the list and delete it
const auto tag = test.createdTags.last();
QVERIFY(test.deleteTag(tag, true));
// Wait for confirmation
QVERIFY(monitorSpy.wait());
QCOMPARE(monitorSpy.at(0).at(0).value<Tag>(), tag);
test.createdTags.pop_back(); // remove the tag from the list
}
// Verify that we've deleted everything
QVERIFY(test.createdTags.empty());
QCOMPARE(test.model->rowCount(), 1); // only the default tag remains
}
void testRejectingDeleteDialogDoesntDeleteTheTAg()
{
TestSetup test;
QVERIFY(test.valid);
QSignalSpy monitorSpy(test.monitor.get(), &Monitor::tagRemoved);
const auto index = test.model->index(0, 0);
QVERIFY(index.isValid());
const auto tag = test.model->data(index, TagModel::TagRole).value<Tag>();
QVERIFY(test.deleteTag(tag, false));
QTest::qWait(500); // wait some amount of time to see that nothing has changed...
QVERIFY(monitorSpy.empty());
QCOMPARE(test.model->rowCount(), 1);
}
};
QTEST_AKONADIMAIN(TagEditWidgetTest)
#include "tageditwidgettest.moc"
/*
* Copyright 2020 Daniel Vrátil <dvratil@kde.org>
*
* 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, see <https://www.gnu.org/licenses/>.
*
*/
#include "qtest_akonadi.h"
#include <iterator>
#include <qnamespace.h>
#include <qtestmouse.h>
#include <shared/aktest.h>
#include "tagwidget.h"
#include "tagselectiondialog.h"
#include "tagmodel.h"
#include "monitor.h"
#include "tag.h"
#include "tagdeletejob.h"
#include "tagcreatejob.h"
#include <QSignalSpy>
#include <QTest>
#include <QLineEdit>
#include <QToolButton>
#include <QDialogButtonBox>
#include <QPushButton>
#include <memory>
using namespace Akonadi;
class TagWidgetTest: public QObject
{
Q_OBJECT
struct TestSetup {
TestSetup()
{
widget = std::make_unique<TagWidget>();
widget->show();
monitor = widget->findChild<Monitor*>();
QVERIFY(monitor);
model = widget->findChild<TagModel*>();
QVERIFY(model);
QSignalSpy modelSpy(model, &TagModel::populated);
QVERIFY(modelSpy.wait());
QVERIFY(QTest::qWaitForWindowActive(widget.get()));
tagView = widget->findChild<QLineEdit*>(QStringLiteral("tagView"));
QVERIFY(tagView);
editButton = widget->findChild<QToolButton*>(QStringLiteral("editButton"));
QVERIFY(editButton);
valid = true;
}
~TestSetup()
{
if (!createdTags.empty()) {
auto deleteJob = new TagDeleteJob(createdTags);
AKVERIFYEXEC(deleteJob);
}
}
bool createTags(int count)
{
const auto doCreateTags = [this, count]() {
QSignalSpy monitorSpy(monitor, &Monitor::tagAdded);
for (int i = 0; i < count; ++i) {
auto job = new TagCreateJob(Tag(QStringLiteral("TestTag-%1").arg(i)));
AKVERIFYEXEC(job);
createdTags.push_back(job->tag());
}
QTRY_COMPARE(monitorSpy.count(), count);
};
doCreateTags();
return createdTags.size() == count;
}
bool testSelectionMatches(QSignalSpy &selectionSpy, const Tag::List &selection)
{
QStringList names;
std::transform(selection.begin(), selection.end(), std::back_inserter(names), std::bind(&Tag::name, std::placeholders::_1));
AKCOMPARE(widget->selection(), selection);
AKVERIFY(selectionSpy.wait());
AKCOMPARE(selectionSpy.at(0).at