diff options
Diffstat (limited to 'tests/test_calendarwindow.cpp')
| -rw-r--r-- | tests/test_calendarwindow.cpp | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/tests/test_calendarwindow.cpp b/tests/test_calendarwindow.cpp new file mode 100644 index 0000000..26f7237 --- /dev/null +++ b/tests/test_calendarwindow.cpp @@ -0,0 +1,222 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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 <QtTest> +#include <QAction> +#include <QApplication> +#include <QDir> +#include <QFile> +#include <QFileInfo> +#include <QLineEdit> +#include <QMessageBox> +#include <QPushButton> +#include <QTemporaryDir> +#include <QTimer> +#include <QUndoStack> + +#include <memory> + +#include "calendarwindow.h" +#include "config.h" + +namespace { + +QByteArray eventText(const QString &uid, const QString &summary, int day) +{ + return QStringLiteral( + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//example//test//EN\r\n" + "BEGIN:VEVENT\r\nUID:%1\r\nDTSTAMP:20260901T000000Z\r\n" + "DTSTART:202609%2T080000Z\r\nDTEND:202609%2T090000Z\r\nSUMMARY:%3\r\n" + "END:VEVENT\r\nEND:VCALENDAR\r\n") + .arg(uid).arg(day, 2, 10, QLatin1Char('0')).arg(summary).toUtf8(); +} + +void writeFile(const QString &path, const QByteArray &content) +{ + QDir().mkpath(QFileInfo(path).absolutePath()); + QFile f(path); + QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Truncate)); + f.write(content); +} + +QByteArray read(const QString &path) +{ + QFile f(path); + return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray(); +} + +/// A vdir with collections "a" and "b", two events in "a", and a config +/// pointing at it with syncing off, so no test ever runs vdirsyncer. +struct Fixture +{ + QTemporaryDir dir; + Config config; + QString a() const { return dir.filePath(QStringLiteral("cal/a/one.ics")); } + + Fixture() + { + writeFile(a(), eventText(QStringLiteral("one@example.org"), QStringLiteral("One"), 22)); + writeFile(dir.filePath(QStringLiteral("cal/a/two.ics")), + eventText(QStringLiteral("two@example.org"), QStringLiteral("Two"), 23)); + QDir().mkpath(dir.filePath(QStringLiteral("cal/b"))); + const QString ini = dir.filePath(QStringLiteral("qtmaildir.conf")); + writeFile(ini, QStringLiteral("[general]\ncalendars_dir = %1\n" + "default_calendar = b\ncalendar_sync_command = \n") + .arg(dir.filePath(QStringLiteral("cal"))).toUtf8()); + config.load(ini); + } + + CalendarWindow *window() + { + auto *w = new CalendarWindow(config, { QStringLiteral("me@example.org") }, + dir.filePath(QStringLiteral("uistate.conf"))); + w->showMonth(2026, 9); + w->show(); + return w; + } +}; + +/// Answers the next modal QMessageBox with `button` once it is up. +void answerNextBox(QMessageBox::StandardButton button) +{ + QTimer::singleShot(0, [button]() { + auto *box = qobject_cast<QMessageBox *>(QApplication::activeModalWidget()); + QVERIFY2(box, "no modal message box appeared"); + box->button(button)->click(); + }); +} + +void editTitle(CalendarWindow *w, const QString &title) +{ + w->findChild<QPushButton *>(QStringLiteral("editEvent"))->click(); + w->findChild<QLineEdit *>(QStringLiteral("eventTitle"))->setText(title); +} + +} // namespace + +class TestCalendarWindow : public QObject +{ + Q_OBJECT + +private slots: + void loadsAndSelects(); + void theEditLockRefusesASelectionChange(); + void closingWithUnsavedChangesAsksOnEveryRoute(); + void saveWritesAndUndoRestoresTheOriginalBytes(); + void aNewEventGoesToTheDefaultCalendar(); + void aStaleSaveKeepsTheFormAndTheSyncedFile(); +}; + +void TestCalendarWindow::loadsAndSelects() +{ + Fixture f; + std::unique_ptr<CalendarWindow> w(f.window()); + QVERIFY(w->selectEvent(QStringLiteral("two@example.org"))); + QCOMPARE(w->selectedUid(), QStringLiteral("two@example.org")); + QVERIFY(!w->selectEvent(QStringLiteral("absent@example.org"))); +} + +void TestCalendarWindow::theEditLockRefusesASelectionChange() +{ + Fixture f; + std::unique_ptr<CalendarWindow> w(f.window()); + QVERIFY(w->selectEvent(QStringLiteral("one@example.org"))); + editTitle(w.get(), QStringLiteral("Changed")); + QVERIFY(!w->selectEvent(QStringLiteral("two@example.org"))); + QCOMPARE(w->selectedUid(), QStringLiteral("one@example.org")); +} + +void TestCalendarWindow::closingWithUnsavedChangesAsksOnEveryRoute() +{ + Fixture f; + const QByteArray original = read(f.a()); + + // Cancel keeps the window and the file. Route: close(). + std::unique_ptr<CalendarWindow> w(f.window()); + w->selectEvent(QStringLiteral("one@example.org")); + editTitle(w.get(), QStringLiteral("Changed")); + answerNextBox(QMessageBox::Cancel); + w->close(); + QVERIFY(w->isVisible()); + QCOMPARE(read(f.a()), original); + + // Discard closes and writes nothing. Route: the File menu's Close action. + answerNextBox(QMessageBox::Discard); + w->findChild<QAction *>(QStringLiteral("closeCalendar"))->trigger(); + QVERIFY(!w->isVisible()); + QCOMPARE(read(f.a()), original); + + // Save closes and writes. + std::unique_ptr<CalendarWindow> again(f.window()); + again->selectEvent(QStringLiteral("one@example.org")); + editTitle(again.get(), QStringLiteral("Saved on close")); + answerNextBox(QMessageBox::Save); + again->close(); + QVERIFY(!again->isVisible()); + QVERIFY(read(f.a()).contains("SUMMARY:Saved on close")); +} + +void TestCalendarWindow::saveWritesAndUndoRestoresTheOriginalBytes() +{ + Fixture f; + const QByteArray original = read(f.a()); + std::unique_ptr<CalendarWindow> w(f.window()); + w->selectEvent(QStringLiteral("one@example.org")); + editTitle(w.get(), QStringLiteral("Renamed")); + w->findChild<QPushButton *>(QStringLiteral("saveEvent"))->click(); + QVERIFY(read(f.a()).contains("SUMMARY:Renamed")); + + w->undoStack()->undo(); + QCOMPARE(read(f.a()), original); // exactly the bytes it had: item 176's rule + w->undoStack()->redo(); + QVERIFY(read(f.a()).contains("SUMMARY:Renamed")); +} + +void TestCalendarWindow::aNewEventGoesToTheDefaultCalendar() +{ + Fixture f; + std::unique_ptr<CalendarWindow> w(f.window()); + w->findChild<QAction *>(QStringLiteral("newEvent"))->trigger(); + w->findChild<QLineEdit *>(QStringLiteral("eventTitle"))->setText(QStringLiteral("Fresh")); + w->findChild<QPushButton *>(QStringLiteral("saveEvent"))->click(); + const QStringList created = QDir(f.dir.filePath(QStringLiteral("cal/b"))) + .entryList({ QStringLiteral("*.ics") }, QDir::Files); + QCOMPARE(created.size(), 1); + QVERIFY(read(f.dir.filePath(QStringLiteral("cal/b/")) + created.first()).contains("SUMMARY:Fresh")); + + w->undoStack()->undo(); + QVERIFY(QDir(f.dir.filePath(QStringLiteral("cal/b"))).entryList({ QStringLiteral("*.ics") }).isEmpty()); +} + +void TestCalendarWindow::aStaleSaveKeepsTheFormAndTheSyncedFile() +{ + Fixture f; + std::unique_ptr<CalendarWindow> w(f.window()); + w->selectEvent(QStringLiteral("one@example.org")); + editTitle(w.get(), QStringLiteral("Mine")); + // A sync pulls a server change while the form is open. + const QByteArray synced = eventText(QStringLiteral("one@example.org"), QStringLiteral("Theirs"), 22); + writeFile(f.a(), synced); + w->findChild<QPushButton *>(QStringLiteral("saveEvent"))->click(); + QCOMPARE(read(f.a()), synced); // nothing clobbered + QVERIFY(w->isEditing()); // the user's values are still there + QCOMPARE(w->findChild<QLineEdit *>(QStringLiteral("eventTitle"))->text(), QStringLiteral("Mine")); +} + +QTEST_MAIN(TestCalendarWindow) +#include "test_calendarwindow.moc" |
