aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_mainwindow.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-29 10:05:22 +0200
committerDanilo M. <danix@danix.xyz>2026-08-29 10:05:22 +0200
commitfeb15bcec261501eb49ad0de1fdc05a9992227c2 (patch)
treefe3228e437c78fe6f77d68c4408f15cdf9e9f71f /tests/test_mainwindow.cpp
parentf051dd657757bca3ac7e36454d7f2fc21f322f73 (diff)
downloadqtmaildir-feb15bcec261501eb49ad0de1fdc05a9992227c2.tar.gz
qtmaildir-feb15bcec261501eb49ad0de1fdc05a9992227c2.zip
fix: refresh the conversation dashboard after a write
Item 181, from the user's notes: "the thread dashboard doesn't update live with the modifications applied to the list pane. If I mark the thread as read, the dash still reports N unread". ThreadDashboard draws a ThreadDigest, which the worker builds from the index and which reached the pane only when a conversation was selected. A tag write updated the model optimistically and repainted the card beside it, and nothing touched the digest, so the pane went on reporting the unread count, the progress bar and the Waiting-for-you list the conversation had when it was opened. Reachable from the dashboard's own Mark all read button, which is the worst version of it: the number sits directly above the button that fails to move it. refreshDashboardDigest() re-asks the worker for the digest of the conversation on display, and returns at once when the pane is showing anything else. It bumps m_digestGeneration like any other request, so the guards in onThreadDigestLoaded() discard a reply that arrives after the user has moved on. No placeholder digest, unlike the selection path: the pane already holds this conversation, and blanking it to re-fill it would flicker the whole dashboard for a change to one number. Called from onTagsApplied(), where a write is CONFIRMED, and not from the two write funnels. The first attempt put it beside the optimistic model update by analogy with every other optimistic repaint, and that analogy does not hold here: the digest is rebuilt from the index, so a refresh queued beside the write reaches the worker before the write does and answers from the state before it. The test failed identically to no fix at all. Every write rather than a chosen subset, at the user's decision: narrowing it to the writes that change what the dashboard happens to draw today is a list the dashboard can outgrow silently, and this costs a round trip only while a conversation is on screen. Re-requested rather than edited in place, because the digest is a derived summary and recomputing it here would be a second place that has to agree with the worker about what a write did. The test is worker-backed over a real two-message conversation and is driven through the mark_all_read action rather than the private funnel, which is the path the dashboard's own button takes. It asserts the pane carries the unread state before the write, so the assertion after it means something. Suite: 42 of 43, with undoMovesTheMessageBack failing as it does on master (item 136).
Diffstat (limited to 'tests/test_mainwindow.cpp')
-rw-r--r--tests/test_mainwindow.cpp78
1 files changed, 78 insertions, 0 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index e2254f1..a8e6284 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -73,6 +73,7 @@
#include <QScrollBar>
#include "tagchip.h"
#include "tagstrip.h"
+#include "threaddashboard.h"
#include "threadlistmodel.h"
#include "threadlistview.h"
#include "notmuchfixture.h"
@@ -268,6 +269,7 @@ private slots:
void aMalformedAccountIsReportedWithoutBlockingTheConstructor();
void aWorkerBackedWindowReturnsRealThreads();
void aPurgeTakesTheRowsOutOfTheViewWithoutARefresh();
+ void theDashboardFollowsAWriteToTheConversationItShows();
// Compose and send, item 123 task 12.
void theMailRootComesFromTheConfigNotTheIndex();
@@ -9212,6 +9214,82 @@ void TestMainWindow::aWorkerBackedWindowReturnsRealThreads()
QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 1, 15000);
}
+/// Item 181, from the user: "the thread dashboard doesn't update live with the
+/// modifications applied to the list pane. If I mark the thread as read, the
+/// dash still reports N unread".
+///
+/// The dashboard draws a ThreadDigest, which the worker builds from the index
+/// and which arrived only when a conversation was SELECTED. A tag write moved
+/// the model and the card beside it and never touched the digest, so the pane
+/// went on reporting the unread count the conversation had when it was opened.
+///
+/// Reachable from the dashboard's own Mark all read button, which is the worst
+/// version of it: the user presses a button and the number above it does not
+/// move.
+void TestMainWindow::theDashboardFollowsAWriteToTheConversationItShows()
+{
+ WorkerBackedWindow backed;
+ QVERIFY(backed.fixture().addMessage(
+ QStringLiteral("inbox"), QStringLiteral("conv0@example.org"),
+ QStringLiteral("A conversation"), QStringLiteral("alice@example.org"),
+ // Friday, verified with `date -d 2026-08-14 +%A`. Qt::RFC2822Date
+ // validates the weekday against the date.
+ QStringLiteral("Fri, 14 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("Root."), true));
+ QVERIFY(backed.fixture().addMessage(
+ QStringLiteral("inbox"), QStringLiteral("conv1@example.org"),
+ QStringLiteral("Re: A conversation"), QStringLiteral("bob@example.org"),
+ QStringLiteral("Sat, 15 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("Reply."), true, QStringLiteral("conv0@example.org")));
+ QVERIFY2(backed.build(), qPrintable(backed.error()));
+
+ MainWindow window(backed.config());
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *view = window.findChild<QTreeView *>();
+ QVERIFY(view);
+ auto *messageView = window.findChild<MessageView *>();
+ QVERIFY(messageView);
+ ThreadDashboard *dashboard = messageView->dashboard();
+ QVERIFY(dashboard);
+
+ auto *queryEdit =
+ window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
+ QVERIFY(queryEdit);
+ queryEdit->setText(QStringLiteral("tag:inbox"));
+ queryEdit->returnPressed();
+ QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 1, 15000);
+
+ const QModelIndex row = model->index(0, 0, {});
+ QVERIFY2(model->isConversationRow(row),
+ "the fixture is not a conversation, so this test cannot see the "
+ "dashboard at all");
+
+ view->setCurrentIndex(row);
+
+ // The digest is a worker round trip, so wait for the pane to actually
+ // carry the conversation's state before asserting anything about it. Not a
+ // fixed wait: that passes when the digest never arrives.
+ QTRY_VERIFY_WITH_TIMEOUT(messageView->showingDashboard(), 15000);
+ QTRY_VERIFY_WITH_TIMEOUT(!dashboard->showingAllCaughtUp(), 15000);
+ QVERIFY2(dashboard->unreadCountShown() == 2,
+ qPrintable(QStringLiteral("expected 2 unread listed, got %1")
+ .arg(dashboard->unreadCountShown())));
+
+ // Driven through the ACTION rather than the private funnel, which is both
+ // the only route from here and the better assertion: it is the path the
+ // dashboard's own Mark all read button takes.
+ auto *markAllRead =
+ window.findChild<QAction *>(QStringLiteral("mark_all_read"));
+ QVERIFY(markAllRead);
+ markAllRead->trigger();
+
+ // The pane must follow it. Without the refresh the digest is the one built
+ // when the row was selected, and this stays at 2 for ever.
+ QTRY_VERIFY_WITH_TIMEOUT(dashboard->showingAllCaughtUp(), 15000);
+ QCOMPARE(dashboard->unreadCountShown(), 0);
+}
+
void TestMainWindow::aPurgeTakesTheRowsOutOfTheViewWithoutARefresh()
{
// Found by hand: the mail was destroyed correctly and the list went on