diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-04 19:54:40 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-04 19:54:40 +0200 |
| commit | bd1b907ba22464bb869bcc10cc9327ed74c44791 (patch) | |
| tree | c593b55f9a5fd4912104c33cddf59f894c9308aa /tests | |
| parent | a1f11aa861136fca3ec464e0a6268abeed1d8109 (diff) | |
| download | qtmaildir-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.cpp | 110 |
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. |
