summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-15 12:56:54 +0200
committerDanilo M. <danix@danix.xyz>2026-08-15 12:56:54 +0200
commit854134ef967568f1441093774cf7497e8c5d6b11 (patch)
tree2eafe37300fbedcaa6cdf028dc8531ad42e026fb /tests
parentc9392961aa3554a17ab88bfe3ad4e6b76b9a60bc (diff)
downloadqtmaildir-854134ef967568f1441093774cf7497e8c5d6b11.tar.gz
qtmaildir-854134ef967568f1441093774cf7497e8c5d6b11.zip
feat(ui): highlight the built-in filter matching the current view
The four filter buttons gave no sign of which one you were looking at, so the row said what you could do and never where you were. The active filter is drawn as a checked QToolButton, which lets the style paint its own pressed look: a hand-picked highlight colour would have to be picked once per theme and would still be wrong under a third. The check state is derived from the query TEXT rather than from the last button clicked, which is the whole design decision. A record of what was pressed goes on lying the moment the query is edited into something else, where a highlight that follows the query clears itself and lights again when a filter's query is typed by hand. It is resolved against the account box, so changing account recomputes it rather than dropping it: the same filter under two accounts is two different query strings and both are still "Inbox". Buttons are held in a hash keyed by generator, cleared at the top of the row build because the row is rebuilt wholesale on every saved-query edit and stale entries would dangle. The connections are owned by the row widget, so a rebuild takes them with it rather than leaving a second copy firing at deleted buttons. Unread opens already highlighted, which is correct rather than incidental: startup_query defaults to it, so the window opens on that view. The test asserts it, so the assertions that follow are known to be a change of state rather than a button that happened to start unchecked. Mutation checked against the design that was rejected: deriving the state from the click instead of the query fails all three tests, each naming the behaviour it protects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/test_mainwindow.cpp129
1 files changed, 129 insertions, 0 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index 38a8b08..c569f05 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -178,6 +178,9 @@ private slots:
void aFilterComposesWithTheSelectedAccount();
void aFilterAcrossAllAccountsIsUnscoped();
void aFilterDoesNotClearTheAccountSelection();
+ void theActiveFilterButtonIsChecked();
+ void aHandEditedQueryChecksNoFilterButton();
+ void theCheckedFilterFollowsTheAccount();
void aSavedQueryStillClearsTheAccountSelection();
void aFilterOffersNoEditOrDeleteActions();
void changingTheAccountRunsNothing();
@@ -6696,6 +6699,132 @@ void TestMainWindow::aFilterAcrossAllAccountsIsUnscoped()
QCOMPARE(queryEdit->text(), QStringLiteral("tag:unread"));
}
+void TestMainWindow::theActiveFilterButtonIsChecked()
+{
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ Config config;
+ config.load(writeSentConfig(dir, {
+ {QStringLiteral("work"), QStringLiteral("Sent")},
+ }));
+
+ MainWindow window(config);
+ auto *inbox =
+ window.findChild<QAbstractButton *>(QStringLiteral("inboxButton"));
+ auto *unread =
+ window.findChild<QAbstractButton *>(QStringLiteral("unreadButton"));
+ QVERIFY(inbox);
+ QVERIFY(unread);
+
+ // Checkable is what lets the style draw the active look at all. A test that
+ // only asserted isChecked() would pass against a button that can hold the
+ // state and never shows it.
+ QVERIFY2(inbox->isCheckable(), "the filter button cannot show a checked state");
+
+ // Unread is checked before anything is clicked, and that is correct rather
+ // than incidental: startup_query defaults to Unread, so the window opens
+ // showing it and the highlight describes the view from the first frame. The
+ // window having opened on a filter is asserted here so the Inbox
+ // assertions below are known to be a CHANGE of state rather than a button
+ // that happened to start unchecked.
+ QVERIFY2(unread->isChecked(),
+ "the default startup view is Unread, so its button should open "
+ "highlighted");
+ QVERIFY(!inbox->isChecked());
+
+ inbox->click();
+ QVERIFY2(inbox->isChecked(), "the filter that ran is not highlighted");
+ QVERIFY2(!unread->isChecked(), "a filter that did not run is highlighted");
+
+ // And the highlight MOVES rather than accumulating. Exactly one button can
+ // describe the current view.
+ unread->click();
+ QVERIFY(unread->isChecked());
+ QVERIFY2(!inbox->isChecked(), "the previous filter stayed highlighted");
+}
+
+void TestMainWindow::aHandEditedQueryChecksNoFilterButton()
+{
+ // The behaviour the user chose over "remember the last click": the
+ // highlight describes what is on screen, so editing the query away from a
+ // filter's own query clears it rather than leaving a button lit over a view
+ // it no longer describes.
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ Config config;
+ config.load(writeSentConfig(dir, {
+ {QStringLiteral("work"), QStringLiteral("Sent")},
+ }));
+
+ MainWindow window(config);
+ auto *queryEdit = window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
+ auto *inbox =
+ window.findChild<QAbstractButton *>(QStringLiteral("inboxButton"));
+ QVERIFY(queryEdit);
+ QVERIFY(inbox);
+
+ inbox->click();
+ QVERIFY(inbox->isChecked());
+
+ queryEdit->setText(QStringLiteral("from:someone@example.org"));
+ QVERIFY2(!inbox->isChecked(),
+ "a hand-edited query left the Inbox button highlighted");
+
+ // And typing a filter's query by hand lights it, since the highlight is a
+ // property of the query rather than a record of which button was pressed.
+ queryEdit->setText(QStringLiteral("tag:inbox"));
+ QVERIFY2(inbox->isChecked(),
+ "a query equal to the Inbox filter did not highlight it");
+
+ // An empty bar is not "every filter matches nothing", which a naive
+ // comparison against an unresolvable query would make it.
+ queryEdit->clear();
+ QVERIFY(!inbox->isChecked());
+}
+
+void TestMainWindow::theCheckedFilterFollowsTheAccount()
+{
+ // Changing the account re-resolves the filter to a different query string,
+ // and both are still "Inbox". The highlight is recomputed rather than
+ // dropped, or switching account would silently un-highlight the view the
+ // user is still looking at.
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ Config config;
+ config.load(writeSentConfig(dir, {
+ {QStringLiteral("work"), QStringLiteral("Sent")},
+ {QStringLiteral("personal"), QStringLiteral("Sent")},
+ }));
+
+ MainWindow window(config);
+ auto *queryEdit = window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
+ auto *inbox =
+ window.findChild<QAbstractButton *>(QStringLiteral("inboxButton"));
+ QVERIFY(queryEdit);
+ QVERIFY(inbox);
+
+ window.selectAccountForTesting(QStringLiteral("work"));
+ inbox->click();
+ QCOMPARE(queryEdit->text(),
+ QStringLiteral("path:\"work/**\" and (tag:inbox)"));
+ QVERIFY(inbox->isChecked());
+
+ // The query bar still holds work's inbox query, which is NOT personal's, so
+ // the button correctly stops describing the view. The state after an
+ // account change is asserted rather than assumed: this is the case where a
+ // highlight keyed on the last click would go on lying.
+ window.selectAccountForTesting(QStringLiteral("personal"));
+ QVERIFY2(!inbox->isChecked(),
+ "the highlight survived an account change that left a query "
+ "belonging to the other account in the bar");
+
+ // Running it again under the new account lights it once more.
+ inbox->click();
+ QCOMPARE(queryEdit->text(),
+ QStringLiteral("path:\"personal/**\" and (tag:inbox)"));
+ QVERIFY(inbox->isChecked());
+}
+
void TestMainWindow::aFilterDoesNotClearTheAccountSelection()
{
// The defect item 90 filed: the button used to reset the dropdown to "All