diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-29 18:18:33 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-29 18:18:33 +0200 |
| commit | bed9d291370b2b198b2d146c7081066b6dd70641 (patch) | |
| tree | 23f1aa21358456c7df7b373fefed23538926b85b /tests | |
| parent | 04e7db97ae421212614a6ed92807485f743966d3 (diff) | |
| download | qtmaildir-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')
| -rw-r--r-- | tests/test_mainwindow.cpp | 154 |
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" |
