diff options
| -rw-r--r-- | CHANGELOG.md | 6 | ||||
| -rw-r--r-- | src/config.cpp | 68 | ||||
| -rw-r--r-- | src/config.h | 22 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 83 | ||||
| -rw-r--r-- | src/mainwindow.h | 45 | ||||
| -rw-r--r-- | tests/test_config.cpp | 97 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 197 |
7 files changed, 470 insertions, 48 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a6159..9d872ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ point at which they are stable. - Sent mail is shown as a flat list rather than as threads, and the cards name the **recipients** instead of the sender, which is you on every row. Selecting one opens what you sent, not the conversation your message started. +- The blank message pane counts **sent mail and drafts** beside unread, flagged + and inbox. Both are composed from each account's `sent` and `drafts` folder + rather than from a tag, so they follow the same per-account configuration the + Sent view uses. A line is absent entirely when no account configures that + folder, rather than reading 0. The `drafts` key was already accepted and + documented as unused; it now has an effect. - `[general] date_format`, an optional pattern for the date on a thread card. Absent or empty keeps the system locale's short format, which is unchanged and remains the default. A pattern containing no date or time field is diff --git a/src/config.cpp b/src/config.cpp index 47fadec..a81651f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -47,37 +47,67 @@ QString Account::scopedQuery(const QString &query) const return QStringLiteral("%1 and (%2)").arg(prefix, query); } -QString Account::sentQuery() const +namespace { + +/// Composes `path:"<maildir>/<folder>/**"`, or empty when the folder is unset. +/// +/// The QUOTES are load-bearing, not decoration. A real provider nests both its +/// sent and its drafts folder under a bracketed parent with a localised name, +/// "[Provider]/Posta inviata" and "[Provider]/Bozze", and "[" and "]" are +/// Xapian syntax: unquoted, the term is parsed rather than matched and the +/// query silently returns nothing while looking correct. +/// +/// The path is user config and is interpolated into a query, so this is the +/// only place that composition happens; a caller building it by hand would be +/// a second chance to forget the quotes. +QString folderQuery(const QString &maildir, const QString &folder) { - if (sent.isEmpty()) + if (folder.isEmpty()) return QString(); - - // The QUOTES are load-bearing, not decoration. A real provider nests its - // sent folder under a bracketed parent, "[Provider]/Posta inviata", and - // "[" and "]" are Xapian syntax: unquoted, the term is parsed rather than - // matched and the query silently returns nothing while looking correct. - // - // The path is user config and is interpolated into a query, so this is the - // only place that composition happens; a caller building it by hand would - // be a second chance to forget the quotes. - return QStringLiteral("path:\"%1/%2/**\"").arg(maildir, sent); + return QStringLiteral("path:\"%1/%2/**\"").arg(maildir, folder); } -QString Config::allSentQuery() const +/// Joins the non-empty results of `extract` across `accounts` with " or ". +/// +/// Collect first, join after. Appending "or" per account and trimming the +/// result is the version that produced the defect this guards: an account with +/// no key contributes an empty term, notmuch accepts the bare "or" without +/// complaint, and the query quietly means something else. Measured against a +/// real database, `A or or B` returns 190 where the correct pair returns 211. +QString joinAccountQueries(const QList<Account> &accounts, + QString (Account::*extract)() const) { - // Collect first, join after. Appending "or" per account and trimming the - // result is the version that produced the defect this guards: an account - // with no sent key contributes an empty term, notmuch accepts the bare - // "or" without complaint, and the query quietly means something else. QStringList parts; - for (const Account &account : m_accounts) { - const QString query = account.sentQuery(); + for (const Account &account : accounts) { + const QString query = (account.*extract)(); if (!query.isEmpty()) parts.append(query); } return parts.join(QStringLiteral(" or ")); } +} // namespace + +QString Account::sentQuery() const +{ + return folderQuery(maildir, sent); +} + +QString Account::draftsQuery() const +{ + return folderQuery(maildir, drafts); +} + +QString Config::allSentQuery() const +{ + return joinAccountQueries(m_accounts, &Account::sentQuery); +} + +QString Config::allDraftsQuery() const +{ + return joinAccountQueries(m_accounts, &Account::draftsQuery); +} + QString Config::defaultPath() { const QString base = diff --git a/src/config.h b/src/config.h index e3c5b6e..4092141 100644 --- a/src/config.h +++ b/src/config.h @@ -34,7 +34,13 @@ struct Account QString name; QString address; QString maildir; ///< Relative to notmuch's database.path. - QString drafts; ///< Unused in v1; send is v2. + /// The account's drafts folder, relative to maildir. Optional, exactly as + /// `sent` is, and absent more often: an account that composes elsewhere + /// keeps no local drafts folder at all. + /// + /// Composing drafts is v2. Reading them is not: the placeholder pane + /// counts them (item 67), which is why this is no longer unused. + QString drafts; /// The account's sent folder, relative to maildir. Optional and empty for /// an account that has none, which is a real case rather than a @@ -81,6 +87,13 @@ struct Account /// selector wraps whatever query runs, so a Sent view under one account /// intersects to that account's sent mail and cannot leak another's. QString sentQuery() const; + + /// Matches this account's drafts, or empty when `drafts` is unset. + /// + /// Separate from sentQuery() rather than one parameterised helper: the two + /// keys are independent, and one real account configures `drafts` with no + /// `sent` at all. + QString draftsQuery() const; }; struct SavedQuery @@ -145,6 +158,13 @@ public: /// open-coded at the call site. QString allSentQuery() const; + /// Matches every configured account's drafts, or empty when none has one. + /// + /// Joins only the NON-EMPTY draftsQuery() results, for the same reason + /// allSentQuery() does: notmuch accepts a bare "or" without complaint and + /// silently answers a different question. + QString allDraftsQuery() const; + /// A QDateTime::toString() pattern for the date on a card, or empty for the /// system locale's short format. /// diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index be6cfdb..824bdca 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1385,40 +1385,73 @@ void MainWindow::onAllTagsReady(const QStringList &tags) m_queryCompleter->setTags(tags); } -namespace { +QList<MainWindow::PlaceholderLine> MainWindow::placeholderLines() const +{ + // One list of (query, label-maker) pairs rather than two arrays indexed in + // parallel. The parallel version is what the fixed array was, and its + // hazard is that inserting an entry in one and not the other prints a real + // number against the wrong name, which reads as a plausible pane. + // + // The queries are wire format and deliberately untranslated: `tag:` is + // notmuch syntax, not user-facing prose. Only the labels are translated. + QList<PlaceholderLine> lines = { + { QStringLiteral("tag:unread"), + [this](int n) { return tr("%n unread", "", n); } }, + { QStringLiteral("tag:flagged"), + [this](int n) { return tr("%n flagged", "", n); } }, + { QStringLiteral("tag:inbox"), + [this](int n) { return tr("%n in inbox", "", n); } }, + }; -/// The queries behind the placeholder's helper lines, in render order. -/// -/// Wire format, deliberately untranslated: `tag:` is notmuch syntax, not user -/// -facing prose. Only the labels beside them are translated. -const std::array<const char *, 3> kPlaceholderQueries = { - "tag:unread", - "tag:flagged", - "tag:inbox", -}; + // Sent and drafts are composed from the account folders, not from a tag. + // `tag:draft` counts 0 against a real database and no draft-ish tag exists + // in it, so a tag-based line would be a permanent zero. + // + // Omitted entirely when no account configures the folder, rather than + // shown as 0: item 63 established that a missing sent folder is a real + // configuration, and "0 sent" claims the user has sent nothing. + const QString sent = m_config.allSentQuery(); + if (!sent.isEmpty()) { + lines.append({ sent, [this](int n) { return tr("%n sent", "", n); } }); + } -} // namespace + const QString drafts = m_config.allDraftsQuery(); + if (!drafts.isEmpty()) { + lines.append({ drafts, + [this](int n) { return tr("%n draft(s)", "", n); } }); + } + + return lines; +} + +QStringList MainWindow::placeholderQueries() const +{ + QStringList queries; + for (const PlaceholderLine &line : placeholderLines()) + queries.append(line.query); + return queries; +} QList<HtmlBuilder::PlaceholderHelper> MainWindow::placeholderHelpers() const { QList<HtmlBuilder::PlaceholderHelper> helpers; - // Empty until the first reply lands. Rendering three zeroes meanwhile - // would be worse than rendering nothing: a zero is a claim. - if (m_placeholderCounts.size() == int(kPlaceholderQueries.size())) { - const QStringList labels = { - tr("%n unread", "", m_placeholderCounts.at(0)), - tr("%n flagged", "", m_placeholderCounts.at(1)), - tr("%n in inbox", "", m_placeholderCounts.at(2)), - }; + const QList<PlaceholderLine> lines = placeholderLines(); - for (int i = 0; i < labels.size(); ++i) { + // Empty until the first reply lands. Rendering zeroes meanwhile would be + // worse than rendering nothing: a zero is a claim. + // + // The size check is also what keeps the pairing honest across a config + // that changed shape between the request and the reply: counts that do not + // match the current line list are not this list's answers. + if (m_placeholderCounts.size() == lines.size()) { + for (int i = 0; i < lines.size(); ++i) { // A query notmuch could not count yields -1; skip that line rather // than print a negative number at the user. if (m_placeholderCounts.at(i) < 0) continue; - helpers.append({ labels.at(i), - QString::fromLatin1(kPlaceholderQueries[i]) }); + helpers.append({ lines.at(i).label(m_placeholderCounts.at(i)), + lines.at(i).query }); } } @@ -1438,12 +1471,8 @@ void MainWindow::showPlaceholderPane() { m_messageView->showPlaceholder(placeholderHelpers()); - QStringList queries; - for (const char *query : kPlaceholderQueries) - queries.append(QString::fromLatin1(query)); - QMetaObject::invokeMethod(m_worker, "requestCounts", Qt::QueuedConnection, - Q_ARG(QStringList, queries), + Q_ARG(QStringList, placeholderQueries()), Q_ARG(quint64, ++m_countsGeneration)); } diff --git a/src/mainwindow.h b/src/mainwindow.h index 6e90ba1..0912520 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -165,6 +165,25 @@ public: return !m_recoverThreadId.isEmpty(); } + /// The placeholder's queries, in the order requestCounts() asks for them. + /// + /// A test seam. The worker's reply is paired with these POSITIONALLY, so a + /// test standing in for it has to know the order, and that order now + /// depends on config rather than on a fixed list. + QStringList placeholderQueriesForTesting() const + { + return placeholderQueries(); + } + + /// The helper lines as the pane would render them. + QList<HtmlBuilder::PlaceholderHelper> placeholderHelpersForTesting() const + { + return placeholderHelpers(); + } + + /// The generation the next counts reply must carry to be accepted. + quint64 countsGenerationForTesting() const { return m_countsGeneration; } + protected: void closeEvent(QCloseEvent *event) override; @@ -310,6 +329,30 @@ private: /// The helper lines, built from the last counts received. Rendered with /// whatever the previous answer was until the new one lands, so the pane /// never flashes empty while the worker replies. + /// One placeholder line: the query it counts, and how to label the answer. + /// + /// The label is a callable rather than a string because the count is not + /// known until the worker replies, and `tr("%n ...")` has to be given the + /// number to pick its plural form. + /// + /// Query and label travel together deliberately. The version this replaced + /// held them in two arrays indexed in parallel, where inserting an entry in + /// one and not the other put a real number against the wrong name. + struct PlaceholderLine { + QString query; + std::function<QString(int)> label; + }; + + /// The placeholder's lines, in render order. + /// + /// Built per call rather than cached: the sent and drafts lines come from + /// config, and a cache would be a second source of truth for the pairing + /// the counts reply depends on. + QList<PlaceholderLine> placeholderLines() const; + + /// Just the queries, in the order requestCounts() asks for them. + QStringList placeholderQueries() const; + QList<HtmlBuilder::PlaceholderHelper> placeholderHelpers() const; void showWarnings(); @@ -552,7 +595,7 @@ private: /// Indeterminate, shown only while a sync runs. See setSyncBusy(). QProgressBar *m_syncProgress = nullptr; - /// The last counts the worker answered, one per kPlaceholderQueries entry. + /// The last counts the worker answered, one per placeholderLines() entry. /// Empty until the first reply, which renders the pane without its helper /// lines rather than with three zeroes that would be a lie. QVector<int> m_placeholderCounts; diff --git a/tests/test_config.cpp b/tests/test_config.cpp index 60ed071..dad4ecd 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -71,6 +71,10 @@ private slots: void allSentQueryIsEmptyWhenNoAccountHasOne(); void allSentQuerySkipsAccountsWithoutTheKey(); void allSentQueryJoinsEveryConfiguredAccount(); + void draftsQueryIsEmptyWithoutTheKey(); + void draftsQuerySurvivesABracketedPath(); + void allDraftsQuerySkipsAccountsWithoutTheKey(); + void allDraftsQueryIsIndependentOfSent(); }; static QString writeIni(const QTemporaryDir &dir, const QString &body) @@ -916,5 +920,98 @@ void TestConfig::allSentQueryJoinsEveryConfiguredAccount() QCOMPARE(all.count(QStringLiteral(" or ")), 1); } +void TestConfig::draftsQueryIsEmptyWithoutTheKey() +{ + // Optional for the same reason `sent` is, and more often absent: an + // account that composes elsewhere keeps no local drafts folder at all. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[account.provider-c]\n" + "maildir = provider-c\n"))); + + QCOMPARE(config.accounts().size(), 1); + QVERIFY(config.accounts().at(0).draftsQuery().isEmpty()); + QVERIFY(config.problems().isEmpty()); +} + +void TestConfig::draftsQuerySurvivesABracketedPath() +{ + // The same quoting trap sentQuery() exists for, and it bites harder here: + // a real provider's drafts folder is BOTH bracketed and localised + // ("[Provider]/Bozze"). Unquoted, "[" and "]" are Xapian syntax and the + // term is parsed rather than matched, so the count reads 0 and looks like + // an empty drafts folder rather than a broken query. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[account.provider-a]\n" + "maildir = provider-a\n" + "drafts = [Provider]/Bozze\n"))); + + QCOMPARE(config.accounts().at(0).draftsQuery(), + QStringLiteral("path:\"provider-a/[Provider]/Bozze/**\"")); +} + +void TestConfig::allDraftsQuerySkipsAccountsWithoutTheKey() +{ + // The bare-"or" defect allSentQuerySkipsAccountsWithoutTheKey() records, + // asserted again rather than assumed to be inherited: the two compositions + // are separate functions and a rewrite of one does not carry the other. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[account.webmail-primary]\n" + "maildir = webmail-primary\n" + "drafts = Drafts\n" + "\n" + "[account.provider-c]\n" + "maildir = provider-c\n" + "\n" + "[account.webmail-secondary]\n" + "maildir = webmail-secondary\n" + "drafts = Drafts\n"))); + + const QString all = config.allDraftsQuery(); + + QVERIFY2(!all.contains(QStringLiteral("or or")), + "an account without a drafts key left a bare 'or' in the query"); + QVERIFY2(!all.trimmed().endsWith(QStringLiteral("or")), + "the query ends in a dangling 'or'"); + QVERIFY2(!all.trimmed().startsWith(QStringLiteral("or")), + "the query starts with a dangling 'or'"); + QVERIFY(!all.contains(QStringLiteral("provider-c"))); + + QCOMPARE(all.count(QStringLiteral("path:")), 2); + QCOMPARE(all.count(QStringLiteral(" or ")), 1); +} + +void TestConfig::allDraftsQueryIsIndependentOfSent() +{ + // The two keys are independent, and one real account proves it: it + // configures `drafts` and has no `sent` whatsoever. A composition that + // walked the accounts once and emitted both from the same loop iteration + // would either drop this account's drafts or invent a sent term for it. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[account.webmail-primary]\n" + "maildir = webmail-primary\n" + "sent = Sent\n" + "\n" + "[account.provider-b]\n" + "maildir = provider-b\n" + "drafts = [Provider]/Bozze\n"))); + + const QString drafts = config.allDraftsQuery(); + const QString sent = config.allSentQuery(); + + // One term each, from DIFFERENT accounts. + QCOMPARE(drafts, QStringLiteral("path:\"provider-b/[Provider]/Bozze/**\"")); + QCOMPARE(sent, QStringLiteral("path:\"webmail-primary/Sent/**\"")); + QVERIFY(!drafts.contains(QStringLiteral("webmail-primary"))); + QVERIFY(!sent.contains(QStringLiteral("provider-b"))); +} + QTEST_MAIN(TestConfig) #include "test_config.moc" diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index faa8481..818e5b4 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -173,6 +173,10 @@ private slots: void thereIsNoSentButtonWithoutASentKey(); void theSentButtonRunsEveryConfiguredAccount(); void theSentButtonSurvivesABracketedPath(); + void placeholderCountsSkipSentAndDraftsWithoutTheKeys(); + void placeholderCountsCarrySentAndDrafts(); + void placeholderLabelsStayPairedWithTheirQueries(); + void placeholderCountsDropAnUncountableQuery(); void flatModeDoesNotSurviveTheNextQuery(); void noTwoActionsShareAnIcon(); }; @@ -4639,6 +4643,199 @@ void TestMainWindow::theSentButtonSurvivesABracketedPath() QStringLiteral("path:\"provider-a/[Provider]/Posta inviata/**\"")); } +namespace { + +/// Writes a config whose accounts carry a sent folder, a drafts folder, or +/// neither. Separate from writeSentConfig() because the two keys are +/// independent: the interesting cases here are exactly the ones where an +/// account has one and not the other. +struct FolderAccount { + QString maildir; + QString sent; + QString drafts; +}; + +QString writeFolderConfig(const QTemporaryDir &dir, + const QList<FolderAccount> &accounts) +{ + const QString path = dir.filePath(QStringLiteral("qtmaildir.conf")); + QSettings s(path, QSettings::IniFormat); + for (const FolderAccount &account : accounts) { + s.beginGroup(QStringLiteral("account.") + account.maildir); + s.setValue(QStringLiteral("maildir"), account.maildir); + if (!account.sent.isEmpty()) + s.setValue(QStringLiteral("sent"), account.sent); + if (!account.drafts.isEmpty()) + s.setValue(QStringLiteral("drafts"), account.drafts); + s.endGroup(); + } + s.sync(); + return path; +} + +/// The label of the helper whose query is `query`, or a null string. +QString labelForQuery(const QList<HtmlBuilder::PlaceholderHelper> &helpers, + const QString &query) +{ + for (const HtmlBuilder::PlaceholderHelper &helper : helpers) { + if (helper.query == query) + return helper.label; + } + return QString(); +} + +} // namespace + +void TestMainWindow::placeholderCountsSkipSentAndDraftsWithoutTheKeys() +{ + // The tag: lines are unconditional, the folder lines are not. An account + // with no sent or drafts folder must contribute no line at all rather than + // a line reading 0: item 63 established that a missing folder is a real + // configuration, and "0 sent" claims the user has sent nothing. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + Config config; + config.load(writeFolderConfig(dir, {{QStringLiteral("provider-c"), {}, {}}})); + + MainWindow window(config); + + const QStringList queries = window.placeholderQueriesForTesting(); + QCOMPARE(queries.size(), 3); + QVERIFY(queries.contains(QStringLiteral("tag:unread"))); + QVERIFY(queries.contains(QStringLiteral("tag:flagged"))); + QVERIFY(queries.contains(QStringLiteral("tag:inbox"))); +} + +void TestMainWindow::placeholderCountsCarrySentAndDrafts() +{ + // Both composed from the folder keys rather than from a tag. `tag:draft` + // counts 0 against the user's real database (measured 2026-08-11) and no + // draft-ish tag exists in it at all, so a tag-based drafts line would be a + // permanent zero that looks like working code. + // + // The account layout is the real one's shape: one account with both keys, + // one with drafts and NO sent, which is what proves the two are collected + // independently rather than per-account in one pass. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + Config config; + config.load(writeFolderConfig(dir, { + {QStringLiteral("webmail-primary"), QStringLiteral("Sent"), + QStringLiteral("Drafts")}, + {QStringLiteral("provider-a"), {}, QStringLiteral("[Provider]/Bozze")}, + })); + + MainWindow window(config); + + const QStringList queries = window.placeholderQueriesForTesting(); + QCOMPARE(queries.size(), 5); + + // The quoting survives the trip, which is the trap this whole composition + // exists for: unquoted, "[" and "]" are Xapian syntax and the count reads + // 0 while looking like an empty folder. + QVERIFY2(queries.contains(config.allDraftsQuery()), + qPrintable(QStringLiteral("drafts query missing, got: %1") + .arg(queries.join(QStringLiteral(" | "))))); + QVERIFY(queries.contains(config.allSentQuery())); + QVERIFY(config.allDraftsQuery().contains( + QStringLiteral("path:\"provider-a/[Provider]/Bozze/**\""))); + + // The sent term comes from the account that has one, and the drafts terms + // from both. An implementation that emitted a folder line per account + // would produce four folder queries instead of two. + QCOMPARE(config.allSentQuery().count(QStringLiteral("path:")), 1); + QCOMPARE(config.allDraftsQuery().count(QStringLiteral("path:")), 2); +} + +void TestMainWindow::placeholderLabelsStayPairedWithTheirQueries() +{ + // The defect this guards is the one the old fixed array invited: the + // labels were written positionally against a separate query array, so + // inserting an entry in one and not the other put a real number against + // the wrong name. Asserting the PAIRING rather than the order is what + // survives a later reshuffle. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + Config config; + config.load(writeFolderConfig(dir, { + {QStringLiteral("webmail-primary"), QStringLiteral("Sent"), + QStringLiteral("Drafts")}, + })); + + MainWindow window(config); + + const QStringList queries = window.placeholderQueriesForTesting(); + QCOMPARE(queries.size(), 5); + + // Distinct counts, so a label reading the wrong index cannot coincide with + // the right answer. Positional against the queries the window just asked + // for, which is exactly the contract requestCounts() replies under. + QVector<int> counts; + for (int i = 0; i < queries.size(); ++i) + counts.append((i + 1) * 10); + + QMetaObject::invokeMethod(&window, "onCountsReady", + Q_ARG(QVector<int>, counts), + Q_ARG(quint64, window.countsGenerationForTesting())); + + const QList<HtmlBuilder::PlaceholderHelper> helpers = + window.placeholderHelpersForTesting(); + + for (int i = 0; i < queries.size(); ++i) { + const QString label = labelForQuery(helpers, queries.at(i)); + QVERIFY2(!label.isNull(), + qPrintable(QStringLiteral("no helper for query '%1'") + .arg(queries.at(i)))); + QVERIFY2(label.contains(QString::number(counts.at(i))), + qPrintable(QStringLiteral("query '%1' was labelled '%2', which " + "does not carry its own count %3") + .arg(queries.at(i), label) + .arg(counts.at(i)))); + } + + // And the folder lines say what they are, not "in inbox". + QVERIFY(labelForQuery(helpers, config.allSentQuery()) + .contains(QStringLiteral("sent"))); + QVERIFY(labelForQuery(helpers, config.allDraftsQuery()) + .contains(QStringLiteral("draft"))); +} + +void TestMainWindow::placeholderCountsDropAnUncountableQuery() +{ + // The worker answers -1 for a query it could not count, rather than + // skipping the entry, precisely so the positional pairing holds. The pane + // must then drop that LINE rather than print a negative number, and drop + // only that one. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + Config config; + config.load(writeFolderConfig(dir, { + {QStringLiteral("webmail-primary"), QStringLiteral("Sent"), + QStringLiteral("Drafts")}, + })); + + MainWindow window(config); + + const QStringList queries = window.placeholderQueriesForTesting(); + QVector<int> counts; + for (int i = 0; i < queries.size(); ++i) + counts.append(i == 0 ? -1 : (i + 1) * 10); + + QMetaObject::invokeMethod(&window, "onCountsReady", + Q_ARG(QVector<int>, counts), + Q_ARG(quint64, window.countsGenerationForTesting())); + + const QList<HtmlBuilder::PlaceholderHelper> helpers = + window.placeholderHelpersForTesting(); + + QVERIFY(labelForQuery(helpers, queries.at(0)).isNull()); + for (int i = 1; i < queries.size(); ++i) { + QVERIFY2(!labelForQuery(helpers, queries.at(i)).isNull(), + qPrintable(QStringLiteral("an uncountable query took '%1' " + "down with it").arg(queries.at(i)))); + } +} + void TestMainWindow::flatModeDoesNotSurviveTheNextQuery() { // The condition the user set for this feature: a flat Sent list is fine, a |
