summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-10 08:53:13 +0200
committerDanilo M. <danix@danix.xyz>2026-08-10 08:53:13 +0200
commit91311854173c06b5007302341fcf1840585d9d37 (patch)
tree0dfc0d8c8fc2d4fe5622870bcfd91a5d20d8f8d7 /tests
parentb59b9ec616fb5c965cfd32385879162989589bb6 (diff)
downloadqtmaildir-91311854173c06b5007302341fcf1840585d9d37.tar.gz
qtmaildir-91311854173c06b5007302341fcf1840585d9d37.zip
feat(ui): let the user choose newest or oldest first
Two entries, straight to notmuch. This adds a feature rather than replacing one: the column header was decorative and nothing implemented click-to-sort, so removing the header with the grid lost nothing. Stored in uistate.conf, never in the hand-edited config, and range-guarded on read: a stale file can hold anything, which is the lesson item 58 recorded. SortOrder needed qRegisterMetaType despite carrying Q_ENUM. Q_ENUM gives the enum a meta-object entry, not a metatype registered under the name invokeMethod resolves, so the queued runQuery would have dropped its sort argument at runtime and every query would have silently run newest-first. Nothing in the suite exercises a real worker thread, so this was asserted directly rather than left to a warning nobody would see. It is registered beside the type rather than in MainWindow's constructor: a first attempt put it there and passed only because the test that catches it never constructs a MainWindow. The account dropdown's entries now carry their account's colour as a swatch, which is what makes the accent bar on a card mean anything: a colour down a card's edge says nothing until something maps it to a name. Raw colour here rather than the blended line colour, since a swatch is a filled patch like a chip rather than a thin line. Its test builds its own two-account config: reading the environment's made it SKIP wherever no accounts are configured, which is a test that asserts nothing while reporting success.
Diffstat (limited to 'tests')
-rw-r--r--tests/test_mainwindow.cpp90
-rw-r--r--tests/test_notmuchworker.cpp22
2 files changed, 112 insertions, 0 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index b3511ec..cdb08ca 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -51,6 +51,7 @@
#include <QImage>
#include <QPainter>
+#include <QComboBox>
#include <QScrollBar>
#include "tagchip.h"
#include "threadlistmodel.h"
@@ -107,6 +108,8 @@ private slots:
void nextThreadLeavesTheLastReply();
void altDownSkipsReplies();
void bothThreadStepBindingsReachTheAction();
+ void sortChoiceSurvivesRestart();
+ void accountEntriesCarryTheirColour();
void replyRowsKeepTheirTextUnderTheThreadLine();
void clickingTheExpanderTogglesTheThread();
void selectingAMessageRowTargetsThatMessageNotItsThread();
@@ -773,6 +776,93 @@ void TestMainWindow::bothThreadStepBindingsReachTheAction()
}
}
+void TestMainWindow::sortChoiceSurvivesRestart()
+{
+ QStandardPaths::setTestModeEnabled(true);
+ QFile::remove(MainWindow::uiStatePath());
+
+ {
+ const Config config;
+ MainWindow window(config);
+ auto *sort = window.findChild<QComboBox *>(QStringLiteral("sortOrder"));
+ QVERIFY(sort);
+ QCOMPARE(sort->count(), 2);
+ QCOMPARE(sort->currentIndex(), 0); // Newest first by default.
+ sort->setCurrentIndex(1);
+ window.close();
+ }
+
+ const Config config;
+ MainWindow second(config);
+ auto *sort = second.findChild<QComboBox *>(QStringLiteral("sortOrder"));
+ QVERIFY(sort);
+ QCOMPARE(sort->currentIndex(), 1);
+
+ // A stale or hand-edited file can hold anything, which is the lesson item
+ // 58 recorded: an out-of-range value must fall back rather than select a
+ // row that does not exist.
+ {
+ QSettings state(MainWindow::uiStatePath(), QSettings::IniFormat);
+ state.setValue(QStringLiteral("threadlist/sortOrder"), 47);
+ }
+ MainWindow third(config);
+ auto *thirdSort = third.findChild<QComboBox *>(QStringLiteral("sortOrder"));
+ QCOMPARE(thirdSort->currentIndex(), 0);
+
+ QFile::remove(MainWindow::uiStatePath());
+ QStandardPaths::setTestModeEnabled(false);
+}
+
+void TestMainWindow::accountEntriesCarryTheirColour()
+{
+ // Its own config, not the environment's. Reading the real one made this
+ // SKIP wherever no accounts are configured, which is a test that asserts
+ // nothing while reporting success.
+ QTemporaryDir dir;
+ const QString path = dir.filePath(QStringLiteral("qtmaildir.conf"));
+ {
+ QSettings s(path, QSettings::IniFormat);
+ s.beginGroup(QStringLiteral("account.work"));
+ s.setValue(QStringLiteral("maildir"), QStringLiteral("work"));
+ s.setValue(QStringLiteral("color"), QStringLiteral("#3d7fd1"));
+ s.endGroup();
+ s.beginGroup(QStringLiteral("account.personal"));
+ s.setValue(QStringLiteral("maildir"), QStringLiteral("personal"));
+ s.endGroup();
+ }
+
+ Config config;
+ config.load(path);
+ QCOMPARE(config.accounts().size(), 2);
+
+ MainWindow window(config);
+ auto *box = window.findChild<QComboBox *>(QStringLiteral("accountBox"));
+ QVERIFY(box);
+ QCOMPARE(box->count(), 3);
+
+ // "All accounts" is not an account and carries no swatch.
+ QVERIFY(!box->itemData(0, Qt::DecorationRole).isValid());
+
+ // Every real account does, including the one with no color= key:
+ // colourFor() never fails, deriving a stable colour from the tag name, so
+ // adding an account and forgetting to colour it degrades to something
+ // usable rather than to nothing.
+ QSet<QRgb> seen;
+ for (int i = 1; i < box->count(); ++i) {
+ const QVariant swatch = box->itemData(i, Qt::DecorationRole);
+ QVERIFY2(swatch.isValid(),
+ qPrintable(QStringLiteral("account %1 carries no swatch")
+ .arg(box->itemText(i))));
+ const QColor colour = swatch.value<QColor>();
+ QVERIFY(colour.isValid());
+ seen.insert(colour.rgb());
+ }
+
+ // Guard: two accounts sharing one colour would make the swatches useless
+ // as a key to the accent bars, and would let a broken lookup pass.
+ QCOMPARE(seen.size(), 2);
+}
+
void TestMainWindow::cardsNeverScrollSideways()
{
const Config config;
diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp
index 3342013..88dcf0b 100644
--- a/tests/test_notmuchworker.cpp
+++ b/tests/test_notmuchworker.cpp
@@ -40,6 +40,7 @@ private slots:
void unreadableConfigEmitsError();
void queryPassesGenerationThrough();
void oldestFirstReversesTheOrder();
+ void theSortOrderCrossesAQueuedCall();
void loadThreadReturnsMessagesOldestFirst();
void loadThreadMarksMatchedMessages();
@@ -346,6 +347,27 @@ void TestNotmuchWorker::oldestFirstReversesTheOrder()
QCOMPARE(oldest.last().threadId, newest.first().threadId);
}
+void TestNotmuchWorker::theSortOrderCrossesAQueuedCall()
+{
+ // MainWindow reaches the worker with invokeMethod(..., QueuedConnection)
+ // across a thread boundary, and a Q_ARG whose type the meta-object system
+ // does not know FAILS AT RUNTIME with a warning, not at compile time. So
+ // the enum's registration is asserted here rather than assumed from Q_ENUM.
+ QVERIFY2(QMetaType::fromName("NotmuchWorker::SortOrder").isValid(),
+ "SortOrder is not a registered metatype, so the queued runQuery "
+ "call will drop its sort argument at runtime");
+
+ NotmuchWorker worker(m_fixture.configPath());
+ QSignalSpy ready(&worker, &NotmuchWorker::threadsReady);
+
+ // The real call shape, invoked by NAME exactly as MainWindow does.
+ QVERIFY(QMetaObject::invokeMethod(
+ &worker, "runQuery", Qt::DirectConnection,
+ Q_ARG(QString, QStringLiteral("*")), Q_ARG(quint64, 1),
+ Q_ARG(NotmuchWorker::SortOrder, NotmuchWorker::OldestFirst)));
+ QCOMPARE(ready.size(), 1);
+}
+
void TestNotmuchWorker::loadThreadReturnsMessagesOldestFirst()
{
const QString threadId = threadIdOf(QStringLiteral("Release notes"));