From 2c51c9653e4e045373120d50ce026f0a752a608f Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 28 Aug 2026 18:14:58 +0200 Subject: feat: add the thread dashboard A widget over a ThreadDigest: header, tags, counts, the unread list capped with a link to the rest, and an activity sparkline, scrolling under a pinned action strip. It invents no colours. ThreadDigest::unread is QVector rather than QVector. The dashboard draws rows for messages it never opens, which is what MessageNode exists for; MessageRef carries only what the pane needs once a message is already open and has no subject, sender or date. The struct's no-file-opening contract still holds, since all four of those fields are served from the notmuch index. --- tests/CMakeLists.txt | 1 + tests/test_threaddashboard.cpp | 74 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 tests/test_threaddashboard.cpp (limited to 'tests') diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1646028..e67d7e0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -72,6 +72,7 @@ add_qtmaildir_test(searchterm) add_qtmaildir_test(signatures) add_qtmaildir_test(busyindicator) add_qtmaildir_test(tagstrip) +add_qtmaildir_test(threaddashboard) add_qtmaildir_test(messagedetailsdialog) add_qtmaildir_test(markdownrenderer) add_qtmaildir_test(messagebuilder) diff --git a/tests/test_threaddashboard.cpp b/tests/test_threaddashboard.cpp new file mode 100644 index 0000000..66f187f --- /dev/null +++ b/tests/test_threaddashboard.cpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2026 Danilo M. + */ +#include + +#include "threaddashboard.h" +#include "threaddigest.h" + +class TestThreadDashboard : public QObject +{ + Q_OBJECT + +private slots: + void itNamesTheUnreadCountItWasGiven(); + void itSaysAllCaughtUpWhenNothingIsUnread(); + void itOffersMoreWhenTheListIsCapped(); + void itOffersNoMoreLinkWhenTheListIsComplete(); +}; + +static ThreadDigest digestWith(int unreadTotal, int shown) +{ + ThreadDigest d; + d.threadId = QStringLiteral("t1"); + d.totalCount = 44; + d.unreadTotal = unreadTotal; + for (int i = 0; i < shown; ++i) { + MessageNode ref; + ref.messageId = QStringLiteral("m%1@example.org").arg(i); + ref.subject = QStringLiteral("Re: something"); + // The avatar hashes the bare address, so a node without one exercises + // only the no-sender fallback. + ref.senderAddress = QStringLiteral("someone@example.org"); + d.unread.append(ref); + } + d.buckets = QVector(ThreadDigest::kBuckets, 1); + return d; +} + +void TestThreadDashboard::itNamesTheUnreadCountItWasGiven() +{ + ThreadDashboard dashboard; + dashboard.setDigest(digestWith(3, 3)); + QCOMPARE(dashboard.unreadCountShown(), 3); + QVERIFY(!dashboard.showingAllCaughtUp()); +} + +void TestThreadDashboard::itSaysAllCaughtUpWhenNothingIsUnread() +{ + ThreadDashboard dashboard; + dashboard.setDigest(digestWith(0, 0)); + QVERIFY2(dashboard.showingAllCaughtUp(), + "a fully read thread shows an empty Waiting for you block rather " + "than saying so"); +} + +void TestThreadDashboard::itOffersMoreWhenTheListIsCapped() +{ + // 20 unread, 5 shown: the link has to name the 15 the pane cannot list, or + // the dashboard under-reports exactly when the number matters most. + ThreadDashboard dashboard; + dashboard.setDigest(digestWith(20, ThreadDigest::kUnreadShown)); + QCOMPARE(dashboard.hiddenUnreadCount(), 15); +} + +void TestThreadDashboard::itOffersNoMoreLinkWhenTheListIsComplete() +{ + ThreadDashboard dashboard; + dashboard.setDigest(digestWith(4, 4)); + QCOMPARE(dashboard.hiddenUnreadCount(), 0); +} + +QTEST_MAIN(TestThreadDashboard) +#include "test_threaddashboard.moc" -- cgit v1.2.3