aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-03 14:57:11 +0200
committerDanilo M. <danix@danix.xyz>2026-08-04 12:53:23 +0200
commit85bb759c46aef929beb66613159e4b23fcaf75fb (patch)
treeef8c08db29a7ce6b4052e6533c4f66b8f5a258bf /tests
parentc40cedf5250514cdf0d4a82aac71158df2750c12 (diff)
downloadqtmaildir-85bb759c46aef929beb66613159e4b23fcaf75fb.tar.gz
qtmaildir-85bb759c46aef929beb66613159e4b23fcaf75fb.zip
feat: show that a tag action landed
Selecting a thread and hitting Delete changed nothing on screen, so there was no way to tell the action had stuck. The tag was always applied: applyTagChange() emitted dataChanged across the row, and the Tags column did update. But Subject was set to stretch while Tags came after it, so Subject took all free width and pushed Tags out of view. The feedback lived in the one column that could not be seen. Columns are now Tags, Date, From, Subject, with Subject stretching last so nothing can be pushed off the right edge. A thread tagged deleted or spam fills its whole row, muted red or orange with white struck-through text, through the background, foreground and font roles, so no cue depends on one column remaining visible. Strike-through accompanies the fill on purpose: it survives a theme that overrides backgrounds and reads without colour. Bold for unread still composes with it. Archive adds no tag, so an archived row is left unstyled for now.
Diffstat (limited to 'tests')
-rw-r--r--tests/test_threadlistmodel.cpp102
1 files changed, 102 insertions, 0 deletions
diff --git a/tests/test_threadlistmodel.cpp b/tests/test_threadlistmodel.cpp
index 24ba9e2..98c477c 100644
--- a/tests/test_threadlistmodel.cpp
+++ b/tests/test_threadlistmodel.cpp
@@ -33,6 +33,11 @@ private slots:
void reportsSubjectAndAuthors();
void subjectShowsMessageCountOnlyForRealThreads();
void unreadThreadsRenderBold();
+ void tagsAreTheFirstColumnAndSubjectTheLast();
+ void deletedThreadsAreRedAndStruckThrough();
+ void spamThreadsAreOrangeAndStruckThrough();
+ void doomedStylingCoversEveryColumn();
+ void ordinaryThreadsCarryNoRowColour();
void threadIdIsReachableFromAnIndex();
void invalidIndexesReturnNothing();
void threadAtOutOfRangeIsSafe();
@@ -153,6 +158,103 @@ void TestThreadListModel::unreadThreadsRenderBold()
QVERIFY(unreadFont.value<QFont>().bold());
}
+void TestThreadListModel::tagsAreTheFirstColumnAndSubjectTheLast()
+{
+ // Subject stretches to fill the view, so whatever sits after it is pushed
+ // off-screen. Tags used to be there, which is why acting on a thread
+ // looked like it did nothing: the only column that changed was invisible.
+ QCOMPARE(ThreadListModel::TagsColumn, 0);
+ QCOMPARE(ThreadListModel::SubjectColumn, ThreadListModel::ColumnCount - 1);
+
+ ThreadListModel model;
+ model.appendBatch({ makeThread(QStringLiteral("t1"), QStringLiteral("hello")) });
+ QCOMPARE(model.headerData(ThreadListModel::TagsColumn, Qt::Horizontal,
+ Qt::DisplayRole).toString(),
+ QStringLiteral("Tags"));
+ QCOMPARE(model.headerData(ThreadListModel::SubjectColumn, Qt::Horizontal,
+ Qt::DisplayRole).toString(),
+ QStringLiteral("Subject"));
+}
+
+void TestThreadListModel::deletedThreadsAreRedAndStruckThrough()
+{
+ ThreadListModel model;
+ ThreadSummary thread = makeThread(QStringLiteral("t1"), QStringLiteral("doomed"));
+ thread.tags = QStringList{ QStringLiteral("inbox") };
+ model.appendBatch({ thread });
+
+ const QModelIndex subject = model.index(0, ThreadListModel::SubjectColumn);
+ QVERIFY(!model.data(subject, Qt::BackgroundRole).isValid());
+
+ model.applyTagChange(QStringLiteral("t1"), { QStringLiteral("deleted") }, {});
+
+ const QVariant background = model.data(subject, Qt::BackgroundRole);
+ QVERIFY(background.isValid());
+ QCOMPARE(background.value<QBrush>().color(), ThreadListModel::deletedColour());
+
+ // White text on the fill, and struck through so the state reads even in a
+ // screenshot with the colours stripped.
+ QCOMPARE(model.data(subject, Qt::ForegroundRole).value<QBrush>().color(),
+ QColor(Qt::white));
+ QVERIFY(model.data(subject, Qt::FontRole).value<QFont>().strikeOut());
+}
+
+void TestThreadListModel::spamThreadsAreOrangeAndStruckThrough()
+{
+ ThreadListModel model;
+ ThreadSummary thread = makeThread(QStringLiteral("t1"), QStringLiteral("junk"));
+ thread.tags = QStringList{ QStringLiteral("inbox") };
+ model.appendBatch({ thread });
+
+ model.applyTagChange(QStringLiteral("t1"), { QStringLiteral("spam") }, {});
+
+ const QModelIndex subject = model.index(0, ThreadListModel::SubjectColumn);
+ QCOMPARE(model.data(subject, Qt::BackgroundRole).value<QBrush>().color(),
+ ThreadListModel::spamColour());
+ QVERIFY(model.data(subject, Qt::FontRole).value<QFont>().strikeOut());
+
+ // Spam and deleted must be distinguishable, not two shades of one colour.
+ QVERIFY(ThreadListModel::spamColour() != ThreadListModel::deletedColour());
+}
+
+void TestThreadListModel::doomedStylingCoversEveryColumn()
+{
+ // A cue on one column would vanish the moment that column scrolled out of
+ // view, which is the bug this whole change exists to fix.
+ ThreadListModel model;
+ ThreadSummary thread = makeThread(QStringLiteral("t1"), QStringLiteral("doomed"));
+ thread.tags = QStringList{ QStringLiteral("inbox") };
+ model.appendBatch({ thread });
+
+ model.applyTagChange(QStringLiteral("t1"), { QStringLiteral("deleted") }, {});
+
+ for (int column = 0; column < ThreadListModel::ColumnCount; ++column) {
+ const QModelIndex index = model.index(0, column);
+ QVERIFY2(model.data(index, Qt::BackgroundRole).isValid(),
+ qPrintable(QStringLiteral("column %1 has no background").arg(column)));
+ QVERIFY2(model.data(index, Qt::FontRole).value<QFont>().strikeOut(),
+ qPrintable(QStringLiteral("column %1 is not struck through").arg(column)));
+ }
+}
+
+void TestThreadListModel::ordinaryThreadsCarryNoRowColour()
+{
+ // Undo has to restore the plain look, not merely drop the tag.
+ ThreadListModel model;
+ ThreadSummary thread = makeThread(QStringLiteral("t1"), QStringLiteral("normal"));
+ thread.tags = QStringList{ QStringLiteral("inbox") };
+ model.appendBatch({ thread });
+
+ model.applyTagChange(QStringLiteral("t1"), { QStringLiteral("deleted") }, {});
+ model.applyTagChange(QStringLiteral("t1"), {}, { QStringLiteral("deleted") });
+
+ const QModelIndex subject = model.index(0, ThreadListModel::SubjectColumn);
+ QVERIFY(!model.data(subject, Qt::BackgroundRole).isValid());
+ QVERIFY(!model.data(subject, Qt::ForegroundRole).isValid());
+ const QVariant font = model.data(subject, Qt::FontRole);
+ QVERIFY(!font.isValid() || !font.value<QFont>().strikeOut());
+}
+
void TestThreadListModel::threadIdIsReachableFromAnIndex()
{
// The view hands MainWindow a QModelIndex; the worker needs a thread id.