aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_mainwindow.cpp124
1 files changed, 123 insertions, 1 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index 66f11c0..0dfd834 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -547,6 +547,7 @@ private slots:
void notSpamIsOfferedInTheSpamView();
void notSpamIsAbsentOnAReplyRow();
void notSpamIsHiddenOutsideTheSpamFolder();
+ void notSpamThreadMovesEveryMessageHome();
void undoOfNotSpamReturnsTheFileToTheSpamFolder();
// ComposeWindow, item 123. These need a window but no worker: the composer
@@ -12818,6 +12819,16 @@ void TestMainWindow::deleteThreadMovesEveryMessageAndRepaintsTheRootCard()
QVERIFY(!folderHasMessageFile(trash, QStringLiteral("dt0.example.org")));
QVERIFY(!folderHasMessageFile(trash, QStringLiteral("dt1.example.org")));
QVERIFY(!folderHasMessageFile(trash, QStringLiteral("dt2.example.org")));
+
+ // And the `inbox` tag came back with each message, not only the file. The
+ // message-scoped restore always did this; the thread-scoped one did not,
+ // because it passed an empty add list. Item 201 folded the two routes into
+ // one implementation, and this is the property that was silently missing:
+ // the conversation sat in the inbox FOLDER with no `inbox` tag, so the
+ // Inbox view could not see it until the next hook run.
+ QTRY_VERIFY_WITH_TIMEOUT(
+ notmuchCount(cfg, thread + QStringLiteral(" and tag:inbox")) == 3,
+ 15000);
}
void TestMainWindow::aFolderNameWithASpaceSurvivesTheRoundTrip()
@@ -17468,15 +17479,24 @@ void TestMainWindow::notSpamIsAbsentOnAReplyRow()
root.messageId = QStringLiteral("m1");
root.threadId = QStringLiteral("t1");
root.depth = 0;
+ root.filePath = QStringLiteral("acct/spam/cur/1:2,S");
MessageNode reply;
reply.messageId = QStringLiteral("m2");
reply.threadId = QStringLiteral("t1");
reply.depth = 1;
+ // A REAL spam path, so the predicate answers true for this row and the
+ // ONLY thing that can hide Not spam is the reply guard. Without it the
+ // empty path made everySelectedRowIsInAFolder() return false and the test
+ // would pass against a missing guard.
+ reply.filePath = QStringLiteral("acct/spam/cur/2:2,S");
model->setThreadMessages(QStringLiteral("t1"), { root, reply });
const QModelIndex thread = model->index(0, 0, QModelIndex());
view->expand(thread);
- const QModelIndex replyRow = model->index(0, 0, thread);
+ // Child 1, not child 0: since item 177 a conversation lists its FIRST
+ // message as a child too, so child 0 is the root message and child 1 is
+ // the reply whose row is under test.
+ const QModelIndex replyRow = model->index(1, 0, thread);
view->selectionModel()->select(
replyRow, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
view->setCurrentIndex(replyRow);
@@ -17609,4 +17629,106 @@ void TestMainWindow::undoOfNotSpamReturnsTheFileToTheSpamFolder()
QCOMPARE(notmuchCount(cfg, QStringLiteral("id:nsundo@example.org")), 1);
}
+void TestMainWindow::notSpamThreadMovesEveryMessageHome()
+{
+ // The thread-scoped half of Not spam, mirroring
+ // deleteThreadMovesEveryMessageAndRepaintsTheRootCard: a conversation row
+ // moves its whole conversation, each message back to its OWN origin. The
+ // single-message test cannot see notSpamThreads(), the optimistic
+ // applyTagChange, m_pendingThreadScope or the wholeThreadIds repaint.
+ WorkerBackedWindow backed;
+ QVERIFY(backed.fixture().addMessage(
+ QStringLiteral("acct/inbox"), QStringLiteral("nst0@example.org"),
+ QStringLiteral("NST root"), QStringLiteral("sender@example.org"),
+ QStringLiteral("Fri, 14 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("Root body.")));
+ QVERIFY(backed.fixture().addMessage(
+ QStringLiteral("acct/inbox"), QStringLiteral("nst1@example.org"),
+ QStringLiteral("Re: NST root"), QStringLiteral("other@example.org"),
+ QStringLiteral("Fri, 14 Aug 2026 11:00:00 +0200"),
+ QStringLiteral("Reply one."), true, QStringLiteral("nst0@example.org")));
+ QVERIFY(backed.fixture().addMessage(
+ QStringLiteral("acct/inbox"), QStringLiteral("nst2@example.org"),
+ QStringLiteral("Re: NST root"), QStringLiteral("third@example.org"),
+ QStringLiteral("Fri, 14 Aug 2026 12:00:00 +0200"),
+ QStringLiteral("Reply two."), true, QStringLiteral("nst0@example.org")));
+ QVERIFY2(backed.build(QStringLiteral("acct"), QStringLiteral("acct"),
+ QStringLiteral("Trash"), QStringLiteral("Spam")),
+ qPrintable(backed.error()));
+
+ MainWindow window(backed.config());
+ auto *model = window.findChild<ThreadListModel *>();
+ auto *view = window.findChild<ThreadListView *>();
+ auto *queryEdit =
+ window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
+ QVERIFY(model && view && queryEdit);
+
+ const QString root = backed.fixture().maildirPath();
+ const QString cfg = backed.fixture().configPath();
+ const QString spam = root + QStringLiteral("/acct/Spam/cur");
+ const QString thread = QStringLiteral("thread:{id:nst0@example.org}");
+
+ queryEdit->setText(QStringLiteral("tag:inbox"));
+ queryEdit->returnPressed();
+ QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 1, 15000);
+ QCOMPARE(notmuchCount(cfg, thread), 3);
+
+ // A conversation row, so the move is thread-scoped.
+ view->setCurrentIndex(model->index(0, 0, QModelIndex()));
+ window.findChild<QAction *>(QStringLiteral("spam"))->trigger();
+
+ QTRY_VERIFY_WITH_TIMEOUT(
+ folderHasMessageFile(spam, QStringLiteral("nst0.example.org"))
+ && folderHasMessageFile(spam, QStringLiteral("nst1.example.org"))
+ && folderHasMessageFile(spam, QStringLiteral("nst2.example.org")),
+ 15000);
+ QTRY_VERIFY_WITH_TIMEOUT(
+ notmuchCount(cfg, thread + QStringLiteral(" and tag:spam")) == 3,
+ 15000);
+ QCOMPARE(notmuchCount(cfg, thread
+ + QStringLiteral(" and "
+ "tag:\"moved-from:inbox\"")),
+ 3);
+
+ // Now Not spam on the same conversation row, from the Spam view.
+ queryEdit->setText(QStringLiteral("path:\"acct/Spam/**\""));
+ queryEdit->returnPressed();
+ QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 1, 15000);
+ view->setCurrentIndex(model->index(0, 0, QModelIndex()));
+ window.findChild<QAction *>(QStringLiteral("not_spam"))->trigger();
+
+ // Every message back in the inbox folder, none left in spam, each carrying
+ // the `inbox` tag its origin move stripped.
+ QTRY_VERIFY_WITH_TIMEOUT(
+ (folderHasMessageFile(root + QStringLiteral("/acct/inbox/cur"),
+ QStringLiteral("nst0.example.org"))
+ || folderHasMessageFile(root + QStringLiteral("/acct/inbox/new"),
+ QStringLiteral("nst0.example.org")))
+ && (folderHasMessageFile(root + QStringLiteral("/acct/inbox/cur"),
+ QStringLiteral("nst1.example.org"))
+ || folderHasMessageFile(root + QStringLiteral("/acct/inbox/new"),
+ QStringLiteral("nst1.example.org")))
+ && (folderHasMessageFile(root + QStringLiteral("/acct/inbox/cur"),
+ QStringLiteral("nst2.example.org"))
+ || folderHasMessageFile(root + QStringLiteral("/acct/inbox/new"),
+ QStringLiteral("nst2.example.org"))),
+ 15000);
+ QVERIFY(!folderHasMessageFile(spam, QStringLiteral("nst0.example.org")));
+ QVERIFY(!folderHasMessageFile(spam, QStringLiteral("nst1.example.org")));
+ QVERIFY(!folderHasMessageFile(spam, QStringLiteral("nst2.example.org")));
+
+ QCOMPARE(notmuchCount(cfg, thread), 3);
+ QTRY_VERIFY_WITH_TIMEOUT(
+ notmuchCount(cfg, thread + QStringLiteral(" and tag:spam")) == 0,
+ 15000);
+ QTRY_VERIFY_WITH_TIMEOUT(
+ notmuchCount(cfg, thread
+ + QStringLiteral(" and "
+ "tag:\"moved-from:inbox\"")) == 0,
+ 15000);
+ QTRY_VERIFY_WITH_TIMEOUT(
+ notmuchCount(cfg, thread + QStringLiteral(" and tag:inbox")) == 3,
+ 15000);
+}
+
#include "test_mainwindow.moc"