aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-04 19:54:40 +0200
committerDanilo M. <danix@danix.xyz>2026-08-04 19:54:40 +0200
commitbd1b907ba22464bb869bcc10cc9327ed74c44791 (patch)
treec593b55f9a5fd4912104c33cddf59f894c9308aa /tests
parenta1f11aa861136fca3ec464e0a6268abeed1d8109 (diff)
downloadqtmaildir-bd1b907ba22464bb869bcc10cc9327ed74c44791.tar.gz
qtmaildir-bd1b907ba22464bb869bcc10cc9327ed74c44791.zip
feat(ui): make Delete a toggle, and expire transient status messages
Items 16 and 33. Delete now removes the `deleted` tag when every selected thread already carries it, so pressing it twice puts a thread back. One direction for the whole selection, never per row: toggling each independently would leave a single keystroke with the selection in two states, which is worse than either outcome. Status messages are classified rather than blanket-timed, which is the substance of item 33. Events expire after six seconds and fall back to the last query's thread count: "Sync complete", "Nothing to undo", the skip notice, the per-action "Archive: 3 threads". State does not expire: "Searching...", "Syncing...", the selection count, and "Sync failed (exit N)", because an error must not vanish before it is read. A test caught a mistake in that routing. Making the per-action message transient armed the timer during select-all, since tagSelected() runs on a selection onSelectionChanged() had just described, and the count would then be replaced while it was still true. Writing the count now cancels any transient still counting down. QStatusBar::showMessage() would give the same behaviour but the label is added with addWidget() beside permanent widgets, so adopting it means reworking that arrangement. One timer beside the label is the smaller change. Both fixes verified by reverting them and watching the tests fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/test_mainwindow.cpp110
1 files changed, 110 insertions, 0 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index 8a1dde3..f791d8f 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -82,6 +82,10 @@ private slots:
void theSyncButtonIsDisabledWhileABackgroundSyncHoldsTheLock();
void anUnobservableLockTableLeavesTheSyncButtonUsable();
void escapeBlanksTheMessagePane();
+ void deleteTogglesOnAnAlreadyDeletedThread();
+ void deleteOnAMixedSelectionDeletesRatherThanSplittingIt();
+ void aTransientStatusMessageExpires();
+ void theSelectionCountIsStateAndDoesNotExpire();
};
void TestMainWindow::everyKnownActionIsRegistered()
@@ -1070,6 +1074,112 @@ void TestMainWindow::escapeBlanksTheMessagePane()
QCOMPARE(view->selectionModel()->selectedRows().size(), 1);
}
+void TestMainWindow::deleteTogglesOnAnAlreadyDeletedThread()
+{
+ // Hitting Delete twice is the natural way to say "no, put it back", and
+ // adding a tag that is already present is a no-op the user cannot see.
+ const Config config;
+ MainWindow window(config);
+
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *view = window.findChild<QTableView *>();
+ QVERIFY(view);
+ auto *action = window.findChild<QAction *>(QStringLiteral("delete"));
+ QVERIFY(action);
+
+ model->appendBatch({ makeThread(QStringLiteral("t1"),
+ { QStringLiteral("deleted") }) });
+ view->selectRow(0);
+
+ action->trigger();
+
+ // The optimistic model update is synchronous, so the row reflects the
+ // change without a worker.
+ QVERIFY2(!model->threadAt(0).isDeleted(),
+ "delete on an already-deleted thread did not undelete it");
+}
+
+void TestMainWindow::deleteOnAMixedSelectionDeletesRatherThanSplittingIt()
+{
+ // The constraint that makes this more than a one-liner: toggling each
+ // thread independently would leave one keystroke with the selection in two
+ // states, which is worse than either outcome. Undelete only when every
+ // selected thread is already deleted.
+ const Config config;
+ MainWindow window(config);
+
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *view = window.findChild<QTableView *>();
+ QVERIFY(view);
+ auto *action = window.findChild<QAction *>(QStringLiteral("delete"));
+ QVERIFY(action);
+
+ model->appendBatch({ makeThread(QStringLiteral("t1"),
+ { QStringLiteral("deleted") }),
+ makeThread(QStringLiteral("t2"), {}) });
+
+ view->selectAll();
+ QCOMPARE(view->selectionModel()->selectedRows().size(), 2);
+
+ action->trigger();
+
+ QVERIFY2(model->threadAt(0).isDeleted() && model->threadAt(1).isDeleted(),
+ "a mixed selection split instead of deleting the whole selection");
+}
+
+void TestMainWindow::aTransientStatusMessageExpires()
+{
+ // "Sync complete" describes an event, not a state, and reads as though it
+ // describes the present until something else overwrites it.
+ const Config config;
+ MainWindow window(config);
+
+ auto *status = window.findChild<QLabel *>(QStringLiteral("statusMessage"));
+ QVERIFY(status);
+ auto *timer = window.findChild<QTimer *>(QStringLiteral("statusTimer"));
+ QVERIFY2(timer, "no status expiry timer");
+
+ QMetaObject::invokeMethod(&window, "showTransientStatus",
+ Q_ARG(QString, QStringLiteral("Sync complete")));
+ QCOMPARE(status->text(), QStringLiteral("Sync complete"));
+ QVERIFY(timer->isActive());
+
+ // Fire it rather than waiting out the real interval.
+ timer->setInterval(0);
+ QTRY_VERIFY_WITH_TIMEOUT(status->text() != QStringLiteral("Sync complete"),
+ 2000);
+}
+
+void TestMainWindow::theSelectionCountIsStateAndDoesNotExpire()
+{
+ // Not everything in the status bar is an event. The selection count
+ // describes what is true right now and must persist while it stays true;
+ // expiring it would undo the 0.8.0 discoverability work.
+ const Config config;
+ MainWindow window(config);
+
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *view = window.findChild<QTableView *>();
+ QVERIFY(view);
+ auto *status = window.findChild<QLabel *>(QStringLiteral("statusMessage"));
+ QVERIFY(status);
+ auto *timer = window.findChild<QTimer *>(QStringLiteral("statusTimer"));
+ QVERIFY(timer);
+
+ model->appendBatch({ makeThread(QStringLiteral("t1"), {}),
+ makeThread(QStringLiteral("t2"), {}) });
+ view->selectAll();
+
+ QVERIFY2(status->text().contains(QStringLiteral("2")),
+ "the selection count was not reported");
+ QVERIFY2(!timer->isActive(),
+ "the selection count armed the expiry timer; it is state, "
+ "not an event");
+}
+
// 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.