diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_config.cpp | 47 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 163 |
2 files changed, 210 insertions, 0 deletions
diff --git a/tests/test_config.cpp b/tests/test_config.cpp index a09738b..4d92891 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -45,6 +45,9 @@ private slots: void markReadDelayIsActuallyRead(); void markReadDelayAcceptsZeroAndNegative(); void markReadDelayRejectsGarbage(); + void syncOnExitDefaultsToAsk(); + void syncOnExitReadsAllThreeValues(); + void syncOnExitWarnsOnGarbage(); void extraMimetypesAppendToBuiltins(); void extraMimetypeDescriptionMayContainComma(); void malformedExtraMimetypeIsSkipped(); @@ -429,6 +432,50 @@ void TestConfig::markReadDelayRejectsGarbage() QVERIFY(!config.problems().isEmpty()); } +void TestConfig::syncOnExitDefaultsToAsk() +{ + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\n"))); + QCOMPARE(config.syncOnExit(), Config::SyncOnExit::Ask); + QVERIFY(config.problems().isEmpty()); +} + +void TestConfig::syncOnExitReadsAllThreeValues() +{ + // A bool could only carry two of these. Each is a distinct behaviour at + // exit, so each has to round-trip. + const QList<QPair<QString, Config::SyncOnExit>> cases = { + { QStringLiteral("ask"), Config::SyncOnExit::Ask }, + { QStringLiteral("always"), Config::SyncOnExit::Always }, + { QStringLiteral("never"), Config::SyncOnExit::Never }, + // Case and surrounding space are the user's, not the parser's problem. + { QStringLiteral(" Always "), Config::SyncOnExit::Always }, + }; + + for (const auto &testCase : cases) { + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\nsync_on_exit=%1\n") + .arg(testCase.first))); + QCOMPARE(config.syncOnExit(), testCase.second); + QVERIFY2(config.problems().isEmpty(), + qPrintable(QStringLiteral("'%1' warned").arg(testCase.first))); + } +} + +void TestConfig::syncOnExitWarnsOnGarbage() +{ + // Silently falling back would change what happens to unsynced work without + // telling the user, so a typo has to be named. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\n" + "sync_on_exit=maybe\n"))); + QCOMPARE(config.syncOnExit(), Config::SyncOnExit::Ask); + QVERIFY(!config.problems().isEmpty()); +} + void TestConfig::extraMimetypesAppendToBuiltins() { QTemporaryDir dir; diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index fb33462..d307a99 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -20,8 +20,10 @@ #include <QAction> #include <QApplication> +#include <QCloseEvent> #include <QDir> #include <QKeyEvent> +#include <QLabel> #include <QLineEdit> #include <QFile> #include <QSettings> @@ -35,6 +37,7 @@ #include "keymap.h" #include "mainwindow.h" #include "messageview.h" +#include "notmuchworker.h" #include "threadlistmodel.h" /// MainWindow is mostly wiring, and the parts that need a real database are @@ -59,6 +62,10 @@ private slots: void markReadTimerRestartsRatherThanStacking(); void markReadTimerIsNotArmedForAReadThread(); void markReadCanBeDisabled(); + void pendingEditCountSurvivesAQuery(); + void aFailedSyncDoesNotClearThePendingCount(); + void closingWithNoPendingEditsDoesNotPrompt(); + void syncOnExitNeverClosesSilently(); }; void TestMainWindow::everyKnownActionIsRegistered() @@ -428,6 +435,162 @@ void TestMainWindow::markReadCanBeDisabled() "a negative mark_read_delay_ms must disable the timer"); } +void TestMainWindow::pendingEditCountSurvivesAQuery() +{ + // The defining property, and the reason this is a counter of its own rather + // than QUndoStack::isClean(): the undo stack is cleared on every query, + // because its entries refer to rows the new result set discards. Tag a + // thread, run any query, and the stack is empty while the change is still + // sitting unsynced in the database. + const Config config; + MainWindow window(config); + + auto *label = window.findChild<QLabel *>(QStringLiteral("pendingEdits")); + QVERIFY(label); + QVERIFY2(label->isHidden(), "the indicator must start hidden at zero"); + + // Confirm a write the way the worker really does, by emitting the signal + // the window listens to. No test-only entry point on MainWindow. + TagChange change; + change.added = { QStringLiteral("deleted") }; + change.description = QStringLiteral("Delete"); + QVERIFY(QMetaObject::invokeMethod(&window, "onTagsApplied", + Q_ARG(TagChange, change))); + + QVERIFY2(!label->isHidden(), "a confirmed edit must show the indicator"); + const QString afterEdit = label->text(); + QVERIFY(!afterEdit.isEmpty()); + + // Now run a query, which clears the undo stack. The indicator must not + // follow it down. + window.findChild<QLineEdit *>()->setText(QStringLiteral("tag:inbox")); + QMetaObject::invokeMethod(&window, "runCurrentQuery"); + + QVERIFY2(!label->isHidden(), + "the indicator was cleared by a query, so it is tracking the undo " + "stack rather than unsynced state"); + QCOMPARE(label->text(), afterEdit); +} + +void TestMainWindow::aFailedSyncDoesNotClearThePendingCount() +{ + // A failed sync means the edits are still unsynced. Clearing here would + // assert the opposite, and the user would quit believing their tagging had + // been carried over. + const Config config; + MainWindow window(config); + + auto *label = window.findChild<QLabel *>(QStringLiteral("pendingEdits")); + QVERIFY(label); + + TagChange change; + change.added = { QStringLiteral("flagged") }; + QVERIFY(QMetaObject::invokeMethod(&window, "onTagsApplied", + Q_ARG(TagChange, change))); + QVERIFY(!label->isHidden()); + const QString afterEdit = label->text(); + + QMetaObject::invokeMethod(&window, "onSyncFinished", + Q_ARG(bool, false), Q_ARG(int, 1)); + QVERIFY2(!label->isHidden(), "a FAILED sync cleared the pending count"); + QCOMPARE(label->text(), afterEdit); + + // A successful one does clear it, so this is not "never clears". + QMetaObject::invokeMethod(&window, "onSyncFinished", + Q_ARG(bool, true), Q_ARG(int, 0)); + QVERIFY2(label->isHidden(), "a successful sync must clear the indicator"); +} + +/// Closes a window and reports whether it accepted, failing rather than hanging +/// if a modal appears. +/// +/// A modal spins its own event loop, so a test that simply sends a close event +/// blocks forever when a dialog it did not expect opens. This arms a timer that +/// closes any active modal and records that one was there, which turns "a +/// dialog appeared" into an assertion instead of a hung run. +struct CloseProbe +{ + bool accepted = false; + bool sawModal = false; + + void run(MainWindow *window) + { + QTimer poll; + poll.setInterval(50); + int ticks = 0; + QObject::connect(&poll, &QTimer::timeout, [this, &poll, &ticks]() { + if (QWidget *modal = QApplication::activeModalWidget()) { + sawModal = true; + modal->close(); + poll.stop(); + return; + } + if (++ticks > 20) // one second is ample for a synchronous close + poll.stop(); + }); + poll.start(); + + QCloseEvent event; + QApplication::sendEvent(window, &event); + accepted = event.isAccepted(); + poll.stop(); + } +}; + +void TestMainWindow::closingWithNoPendingEditsDoesNotPrompt() +{ + // Nothing outstanding means nothing to ask about. If a prompt fires here it + // is keying off something other than there being work to lose, and every + // quit would carry a dialog. + const Config config; + MainWindow window(config); + + CloseProbe probe; + probe.run(&window); + + QVERIFY2(!probe.sawModal, "a clean window prompted on close"); + QVERIFY2(probe.accepted, "a clean window refused to close"); +} + +void TestMainWindow::syncOnExitNeverClosesSilently() +{ + // "never" is the behaviour that existed before the prompt did, and it has + // to stay reachable for anyone who does not want to be asked. It must hold + // whether or not a sync command is configured, so this covers both: the + // no-command path has its own dialog, and "never" must skip that one too. + QTemporaryDir dir; + const QString path = dir.filePath(QStringLiteral("qtmaildir.conf")); + { + QFile file(path); + QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Text)); + file.write("[general]\nsync_on_exit=never\n\n[sync]\ncommand=/bin/true\n"); + } + + Config config; + config.load(path); + QCOMPARE(config.syncOnExit(), Config::SyncOnExit::Never); + + MainWindow window(config); + + // Give it something to lose, so this is not passing for the same reason + // the previous test does. + TagChange change; + change.added = { QStringLiteral("deleted") }; + QVERIFY(QMetaObject::invokeMethod(&window, "onTagsApplied", + Q_ARG(TagChange, change))); + auto *label = window.findChild<QLabel *>(QStringLiteral("pendingEdits")); + QVERIFY(label); + QVERIFY(!label->isHidden()); + + CloseProbe probe; + probe.run(&window); + + QVERIFY2(!probe.sawModal, + "sync_on_exit=never prompted anyway"); + QVERIFY2(probe.accepted, + "sync_on_exit=never must close without prompting"); +} + // Constructing a MainWindow needs a QApplication and a platform plugin. The // test has no display under ctest, so it runs offscreen unless the caller // asked for something else. |
