aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-10 08:32:36 +0200
committerDanilo M. <danix@danix.xyz>2026-08-10 08:32:36 +0200
commit497c56a962512949d606c26ae5159621b58a5e7b (patch)
tree186f0886486a10a4626a07d6080def8d1d02f063 /tests
parentc56d826673ec1bbd821cf703e6ee12cbef1a7ffc (diff)
downloadqtmaildir-497c56a962512949d606c26ae5159621b58a5e7b.tar.gz
qtmaildir-497c56a962512949d606c26ae5159621b58a5e7b.zip
feat(view): compute a card's geometry with no painting
Split from the delegate deliberately. A delegate needs a live painter and an exposed view, which is what makes delegate tests fragile: viewport()->render() returns a blank image in several ordinary situations, and a probe reporting no ink is likelier broken than the code it tests. Every geometric claim about a card is made here, where a test is a function call. Three lines at a uniform height, so setUniformRowHeights(true) survives. Indent caps at depth 4 with qMin rather than a branch, so depth 5 and depth 50 land in the same place. The date is measured before the sender, so a long sender elides instead of painting over it. Two traps handled that a first pass gets wrong. QRect::right() is inclusive, so the right edge is carried as an exclusive one and everything sized from it lands where the padding constant says rather than a pixel short. And QFont::pointSizeF returns -1 for a font set in pixels, which qt6ct does, so smallFont branches on which unit the font actually carries instead of silently returning the card's own size.
Diffstat (limited to 'tests')
-rw-r--r--tests/CMakeLists.txt1
-rw-r--r--tests/test_cardlayout.cpp235
2 files changed, 236 insertions, 0 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index a7bb670..4ecc04d 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -41,6 +41,7 @@ add_qtmaildir_test(interceptor)
add_qtmaildir_test(htmlbuilder)
add_qtmaildir_test(notmuchworker)
add_qtmaildir_test(tagcolors)
+add_qtmaildir_test(cardlayout)
add_qtmaildir_test(threadlistmodel)
add_qtmaildir_test(mailsync)
add_qtmaildir_test(syncmonitor)
diff --git a/tests/test_cardlayout.cpp b/tests/test_cardlayout.cpp
new file mode 100644
index 0000000..1082f4c
--- /dev/null
+++ b/tests/test_cardlayout.cpp
@@ -0,0 +1,235 @@
+/*
+ * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "cardlayout.h"
+
+#include <QFont>
+#include <QTest>
+
+class TestCardLayout : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void everyCardIsTheSameHeight();
+ void threeLinesStackWithoutOverlapping();
+ void replyIndentsByDepth();
+ void indentStopsAtTheCap();
+ void expanderSitsOnTheSecondLine();
+ void expanderIsEmptyWithoutReplies();
+ void dateIsFlushRight();
+ void threadCardCarriesAnAccentBar();
+ void replyCardCarriesNoAccentBar();
+};
+
+namespace {
+
+CardLayout::Input threadInput()
+{
+ CardLayout::Input in;
+ in.isMessage = false;
+ in.depth = 0;
+ in.replyCount = 3;
+ return in;
+}
+
+CardLayout::Input replyInput(int depth)
+{
+ CardLayout::Input in;
+ in.isMessage = true;
+ in.depth = depth;
+ in.replyCount = 0;
+ return in;
+}
+
+} // namespace
+
+void TestCardLayout::everyCardIsTheSameHeight()
+{
+ const QFont font;
+ const int thread = CardLayout::heightFor(font);
+
+ // The uniform height is the whole reason setUniformRowHeights(true)
+ // survives this design, so it is asserted directly rather than inferred
+ // from two cards happening to look alike.
+ //
+ // Note what is NOT varied here: the tag list. CardLayout reserves line 3
+ // unconditionally and never sees the tags, which is exactly the property
+ // being asserted. A version of this test that passed a tag list in would
+ // be testing a parameter that does not exist.
+ const CardLayout threadCard =
+ CardLayout::compute(threadInput(), QRect(0, 0, 400, thread), font);
+ const CardLayout deepReply =
+ CardLayout::compute(replyInput(3), QRect(0, 0, 400, thread), font);
+ CardLayout::Input noRepliesIn = threadInput();
+ noRepliesIn.replyCount = 0;
+ const CardLayout noReplies =
+ CardLayout::compute(noRepliesIn, QRect(0, 0, 400, thread), font);
+
+ QCOMPARE(threadCard.totalHeight, thread);
+ QCOMPARE(deepReply.totalHeight, thread);
+ QCOMPARE(noReplies.totalHeight, thread);
+
+ // The third line exists on every card, including one with nothing to put
+ // there. That blank band is the cost the uniform height was bought with.
+ QCOMPARE(noReplies.tagRect.height(), threadCard.tagRect.height());
+}
+
+void TestCardLayout::threeLinesStackWithoutOverlapping()
+{
+ const QFont font;
+ const int h = CardLayout::heightFor(font);
+ const CardLayout card =
+ CardLayout::compute(threadInput(), QRect(0, 0, 400, h), font);
+
+ QVERIFY(card.senderRect.height() > 0);
+ QVERIFY(card.subjectRect.height() > 0);
+ QVERIFY(card.tagRect.height() > 0);
+
+ // Guard: these must actually be three stacked bands. A layout that
+ // collapsed them all to the same rect would satisfy any assertion that
+ // only checked they exist.
+ QVERIFY(card.senderRect.bottom() <= card.subjectRect.top());
+ QVERIFY(card.subjectRect.bottom() <= card.tagRect.top());
+ QVERIFY(card.tagRect.bottom() <= h);
+}
+
+void TestCardLayout::replyIndentsByDepth()
+{
+ const QFont font;
+ const int h = CardLayout::heightFor(font);
+ const QRect rect(0, 0, 400, h);
+
+ const CardLayout root = CardLayout::compute(threadInput(), rect, font);
+ const CardLayout d1 = CardLayout::compute(replyInput(1), rect, font);
+ const CardLayout d2 = CardLayout::compute(replyInput(2), rect, font);
+
+ QVERIFY(d1.contentLeft > root.contentLeft);
+ QVERIFY(d2.contentLeft > d1.contentLeft);
+
+ // One spine per depth level, so the count is the depth itself.
+ QCOMPARE(root.spines.size(), 0);
+ QCOMPARE(d1.spines.size(), 1);
+ QCOMPARE(d2.spines.size(), 2);
+
+ // Each spine runs the full height of the card, which is what makes an
+ // expansion read as one continuous block rather than as dashes.
+ for (const QRect &spine : d2.spines) {
+ QCOMPARE(spine.top(), rect.top());
+ QCOMPARE(spine.bottom(), rect.bottom());
+ }
+}
+
+void TestCardLayout::indentStopsAtTheCap()
+{
+ const QFont font;
+ const int h = CardLayout::heightFor(font);
+ const QRect rect(0, 0, 400, h);
+
+ const CardLayout d4 = CardLayout::compute(replyInput(4), rect, font);
+ const CardLayout d5 = CardLayout::compute(replyInput(5), rect, font);
+ const CardLayout d9 = CardLayout::compute(replyInput(9), rect, font);
+
+ QCOMPARE(d5.contentLeft, d4.contentLeft);
+ QCOMPARE(d9.contentLeft, d4.contentLeft);
+ QCOMPARE(d5.spines.size(), d4.spines.size());
+ QCOMPARE(d9.spines.size(), d4.spines.size());
+
+ // Guard: the cap must not be so low that it has already bitten at depth 3,
+ // which would make the three assertions above true for the wrong reason.
+ const CardLayout d3 = CardLayout::compute(replyInput(3), rect, font);
+ QVERIFY(d3.contentLeft < d4.contentLeft);
+}
+
+void TestCardLayout::expanderSitsOnTheSecondLine()
+{
+ const QFont font;
+ const int h = CardLayout::heightFor(font);
+ const CardLayout card =
+ CardLayout::compute(threadInput(), QRect(0, 0, 400, h), font);
+
+ QVERIFY(!card.expanderRect.isEmpty());
+ // It is the reply count, so it belongs on the line the reply count is on.
+ QVERIFY(card.expanderRect.top() >= card.subjectRect.top());
+ QVERIFY(card.expanderRect.bottom() <= card.subjectRect.bottom());
+ // And it is on the right, where the count is drawn, not in a left gutter.
+ QVERIFY(card.expanderRect.left() > 400 / 2);
+}
+
+void TestCardLayout::expanderIsEmptyWithoutReplies()
+{
+ const QFont font;
+ const int h = CardLayout::heightFor(font);
+ CardLayout::Input in = threadInput();
+ in.replyCount = 0;
+
+ const CardLayout card = CardLayout::compute(in, QRect(0, 0, 400, h), font);
+ QVERIFY(card.expanderRect.isEmpty());
+}
+
+void TestCardLayout::dateIsFlushRight()
+{
+ const QFont font;
+ const int h = CardLayout::heightFor(font);
+ const QRect rect(0, 0, 400, h);
+ const CardLayout card = CardLayout::compute(threadInput(), rect, font);
+
+ // Compared as exclusive edges. QRect::right() is inclusive (left + width -
+ // 1), so asserting card.dateRect.right() == rect.right() - kPaddingX
+ // demands a gap of kPaddingX - 1 pixels and is off by one against the
+ // padding the constant names.
+ QCOMPARE(card.dateRect.right() + 1, rect.right() + 1 - CardLayout::kPaddingX);
+ // The sender must stop before the date starts, or a long sender overwrites
+ // it. This is the assertion that fails if the two are laid out
+ // independently.
+ QVERIFY(card.senderRect.right() <= card.dateRect.left());
+}
+
+void TestCardLayout::threadCardCarriesAnAccentBar()
+{
+ const QFont font;
+ const int h = CardLayout::heightFor(font);
+ const QRect rect(0, 0, 400, h);
+ const CardLayout card = CardLayout::compute(threadInput(), rect, font);
+
+ QCOMPARE(card.accentRect.left(), rect.left());
+ QCOMPARE(card.accentRect.width(), CardLayout::kAccentWidth);
+ // Full height, so a run of cards from one account reads as a continuous
+ // edge rather than as dashes.
+ QCOMPARE(card.accentRect.top(), rect.top());
+ QCOMPARE(card.accentRect.bottom(), rect.bottom());
+
+ // Nothing may be drawn on top of the colour.
+ QVERIFY(card.contentLeft >= card.accentRect.right());
+}
+
+void TestCardLayout::replyCardCarriesNoAccentBar()
+{
+ const QFont font;
+ const int h = CardLayout::heightFor(font);
+ const CardLayout reply =
+ CardLayout::compute(replyInput(1), QRect(0, 0, 400, h), font);
+
+ // A reply's account is its thread's, stated once at the head. The spine
+ // carries the accent instead, so the gutter never holds two lines.
+ QVERIFY(reply.accentRect.isEmpty());
+ QCOMPARE(reply.spines.size(), 1);
+}
+
+QTEST_MAIN(TestCardLayout)
+#include "test_cardlayout.moc"