summaryrefslogtreecommitdiffstats
path: root/tests/test_searchterm.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-14 17:14:33 +0200
committerDanilo M. <danix@danix.xyz>2026-08-14 17:14:33 +0200
commita79725e055699524ec57d09bd484f274ea4a961e (patch)
tree8ca944300b3f8e2650940154df73aa8b561bb3b8 /tests/test_searchterm.cpp
parente876e509b0770a243725b63ea55c9ccf3e41b1bf (diff)
parentbbf3c570215688c553fd70d8f372ae215725ca02 (diff)
downloadqtmaildir-a79725e055699524ec57d09bd484f274ea4a961e.tar.gz
qtmaildir-a79725e055699524ec57d09bd484f274ea4a961e.zip
Merge: searching from the message pane (item 85)
Five surfaces in the message pane offer a search built from what they show: the header's subject and date, its sender and recipients on a single-message thread, a tag chip, a body selection, and every header per message in the details dialog. Each offers Search for this, which replaces the query, and Add to search, which narrows it. The details dialog became labelled rows along the way, which the user wanted independently of this feature. Hand tested through every surface, including the case the parenthesising exists for: adding a sender to 'tag:inbox or tag:flagged' narrows it rather than widening it.
Diffstat (limited to 'tests/test_searchterm.cpp')
-rw-r--r--tests/test_searchterm.cpp146
1 files changed, 146 insertions, 0 deletions
diff --git a/tests/test_searchterm.cpp b/tests/test_searchterm.cpp
new file mode 100644
index 0000000..e0bdb60
--- /dev/null
+++ b/tests/test_searchterm.cpp
@@ -0,0 +1,146 @@
+/*
+ * 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 <QtTest>
+
+#include "searchterm.h"
+
+/// The query grammar of the right-click search actions.
+///
+/// Asserted on the CONSTRUCTED STRING throughout, never on a query notmuch
+/// refuses: notmuch's parser rejects almost nothing, so `from:((((` parses
+/// cleanly and matches zero. A test expecting a failure would pass against
+/// correct code and against broken code alike.
+class TestSearchTerm : public QObject
+{
+ Q_OBJECT
+private slots:
+ void quotesAPlainValue();
+ void escapesEmbeddedQuotes();
+ void collapsesWhitespaceAndNewlines();
+ void rejectsEmptyAndWhitespaceOnly();
+ void capsAnOverlongSelection();
+ void buildsAFieldTerm();
+ void omitsAFieldWithNoValue();
+ void buildsADateRangeForOneDay();
+ void tagIsNotQuoted();
+ void extendParenthesisesBothSides();
+ void extendOntoAnEmptyQueryIsAReplace();
+};
+
+void TestSearchTerm::quotesAPlainValue()
+{
+ QCOMPARE(SearchTerm::quote(QStringLiteral("Quarterly report")),
+ QStringLiteral("\"Quarterly report\""));
+}
+
+void TestSearchTerm::escapesEmbeddedQuotes()
+{
+ // A selection is arbitrary prose and can carry a quote. Unescaped, it ends
+ // the quoted string early and the rest becomes stray query syntax, which
+ // notmuch accepts and matches nothing on.
+ QCOMPARE(SearchTerm::quote(QStringLiteral("say \"hello\" now")),
+ QStringLiteral("\"say \\\"hello\\\" now\""));
+}
+
+void TestSearchTerm::collapsesWhitespaceAndNewlines()
+{
+ QCOMPARE(SearchTerm::quote(QStringLiteral(" two\n\nlines\there ")),
+ QStringLiteral("\"two lines here\""));
+}
+
+void TestSearchTerm::rejectsEmptyAndWhitespaceOnly()
+{
+ // An empty term must yield an empty string, which is what every caller
+ // tests to decide whether to offer a menu entry at all.
+ QVERIFY(SearchTerm::quote(QString()).isEmpty());
+ QVERIFY(SearchTerm::quote(QStringLiteral(" \n\t ")).isEmpty());
+}
+
+void TestSearchTerm::capsAnOverlongSelection()
+{
+ // A multi-kilobyte selection is a mis-drag, not a query.
+ const QString huge(5000, QLatin1Char('x'));
+ const QString term = SearchTerm::quote(huge);
+ QVERIFY2(term.size() < 300,
+ qPrintable(QStringLiteral("term was %1 chars").arg(term.size())));
+ QVERIFY(term.startsWith(QStringLiteral("\"xxx")));
+ QVERIFY(term.endsWith(QLatin1Char('"')));
+}
+
+void TestSearchTerm::buildsAFieldTerm()
+{
+ QCOMPARE(SearchTerm::field(QStringLiteral("from"),
+ QStringLiteral("Foo <foo@example.org>")),
+ QStringLiteral("from:\"Foo <foo@example.org>\""));
+}
+
+void TestSearchTerm::omitsAFieldWithNoValue()
+{
+ // A message with no Cc must not offer cc:"" , which parses cleanly and
+ // matches nothing, so the entry would look enabled and do nothing.
+ QVERIFY(SearchTerm::field(QStringLiteral("cc"), QString()).isEmpty());
+}
+
+void TestSearchTerm::buildsADateRangeForOneDay()
+{
+ // notmuch's date: range is inclusive at both ends, so one day is the day
+ // named twice rather than the day and its successor.
+ const QDate day(2026, 8, 14);
+ QCOMPARE(SearchTerm::onDate(day),
+ QStringLiteral("date:2026-08-14..2026-08-14"));
+ QVERIFY(SearchTerm::onDate(QDate()).isEmpty());
+}
+
+void TestSearchTerm::tagIsNotQuoted()
+{
+ // A tag name is a token from a known vocabulary, not prose. Quoting one
+ // is not wrong but reads badly in the bar, and the user edits that text.
+ QCOMPARE(SearchTerm::tag(QStringLiteral("inbox")),
+ QStringLiteral("tag:inbox"));
+ // A tag containing a space is the exception and does need quoting.
+ QCOMPARE(SearchTerm::tag(QStringLiteral("to do")),
+ QStringLiteral("tag:\"to do\""));
+ QVERIFY(SearchTerm::tag(QString()).isEmpty());
+}
+
+void TestSearchTerm::extendParenthesisesBothSides()
+{
+ // THE case this exists for. The bar may hold a hand-written disjunction,
+ // and `a or b AND c` binds as `a or (b AND c)`: the result WIDENS a search
+ // the user asked to narrow. Both sides are wrapped so neither can rebind.
+ QCOMPARE(SearchTerm::extend(QStringLiteral("tag:inbox or tag:flagged"),
+ QStringLiteral("from:foo@example.org")),
+ QStringLiteral("(tag:inbox or tag:flagged) AND (from:foo@example.org)"));
+}
+
+void TestSearchTerm::extendOntoAnEmptyQueryIsAReplace()
+{
+ // Rather than "() AND (x)", which matches nothing.
+ QCOMPARE(SearchTerm::extend(QString(), QStringLiteral("tag:inbox")),
+ QStringLiteral("tag:inbox"));
+ QCOMPARE(SearchTerm::extend(QStringLiteral(" "),
+ QStringLiteral("tag:inbox")),
+ QStringLiteral("tag:inbox"));
+ // And an empty new term leaves the existing query alone.
+ QCOMPARE(SearchTerm::extend(QStringLiteral("tag:inbox"), QString()),
+ QStringLiteral("tag:inbox"));
+}
+
+QTEST_MAIN(TestSearchTerm)
+#include "test_searchterm.moc"