aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_mainwindow.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-29 18:18:33 +0200
committerDanilo M. <danix@danix.xyz>2026-09-29 18:18:33 +0200
commitbed9d291370b2b198b2d146c7081066b6dd70641 (patch)
tree23f1aa21358456c7df7b373fefed23538926b85b /tests/test_mainwindow.cpp
parent04e7db97ae421212614a6ed92807485f743966d3 (diff)
downloadqtmaildir-bed9d291370b2b198b2d146c7081066b6dd70641.tar.gz
qtmaildir-bed9d291370b2b198b2d146c7081066b6dd70641.zip
fix: find a launch's thread in any account, and name a miss
recoverStaleThread() runs thread:<id> in the account dropdown's scope, so a --thread or --message for a conversation in another account than the one the window was left on came back with no rows, and the list went blank with nothing said. A thread or message selector now switches the dropdown to All accounts first, unless the launch named an account with --account, in which case that scope is kept. For a message the switch waits until the id has resolved, so a message that does not exist leaves the dropdown alone. When the selector's own thread:<id> query returns no rows, the miss is named in the status bar, reusing the existing "No thread matched" and "No message matched" strings, and the view that was on screen when the launch arrived comes back: the dropdown, the bar's text and the list, re-run in the scope it was built in. The notice waits for that query to land, since the query writes its row count to the bar and would cover it. A refused thread id and an unresolved message restore the dropdown the same way, and re-run nothing, since the list never changed. The judgement is keyed to a flag the selector paths set after the query and runQuery() clears, so the stale-thread notice and double-click, which share recoverStaleThread(), behave as before. README and CHANGELOG describe the selectors as they now behave: --account alone opens its startup view, the other two look in every account unless narrowed, brackets are accepted, and a miss keeps the view the user had. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tests/test_mainwindow.cpp')
-rw-r--r--tests/test_mainwindow.cpp154
1 files changed, 154 insertions, 0 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index e89ad69..8ffa806 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -330,6 +330,10 @@ private slots:
void anEmptySelectorSetChangesNothing();
void aBracketedMessageSelectorOpensThatMessage();
void anAccountSelectorAloneShowsThatAccountsStartupView();
+ void aMessageSelectorFindsItsThreadInAnyAccount();
+ void aThreadSelectorFindsItsThreadInAnyAccount();
+ void aThreadOutsideTheGivenAccountIsAMissAndRestoresTheView();
+ void aThreadThatExistsNowhereIsAMissAndRestoresTheView();
void everyBuiltinFilterButtonCarriesAnIconAndItsText();
void theDraftsButtonIsAbsentWithoutADraftsFolder();
void aQueryInTheMenuCanActuallyBeRun();
@@ -18201,4 +18205,154 @@ void TestMainWindow::anAccountSelectorAloneShowsThatAccountsStartupView()
QCOMPARE(model->threadAt(0).subject, QStringLiteral("Work subject"));
}
+void TestMainWindow::aMessageSelectorFindsItsThreadInAnyAccount()
+{
+ // The window is scoped to work and the message lives in personal. The
+ // recovery runs thread:<id> in the dropdown's scope, so without --account
+ // the conversation was searched for in work alone, came back empty, and
+ // the list went blank with nothing said. With no account asked for, the
+ // conversation is found wherever it lives.
+ WorkerBackedWindow backed;
+ QVERIFY2(buildTwoAccounts(backed), qPrintable(backed.error()));
+
+ NotmuchWorker probe(backed.config().notmuchConfig());
+ const QString threadId =
+ probe.threadIdForTesting(QStringLiteral("id:p1@example.org"));
+ QVERIFY(!threadId.isEmpty());
+
+ MainWindow window(backed.config());
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *view = window.findChild<ThreadListView *>();
+ QVERIFY(view);
+ window.selectAccountForTesting(QStringLiteral("work"));
+ QCOMPARE(window.selectedAccountForTesting(), QStringLiteral("work"));
+
+ LaunchSelectors selectors;
+ selectors.messageId = QStringLiteral("p1@example.org");
+ window.applySelectors(selectors);
+
+ QTRY_VERIFY_WITH_TIMEOUT(view->currentIndex().isValid()
+ && model->threadFor(view->currentIndex())
+ .threadId
+ == threadId,
+ 15000);
+ // "All accounts" is the entry with no account key.
+ QCOMPARE(window.selectedAccountForTesting(), QString());
+}
+
+void TestMainWindow::aThreadSelectorFindsItsThreadInAnyAccount()
+{
+ // The --thread twin of the case above. A separate code path in
+ // applySelectors(), so it needs its own test: the message one passes with
+ // this path left scoped to work.
+ WorkerBackedWindow backed;
+ QVERIFY2(buildTwoAccounts(backed), qPrintable(backed.error()));
+
+ NotmuchWorker probe(backed.config().notmuchConfig());
+ const QString threadId =
+ probe.threadIdForTesting(QStringLiteral("id:p1@example.org"));
+ QVERIFY(!threadId.isEmpty());
+
+ MainWindow window(backed.config());
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *view = window.findChild<ThreadListView *>();
+ QVERIFY(view);
+ window.selectAccountForTesting(QStringLiteral("work"));
+ QCOMPARE(window.selectedAccountForTesting(), QStringLiteral("work"));
+
+ LaunchSelectors selectors;
+ selectors.threadId = threadId;
+ window.applySelectors(selectors);
+
+ QTRY_VERIFY_WITH_TIMEOUT(view->currentIndex().isValid()
+ && model->threadFor(view->currentIndex())
+ .threadId
+ == threadId,
+ 15000);
+ QCOMPARE(window.selectedAccountForTesting(), QString());
+}
+
+/// Waits for the startup view to land and returns the query it ran, so a
+/// test can assert the same view came back after a miss.
+static QString settledStartupQuery(MainWindow &window, ThreadListModel *model)
+{
+ if (!QTest::qWaitFor([&]() { return model->rowCount(QModelIndex()) == 2; },
+ 15000))
+ return QString();
+ return window.lastRunQueryForTesting();
+}
+
+void TestMainWindow::aThreadOutsideTheGivenAccountIsAMissAndRestoresTheView()
+{
+ // --account work --thread <a personal thread>. The account was asked for,
+ // so the scope is kept and the thread is not found in it. That is a miss,
+ // and a miss names itself and leaves the user on the view they had rather
+ // than on an empty list.
+ WorkerBackedWindow backed;
+ QVERIFY2(buildTwoAccounts(backed), qPrintable(backed.error()));
+
+ NotmuchWorker probe(backed.config().notmuchConfig());
+ const QString threadId =
+ probe.threadIdForTesting(QStringLiteral("id:p1@example.org"));
+ QVERIFY(!threadId.isEmpty());
+
+ MainWindow window(backed.config());
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *status = window.findChild<QLabel *>(QStringLiteral("statusMessage"));
+ QVERIFY(status);
+
+ const QString before = settledStartupQuery(window, model);
+ QVERIFY(!before.isEmpty());
+ const quint64 generation = window.currentGenerationForTesting();
+
+ LaunchSelectors selectors;
+ selectors.account = QStringLiteral("work");
+ selectors.threadId = threadId;
+ window.applySelectors(selectors);
+
+ // The thread query DID run in the work scope, which is what makes this the
+ // out-of-scope case rather than one refused before reaching notmuch.
+ QVERIFY(window.currentGenerationForTesting() > generation);
+
+ QTRY_VERIFY_WITH_TIMEOUT(status->text().contains(threadId)
+ && window.lastRunQueryForTesting() == before
+ && model->rowCount(QModelIndex()) == 2,
+ 15000);
+ QCOMPARE(window.selectedAccountForTesting(), QString());
+}
+
+void TestMainWindow::aThreadThatExistsNowhereIsAMissAndRestoresTheView()
+{
+ // Well-formed hex, so it passes the syntax check and reaches notmuch, and
+ // matches nothing there. A stale link, which is the ordinary miss.
+ WorkerBackedWindow backed;
+ QVERIFY2(buildTwoAccounts(backed), qPrintable(backed.error()));
+
+ MainWindow window(backed.config());
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *status = window.findChild<QLabel *>(QStringLiteral("statusMessage"));
+ QVERIFY(status);
+
+ const QString before = settledStartupQuery(window, model);
+ QVERIFY(!before.isEmpty());
+
+ const QString missing = QStringLiteral("0123456789abcdef");
+ LaunchSelectors selectors;
+ selectors.threadId = missing;
+ window.applySelectors(selectors);
+
+ QTRY_VERIFY_WITH_TIMEOUT(status->text().contains(missing)
+ && window.lastRunQueryForTesting() == before
+ && model->rowCount(QModelIndex()) == 2,
+ 15000);
+ // Still a miss a moment later: the restored query landing must not stamp
+ // its row count over the notice.
+ QTest::qWait(200);
+ QVERIFY(status->text().contains(missing));
+}
+
#include "test_mainwindow.moc"