aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md6
-rw-r--r--src/config.cpp68
-rw-r--r--src/config.h22
-rw-r--r--src/mainwindow.cpp83
-rw-r--r--src/mainwindow.h45
-rw-r--r--tests/test_config.cpp97
-rw-r--r--tests/test_mainwindow.cpp197
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