summaryrefslogtreecommitdiffstats
path: root/tests/test_calendarwindow.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-24 17:16:11 +0200
committerDanilo M. <danix@danix.xyz>2026-09-24 17:16:11 +0200
commite6a96c8b966fd060090805d9a689187e1ed28468 (patch)
tree323d0c3eb27f06e2f5a728862ddc4ed6be9e8ce1 /tests/test_calendarwindow.cpp
parentab9bad27b4aab552d3aa95164eefacf4945c1d60 (diff)
downloadqtmaildir-e6a96c8b966fd060090805d9a689187e1ed28468.tar.gz
qtmaildir-e6a96c8b966fd060090805d9a689187e1ed28468.zip
feat: the calendar window, with undo and a stale-safe save
Diffstat (limited to 'tests/test_calendarwindow.cpp')
-rw-r--r--tests/test_calendarwindow.cpp222
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"