diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | tests/test_cardlayout.cpp | 173 | ||||
| -rw-r--r-- | tests/test_marks.cpp | 286 | ||||
| -rw-r--r-- | tests/test_threadlistmodel.cpp | 44 |
4 files changed, 487 insertions, 17 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 13063e9..06cc739 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -42,6 +42,7 @@ add_qtmaildir_test(htmlbuilder) add_qtmaildir_test(notmuchworker) add_qtmaildir_test(tagcolors) add_qtmaildir_test(cardlayout) +add_qtmaildir_test(marks) add_qtmaildir_test(carddelegate) add_qtmaildir_test(threadlistmodel) add_qtmaildir_test(mailsync) diff --git a/tests/test_cardlayout.cpp b/tests/test_cardlayout.cpp index bce6a0f..f5f40ab 100644 --- a/tests/test_cardlayout.cpp +++ b/tests/test_cardlayout.cpp @@ -35,6 +35,10 @@ private slots: void expanderSitsOnTheSecondLine(); void expanderIsEmptyWithoutReplies(); void theExpanderReadsAsAPillWithAWord(); + void marksReserveTheirOwnSpaceRatherThanOverlappingTheSubject(); + void anAbsentMarkReservesNothing(); + void theFlagIndentsTheSubjectRatherThanSittingOnIt(); + void marksDoNotCollideWithEachOtherOrTheExpander(); void dateIsFlushRight(); void threadCardCarriesAnAccentBar(); void replyCardCarriesNoAccentBar(); @@ -228,15 +232,22 @@ void TestCardLayout::theExpanderReadsAsAPillWithAWord() // A bare "3" beside the subject reads as an unexplained number and gives // no hint that it can be clicked. The label carries the word, and the rect // carries padding for the pill drawn behind it. - QCOMPARE(CardLayout::expanderLabel(3, false), - QStringLiteral("\u25b8 3 replies")); - QCOMPARE(CardLayout::expanderLabel(3, true), - QStringLiteral("\u25be 3 replies")); + // + // NO triangle in the label since item 70: it is a drawn mark now, and a + // glyph left here would be a second triangle beside the drawn one. The + // label is the words alone, and the state no longer changes it. + QCOMPARE(CardLayout::expanderLabel(3, false), QStringLiteral("3 replies")); + QCOMPARE(CardLayout::expanderLabel(3, true), QStringLiteral("3 replies")); // Singular, because "1 replies" is the kind of detail that makes an // interface look unfinished. - QCOMPARE(CardLayout::expanderLabel(1, false), - QStringLiteral("\u25b8 1 reply")); + QCOMPARE(CardLayout::expanderLabel(1, false), QStringLiteral("1 reply")); + + // The glyphs are gone from the label entirely. Asserted rather than assumed, + // because a stray one would draw underneath the mark and look like a + // rendering fault rather than like a stale string. + QVERIFY(!CardLayout::expanderLabel(3, false).contains(QChar(0x25b8))); + QVERIFY(!CardLayout::expanderLabel(3, true).contains(QChar(0x25be))); const QFont font; const int h = CardLayout::heightFor(font); @@ -244,12 +255,17 @@ void TestCardLayout::theExpanderReadsAsAPillWithAWord() CardLayout::compute(threadInput(), QRect(0, 0, 400, h), font); const QFontMetrics small(CardLayout::smallFont(font)); - // The rect must hold the label AND its padding, or the pill's background - // is narrower than the text sitting on it. + // The rect must hold the label, the drawn triangle, the gap between them + // AND the padding, or the pill's background is narrower than what sits on + // it. The triangle's width came free from the text metrics while it was a + // glyph in the label; since item 70 it is reserved explicitly, and this is + // what would catch it being forgotten. QVERIFY2(card.expanderRect.width() >= small.horizontalAdvance(CardLayout::expanderLabel(3, false)) + + small.ascent() + CardLayout::kMarkGap + CardLayout::kPillPaddingX * 2, - "the expander rect is too narrow for its own label and padding"); + "the expander rect is too narrow for its label, its triangle and " + "its padding"); // And it must NOT change width when the card opens: a pill that resized on // click would shift the subject's elision under the pointer. @@ -259,6 +275,145 @@ void TestCardLayout::theExpanderReadsAsAPillWithAWord() QCOMPARE(expanded.expanderRect.width(), card.expanderRect.width()); } +void TestCardLayout::marksReserveTheirOwnSpaceRatherThanOverlappingTheSubject() +{ + // Item 70. The marks were glyphs INSIDE the subject string until then, so + // their width came free from the text metrics and no arrangement was + // needed. As drawn icons they occupy rects, and a subject sized as though + // they were absent runs underneath them. This is the assertion that would + // catch that, and it cannot be made anywhere else: a rendering probe over + // the delegate would show overlapping ink as a plausible-looking card. + const QFont font; + const int h = CardLayout::heightFor(font); + const QRect rect(0, 0, 400, h); + + CardLayout::Input bare = threadInput(); + CardLayout::Input marked = threadInput(); + marked.hasAttachment = true; + marked.passed = true; + marked.replied = true; + + const CardLayout without = CardLayout::compute(bare, rect, font); + const CardLayout with = CardLayout::compute(marked, rect, font); + + QVERIFY(!with.attachmentRect.isEmpty()); + QVERIFY(!with.passedRect.isEmpty()); + QVERIFY(!with.repliedRect.isEmpty()); + + // The subject gives up exactly the room the marks take. + QVERIFY2(with.subjectRect.width() < without.subjectRect.width(), + "the marks reserved no space, so the subject is sized as though " + "they were not there and its text runs underneath them"); + + // And every mark begins after the subject ends. Compared as exclusive + // edges: QRect::right() is inclusive, which is the trap this file already + // documents for the date. + const int subjectEnd = with.subjectRect.left() + with.subjectRect.width(); + QVERIFY2(with.attachmentRect.left() >= subjectEnd, + "the attachment mark overlaps the subject"); + QVERIFY2(with.passedRect.left() >= subjectEnd, "passed overlaps the subject"); + QVERIFY2(with.repliedRect.left() >= subjectEnd, + "replied overlaps the subject"); + + // Square, so nothing is drawn stretched. + QCOMPARE(with.attachmentRect.width(), with.attachmentRect.height()); +} + +void TestCardLayout::anAbsentMarkReservesNothing() +{ + // A card with no attachment must not leave a hole where the mark would be: + // the subject is the elastic part of line two and every reserved-but-unused + // pixel comes out of it. + const QFont font; + const int h = CardLayout::heightFor(font); + const QRect rect(0, 0, 400, h); + + const CardLayout card = CardLayout::compute(threadInput(), rect, font); + + QVERIFY(card.flagRect.isEmpty()); + QVERIFY(card.attachmentRect.isEmpty()); + QVERIFY(card.passedRect.isEmpty()); + QVERIFY(card.repliedRect.isEmpty()); + + // Guard: the same input WITH a mark must produce one, or the assertions + // above pass against a layout that never draws marks at all. + CardLayout::Input marked = threadInput(); + marked.hasAttachment = true; + QVERIFY(!CardLayout::compute(marked, rect, font).attachmentRect.isEmpty()); +} + +void TestCardLayout::theFlagIndentsTheSubjectRatherThanSittingOnIt() +{ + // The flag is the one mark on the LEFT, where its glyph was, so a flagged + // card still reads flagged from the left edge. + const QFont font; + const int h = CardLayout::heightFor(font); + const QRect rect(0, 0, 400, h); + + CardLayout::Input flagged = threadInput(); + flagged.flagged = true; + + const CardLayout plain = CardLayout::compute(threadInput(), rect, font); + const CardLayout marked = CardLayout::compute(flagged, rect, font); + + QVERIFY(!marked.flagRect.isEmpty()); + QCOMPARE(marked.flagRect.left(), marked.contentLeft); + + // The subject starts after the flag, rather than at contentLeft with the + // flag drawn over it. + QVERIFY2(marked.subjectRect.left() > plain.subjectRect.left(), + "the flag did not move the subject, so it is drawn on top of it"); + QVERIFY(marked.subjectRect.left() + >= marked.flagRect.left() + marked.flagRect.width()); +} + +void TestCardLayout::marksDoNotCollideWithEachOtherOrTheExpander() +{ + // All four marks at once on a card that also has an expander, which is the + // densest line two can get. Nothing may overlap anything. + const QFont font; + const int h = CardLayout::heightFor(font); + const QRect rect(0, 0, 400, h); + + CardLayout::Input in = threadInput(); + in.flagged = true; + in.hasAttachment = true; + in.passed = true; + in.replied = true; + + const CardLayout card = CardLayout::compute(in, rect, font); + + QVERIFY(!card.expanderRect.isEmpty()); + + // Left to right: flag, subject, attachment, passed, replied, expander. + const QList<QRect> ordered = { card.flagRect, card.subjectRect, + card.attachmentRect, card.passedRect, + card.repliedRect, card.expanderRect }; + for (int i = 0; i + 1 < ordered.size(); ++i) { + const QRect &left = ordered.at(i); + const QRect &right = ordered.at(i + 1); + QVERIFY2(left.left() + left.width() <= right.left(), + qPrintable(QStringLiteral("rect %1 (x %2 w %3) overlaps rect " + "%4 (x %5)") + .arg(i) + .arg(left.left()) + .arg(left.width()) + .arg(i + 1) + .arg(right.left()))); + } + + // And the whole line stays inside the card. + QVERIFY(card.repliedRect.left() + card.repliedRect.width() + <= card.expanderRect.left()); + QVERIFY(card.expanderRect.left() + card.expanderRect.width() + <= rect.right() + 1); + + // The subject survives at a usable width rather than being squeezed to + // nothing by four marks: they are small and fixed, it is the elastic part. + QVERIFY2(card.subjectRect.width() > 100, + "four marks left the subject with almost no room on a 400px card"); +} + void TestCardLayout::dateIsFlushRight() { const QFont font; diff --git a/tests/test_marks.cpp b/tests/test_marks.cpp new file mode 100644 index 0000000..2ed6580 --- /dev/null +++ b/tests/test_marks.cpp @@ -0,0 +1,286 @@ +/* + * qtmaildir - a Qt6 GUI for a local notmuch-indexed Maildir + * 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 "marks.h" + +#include <QImage> +#include <QPainter> +#include <QtTest> + +/// Counts pixels with any alpha at all. +/// +/// "Rendering probes lie" in CLAUDE.md is about probes over widgets, where a +/// blank result is more likely a broken probe than broken code. This one is +/// safe for the opposite reason: the input is a fixed SVG payload and a +/// transparent pixmap this test creates itself, with no widget, no exposure and +/// no viewport to come out empty. Every assertion below still states the ink it +/// expects to find before drawing a conclusion from ink it does not. +static int inkPixels(const QImage &image) +{ + int count = 0; + for (int y = 0; y < image.height(); ++y) { + for (int x = 0; x < image.width(); ++x) { + if (qAlpha(image.pixel(x, y)) > 0) + ++count; + } + } + return count; +} + +static QImage renderMark(Marks::Mark mark, int side = 64, + const QColor &color = Qt::black) +{ + return Marks::pixmap(mark, QSize(side, side), color).toImage(); +} + +class TestMarks : public QObject +{ + Q_OBJECT + +private slots: + void everyMarkHasAPayload(); + void everyMarkDrawsSomething(); + void marksAreRecolouredRatherThanShippedPerTheme(); + void theExpanderPairIsTheSameWeightInBothStates(); + void passedAndRepliedAreMirrorsOfEachOther(); + void aMarkIsDistinguishableFromEveryOther(); + void paintCentresTheMarkInItsRect(); + void anEmptySizeOrInvalidColourYieldsNothing(); +}; + +void TestMarks::everyMarkHasAPayload() +{ + // A missing case in the switch returns an empty QByteArray, which + // QSvgRenderer accepts and renders as nothing. That failure is silent + // everywhere else, so it is caught here first. + const QList<Marks::Mark> all = { + Marks::Mark::Attachment, Marks::Mark::Flagged, + Marks::Mark::Passed, Marks::Mark::Replied, + Marks::Mark::ExpanderCollapsed, Marks::Mark::ExpanderExpanded, + }; + + for (const Marks::Mark mark : all) { + const QByteArray payload = Marks::svg(mark); + QVERIFY2(!payload.isEmpty(), + qPrintable(QStringLiteral("mark %1 has no payload") + .arg(static_cast<int>(mark)))); + QVERIFY(payload.contains("<svg")); + // The recolouring in pixmap() depends on this: a payload that named a + // literal colour would ignore the palette and stay that colour on both + // themes. + QVERIFY2(payload.contains("currentColor"), + qPrintable(QStringLiteral("mark %1 does not paint with " + "currentColor, so it cannot be " + "recoloured") + .arg(static_cast<int>(mark)))); + } +} + +void TestMarks::everyMarkDrawsSomething() +{ + // The guard the rest of this file needs: a probe that cannot find ink where + // ink certainly exists is broken, and would pass every "differs from" + // assertion below by finding nothing anywhere. + const QList<QPair<Marks::Mark, QString>> all = { + { Marks::Mark::Attachment, QStringLiteral("attachment") }, + { Marks::Mark::Flagged, QStringLiteral("flagged") }, + { Marks::Mark::Passed, QStringLiteral("passed") }, + { Marks::Mark::Replied, QStringLiteral("replied") }, + { Marks::Mark::ExpanderCollapsed, QStringLiteral("expander-collapsed") }, + { Marks::Mark::ExpanderExpanded, QStringLiteral("expander-expanded") }, + }; + + for (const auto &[mark, name] : all) { + const QImage image = renderMark(mark); + QVERIFY2(!image.isNull(), qPrintable(name + QStringLiteral(" is null"))); + const int ink = inkPixels(image); + QVERIFY2(ink > 100, + qPrintable(QStringLiteral("%1 drew %2 ink pixels at 64x64, " + "which is a blank or near-blank " + "render") + .arg(name) + .arg(ink))); + } +} + +void TestMarks::marksAreRecolouredRatherThanShippedPerTheme() +{ + // One asset serves a light and a dark palette. The payload paints with + // currentColor, which QSvgRenderer renders BLACK rather than resolving, so + // without the SourceIn composite every mark would be black on both themes + // and invisible on a dark one. + const QImage light = renderMark(Marks::Mark::Flagged, 64, QColor(Qt::white)); + const QImage dark = renderMark(Marks::Mark::Flagged, 64, QColor(Qt::black)); + + QCOMPARE(inkPixels(light), inkPixels(dark)); // same shape + + // Find a pixel the shape actually covers and compare the colour there. + // Sampling a fixed coordinate would risk landing outside the star. + bool sampled = false; + for (int y = 0; y < light.height() && !sampled; ++y) { + for (int x = 0; x < light.width() && !sampled; ++x) { + if (qAlpha(light.pixel(x, y)) != 255) + continue; + const QRgb lit = light.pixel(x, y); + const QRgb unlit = dark.pixel(x, y); + QVERIFY2(qRed(lit) > 200 && qGreen(lit) > 200 && qBlue(lit) > 200, + "the white request did not produce a white mark"); + QVERIFY2(qRed(unlit) < 50 && qGreen(unlit) < 50 && qBlue(unlit) < 50, + "the black request did not produce a black mark"); + sampled = true; + } + } + QVERIFY2(sampled, "no fully opaque pixel found, so nothing was compared"); +} + +void TestMarks::theExpanderPairIsTheSameWeightInBothStates() +{ + // The expanded triangle is the collapsed one rotated 90 degrees about the + // centre, so neither state can read as heavier than the other. Asserted as + // equal ink rather than by eye, and it is the property most easily lost by + // hand-editing one of the two paths. + const int collapsed = inkPixels(renderMark(Marks::Mark::ExpanderCollapsed)); + const int expanded = inkPixels(renderMark(Marks::Mark::ExpanderExpanded)); + + QVERIFY2(collapsed > 0 && expanded > 0, "an expander drew nothing"); + + // Not exactly equal: antialiasing along a rotated edge differs by a few + // pixels. 2% is far tighter than any real weight difference would be. + const double ratio = double(qAbs(collapsed - expanded)) + / double(qMax(collapsed, expanded)); + QVERIFY2(ratio < 0.02, + qPrintable(QStringLiteral("expander states differ in weight: %1 " + "against %2 ink pixels") + .arg(collapsed) + .arg(expanded))); +} + +void TestMarks::passedAndRepliedAreMirrorsOfEachOther() +{ + // Item 69 wants these two to read as one pair. They are mirrors about + // x = 8, so mirroring one must reproduce the other; a hand edit to one + // alone would break the pairing while leaving both looking plausible. + const QImage passed = renderMark(Marks::Mark::Passed); + const QImage replied = renderMark(Marks::Mark::Replied); + + QVERIFY(inkPixels(passed) > 100); + + // Near-equal, not equal. These are mirrored CURVES, and the rasteriser + // antialiases a curve and its mirror slightly differently: measured 1383 + // against 1397 at 64x64, a 1% difference that says nothing about the + // shapes. The pixel-by-pixel comparison below is the assertion that would + // actually catch a broken pair; this one only rejects a gross weight + // difference. + const int passedInk = inkPixels(passed); + const int repliedInk = inkPixels(replied); + const double weightRatio = double(qAbs(passedInk - repliedInk)) + / double(qMax(passedInk, repliedInk)); + QVERIFY2(weightRatio < 0.02, + qPrintable(QStringLiteral("passed and replied differ in weight: " + "%1 against %2 ink pixels") + .arg(passedInk) + .arg(repliedInk))); + + const QImage mirrored = passed.mirrored(true, false); + QCOMPARE(mirrored.size(), replied.size()); + + // Compared on alpha rather than on exact pixels: mirroring resamples the + // antialiased edges, so a strict image equality would fail on a correct + // pair. A shape mismatch shows up as a large disagreeing area, not a few + // edge pixels. + int disagreeing = 0; + for (int y = 0; y < replied.height(); ++y) { + for (int x = 0; x < replied.width(); ++x) { + const int a = qAlpha(mirrored.pixel(x, y)) > 127 ? 1 : 0; + const int b = qAlpha(replied.pixel(x, y)) > 127 ? 1 : 0; + if (a != b) + ++disagreeing; + } + } + const double fraction = double(disagreeing) + / double(replied.width() * replied.height()); + QVERIFY2(fraction < 0.02, + qPrintable(QStringLiteral("passed mirrored does not match replied: " + "%1% of pixels disagree") + .arg(fraction * 100, 0, 'f', 1))); +} + +void TestMarks::aMarkIsDistinguishableFromEveryOther() +{ + // The defect the glyphs had: an unrenderable codepoint fell back to "*" for + // BOTH the star and the paperclip, so a flagged thread and one carrying an + // attachment looked identical. Whatever else changes about these marks, no + // two may render the same. + const QList<QPair<Marks::Mark, QString>> all = { + { Marks::Mark::Attachment, QStringLiteral("attachment") }, + { Marks::Mark::Flagged, QStringLiteral("flagged") }, + { Marks::Mark::Passed, QStringLiteral("passed") }, + { Marks::Mark::Replied, QStringLiteral("replied") }, + { Marks::Mark::ExpanderCollapsed, QStringLiteral("expander-collapsed") }, + { Marks::Mark::ExpanderExpanded, QStringLiteral("expander-expanded") }, + }; + + for (int i = 0; i < all.size(); ++i) { + for (int j = i + 1; j < all.size(); ++j) { + const QImage a = renderMark(all.at(i).first); + const QImage b = renderMark(all.at(j).first); + QVERIFY2(a != b, + qPrintable(QStringLiteral("%1 and %2 render identically") + .arg(all.at(i).second, all.at(j).second))); + } + } +} + +void TestMarks::paintCentresTheMarkInItsRect() +{ + // paint() is what the delegate calls, and it must not stretch a mark to a + // non-square rect: the message pane's rects are not square. + QImage canvas(80, 40, QImage::Format_ARGB32_Premultiplied); + canvas.fill(Qt::transparent); + + { + QPainter painter(&canvas); + Marks::paint(&painter, QRect(0, 0, 80, 40), Marks::Mark::Flagged, + QColor(Qt::black)); + } + + const int ink = inkPixels(canvas); + QVERIFY2(ink > 50, "paint() drew nothing into the canvas"); + + // Sized to the SHORTER side, so nothing is drawn outside a centred 40x40 + // square. Columns outside it must be empty. + for (int y = 0; y < canvas.height(); ++y) { + for (int x = 0; x < 20; ++x) { + QVERIFY2(qAlpha(canvas.pixel(x, y)) == 0, + "the mark was stretched past its square, so a non-square " + "rect distorts it"); + } + for (int x = 60; x < canvas.width(); ++x) + QVERIFY(qAlpha(canvas.pixel(x, y)) == 0); + } +} + +void TestMarks::anEmptySizeOrInvalidColourYieldsNothing() +{ + // Rather than asserting or painting at a garbage size. + QVERIFY(Marks::pixmap(Marks::Mark::Flagged, QSize(0, 0), Qt::black).isNull()); + QVERIFY(Marks::pixmap(Marks::Mark::Flagged, QSize(16, 16), QColor()).isNull()); +} + +QTEST_MAIN(TestMarks) +#include "test_marks.moc" diff --git a/tests/test_threadlistmodel.cpp b/tests/test_threadlistmodel.cpp index 1fb8a1f..e338661 100644 --- a/tests/test_threadlistmodel.cpp +++ b/tests/test_threadlistmodel.cpp @@ -54,6 +54,7 @@ private slots: void readThreadsAreDimmedAndUnreadAreNot(); void flaggedThreadsShowAStar(); void pillTagsExcludeWhatTheRowAlreadyShows(); + void aTagDrawnAsAMarkIsNotAlsoAChip(); void theUnreadCueDoesNotDependOnFontWeight(); void aDoomedThreadKeepsItsContrastEvenWhenRead(); void accountTagBecomesAChipLabel(); @@ -583,10 +584,6 @@ void TestThreadListModel::flaggedThreadsShowAStar() ThreadListModel::IsFlaggedRole).toBool(), "a flagged thread does not report itself flagged"); - // The glyph the delegate draws from that flag must be something a font can - // render: an unrenderable codepoint shows as tofu, which reads as - // breakage rather than as a mark. - QVERIFY(!ThreadListModel::flagGlyph().isEmpty()); } void TestThreadListModel::pillTagsExcludeWhatTheRowAlreadyShows() @@ -1001,16 +998,47 @@ void TestThreadListModel::attachmentIsMarkedOnlyOnTaggedThreads() QVERIFY(!model.data(plainCell, ThreadListModel::HasAttachmentRole).toBool()); QVERIFY(model.data(fileCell, ThreadListModel::HasAttachmentRole).toBool()); - // The glyph must be something a font can draw. An unrenderable codepoint - // shows as a tofu box, which reads as breakage rather than as a marker. - QVERIFY(!ThreadListModel::attachmentGlyph().isEmpty()); - // Only the marked thread gets a tooltip, or an empty cell would claim to // have an attachment on hover. QVERIFY(model.data(plainCell, Qt::ToolTipRole).toString().isEmpty()); QVERIFY(!model.data(fileCell, Qt::ToolTipRole).toString().isEmpty()); } +void TestThreadListModel::aTagDrawnAsAMarkIsNotAlsoAChip() +{ + // Items 69 and 70. passed and replied are drawn marks on line two now, so a + // chip repeating the word puts the same fact on the card twice. Caught by + // rendering a real card rather than by any existing test, which is why this + // one exists: every geometry assertion passed while the row said "passed" + // as both an arrow and a green pill. + ThreadListModel model; + ThreadSummary thread = makeThread(QStringLiteral("t1"), + QStringLiteral("subject")); + thread.tags = { QStringLiteral("inbox"), QStringLiteral("flagged"), + QStringLiteral("attachment"), QStringLiteral("passed"), + QStringLiteral("replied"), QStringLiteral("project") }; + model.appendBatch({ thread }); + + const QStringList pills = + model.data(model.index(0, 0), ThreadListModel::PillTagsRole) + .toStringList(); + + for (const QString &drawn : { QStringLiteral("flagged"), + QStringLiteral("attachment"), + QStringLiteral("passed"), + QStringLiteral("replied") }) { + QVERIFY2(!pills.contains(drawn), + qPrintable(QStringLiteral("'%1' is drawn as a mark and still " + "appears as a chip") + .arg(drawn))); + } + + // Guard: a tag with no mark still becomes a chip, or the assertions above + // would pass against a model that dropped every pill. + QVERIFY2(pills.contains(QStringLiteral("project")), + "an ordinary tag lost its chip, so the filter is too broad"); +} + void TestThreadListModel::modelHasOneColumn() { ThreadListModel model; |
