aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_mainwindow.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_mainwindow.cpp')
-rw-r--r--tests/test_mainwindow.cpp152
1 files changed, 152 insertions, 0 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index eb678eb..ea42223 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -82,6 +82,14 @@ class WorkerBackedWindow
public:
/// Builds the database, writes the config and loads it. Check isValid()
/// and error() before constructing the window.
+ /// Extra `[general]` keys, written verbatim as `key=value` lines. Set
+ /// before build(). Used for mark_read_delay_ms, where a test needs the
+ /// timer to fire promptly rather than after the two-second default.
+ void setGeneralKey(const QString &key, const QString &value)
+ {
+ m_extraGeneral.insert(key, value);
+ }
+
bool build()
{
if (!m_fixture.isValid()) {
@@ -113,6 +121,10 @@ public:
// went unnoticed as broken once already.
out << "[general]\n"
<< "notmuch_config=" << m_fixture.configPath() << "\n";
+ for (auto it = m_extraGeneral.cbegin();
+ it != m_extraGeneral.cend(); ++it) {
+ out << it.key() << "=" << it.value() << "\n";
+ }
}
file.close();
@@ -133,6 +145,7 @@ private:
QTemporaryDir m_confDir;
Config m_config;
QString m_error;
+ QMap<QString, QString> m_extraGeneral;
};
/// MainWindow is mostly wiring. Cases that need a real database opt into one
@@ -171,6 +184,8 @@ private slots:
void aWorkerBackedWindowReturnsRealThreads();
void selectingAThreadRootShowsItInTheMessagePane();
void anUnexpandedRootRendersOneMessageNotTheConversation();
+ void aFirstClickIntoAnUnfocusedListStillRenders();
+ void autoMarkReadTouchesOnlyTheMessageOnScreen();
void autoSyncIsNotArmedWhenDisabledOrWithNothingPending();
void autoSyncSkipsWhileABackgroundSyncIsRunning();
void aSuccessfulSyncRefreshesRatherThanRerunningTheQuery();
@@ -6403,4 +6418,141 @@ void TestMainWindow::anUnexpandedRootRendersOneMessageNotTheConversation()
"the pane rendered a conversation, not a single message");
}
+void TestMainWindow::aFirstClickIntoAnUnfocusedListStillRenders()
+{
+ // The user's report: in a fresh session, the first click on a thread does
+ // not show the message; clicking again does.
+ //
+ // setCurrentIndex() cannot reproduce this, which is why the earlier probe
+ // passed: it updates the selection model synchronously. A real click does
+ // not, and onThreadSelected returns early when the clicked row is not yet
+ // selected, a guard that stops QTreeView's focus housekeeping from opening
+ // and marking-read mail nobody looked at. This test clicks the viewport.
+ WorkerBackedWindow backed;
+ QVERIFY(backed.fixture().addMessage(
+ QStringLiteral("inbox"), QStringLiteral("root@example.org"),
+ QStringLiteral("A conversation"), QStringLiteral("sender@example.org"),
+ QStringLiteral("Fri, 14 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("The first message.")));
+ QVERIFY(backed.fixture().addMessage(
+ QStringLiteral("inbox"), QStringLiteral("reply@example.org"),
+ QStringLiteral("Re: A conversation"),
+ QStringLiteral("other@example.org"),
+ QStringLiteral("Fri, 14 Aug 2026 11:00:00 +0200"),
+ QStringLiteral("The reply."), true,
+ QStringLiteral("root@example.org")));
+ QVERIFY2(backed.build(), qPrintable(backed.error()));
+
+ MainWindow window(backed.config());
+ window.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&window));
+
+ QLineEdit *queryEdit =
+ window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
+ QVERIFY2(queryEdit, "no query bar");
+ auto *view = window.findChild<ThreadListView *>();
+ QVERIFY2(view, "no thread list view");
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY2(model, "no thread list model");
+ auto *pane = window.findChild<MessageView *>();
+ QVERIFY2(pane, "no message view");
+
+ queryEdit->setText(QStringLiteral("tag:inbox"));
+ queryEdit->returnPressed();
+ QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 1, 15000);
+
+ // Focus stays on the query bar, as it is after typing a query. This is the
+ // "fresh session" state the user described: the list has never been
+ // clicked and has no current row.
+ queryEdit->setFocus();
+ QVERIFY2(!view->currentIndex().isValid(),
+ "the list already had a current row: not a fresh list");
+ QVERIFY(pane->showingPlaceholder());
+
+ const QModelIndex root = model->index(0, 0, QModelIndex());
+ QVERIFY(root.isValid());
+ const QRect rect = view->visualRect(root);
+ QVERIFY2(rect.isValid() && rect.height() > 0,
+ "the row has no geometry to click");
+
+ // ONE click, the first one into a list that never had focus.
+ QTest::mouseClick(view->viewport(), Qt::LeftButton, Qt::NoModifier,
+ rect.center());
+
+ QTRY_VERIFY_WITH_TIMEOUT(!pane->showingPlaceholder(), 15000);
+}
+
+void TestMainWindow::autoMarkReadTouchesOnlyTheMessageOnScreen()
+{
+ // Reported by the user, and a real data defect rather than a cosmetic one:
+ // selecting an unexpanded thread root marked EVERY message in the thread
+ // read, including replies never displayed. maildir.synchronize_flags is
+ // on, so that rewrites filenames and reaches the server: mail the user has
+ // not seen stops being unread everywhere.
+ //
+ // It was coherent while a root click rendered the whole conversation. It
+ // stopped being coherent when that view was removed and a root began
+ // rendering one message, which is the change that made this urgent.
+ WorkerBackedWindow backed;
+ backed.setGeneralKey(QStringLiteral("mark_read_delay_ms"),
+ QStringLiteral("0"));
+ QVERIFY(backed.fixture().addMessage(
+ QStringLiteral("inbox"), QStringLiteral("root@example.org"),
+ QStringLiteral("A conversation"), QStringLiteral("sender@example.org"),
+ QStringLiteral("Fri, 14 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("The first message."), /*unread=*/true));
+ QVERIFY(backed.fixture().addMessage(
+ QStringLiteral("inbox"), QStringLiteral("reply@example.org"),
+ QStringLiteral("Re: A conversation"),
+ QStringLiteral("other@example.org"),
+ QStringLiteral("Fri, 14 Aug 2026 11:00:00 +0200"),
+ QStringLiteral("The reply."), /*unread=*/true,
+ QStringLiteral("root@example.org")));
+ QVERIFY2(backed.build(), qPrintable(backed.error()));
+
+ MainWindow window(backed.config());
+
+ QLineEdit *queryEdit =
+ window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
+ QVERIFY2(queryEdit, "no query bar");
+ auto *view = window.findChild<ThreadListView *>();
+ QVERIFY2(view, "no thread list view");
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY2(model, "no thread list model");
+
+ queryEdit->setText(QStringLiteral("tag:inbox"));
+ queryEdit->returnPressed();
+ QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 1, 15000);
+
+ const QModelIndex root = model->index(0, 0, QModelIndex());
+ QVERIFY(root.isValid());
+ view->setCurrentIndex(root);
+
+ // Assert on WHAT THE WINDOW ASKED FOR, which is synchronous and exact.
+ //
+ // Reading tags back does not work here, and three separate attempts failed
+ // vacuously before this. TagsRole returns an empty list for a message row
+ // by design (the tag strip is a thread-wide band). MessageOwnTagsRole
+ // subtracts the parent thread's tags AND drops any tag drawn as a mark,
+ // and `unread` is one, so it can never contain it. And the raw node tags
+ // are not refreshed until onTagsApplied() confirms the write, which lands
+ // after this test's window: the model reports pre-write tags whichever
+ // scope was used. Each of those passed against the unfixed code.
+ //
+ // The two scopes reach the worker through different entry points, and the
+ // window records which one it used. That is the actual difference the fix
+ // makes.
+ QTRY_VERIFY_WITH_TIMEOUT(
+ !window.pendingMessageIdsForTesting().isEmpty()
+ || !window.pendingThreadIdsForTesting().isEmpty(),
+ 15000);
+
+ QVERIFY2(window.pendingThreadIdsForTesting().isEmpty(),
+ "the auto mark-read was sent for the whole THREAD: every reply "
+ "loses `unread`, including messages never displayed, and "
+ "maildir.synchronize_flags carries that to the server");
+ QCOMPARE(window.pendingMessageIdsForTesting(),
+ QStringList{ QStringLiteral("root@example.org") });
+}
+
#include "test_mainwindow.moc"