diff options
| -rw-r--r-- | CHANGELOG.md | 16 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | 46 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 47 | ||||
| -rw-r--r-- | src/config.cpp | 26 | ||||
| -rw-r--r-- | src/config.h | 5 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 66 | ||||
| -rw-r--r-- | src/mainwindow.h | 4 | ||||
| -rw-r--r-- | src/savequerydialog.cpp | 8 | ||||
| -rw-r--r-- | src/savequerydialog.h | 2 | ||||
| -rw-r--r-- | tests/test_config.cpp | 69 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 359 | ||||
| -rw-r--r-- | translations/qtmaildir_it_IT.ts | 12 |
12 files changed, 307 insertions, 353 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ee21a2..1144402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,16 @@ point at which they are stable. - The main toolbar keeps the actions that need no particular message (Compose, Sync, Archive, Delete, Mark all read, Undo). Reply moves to the new message-pane bar, where Forward joins it. +- **Saved queries live in the "More queries" menu, and only there.** The query + row is the six built-in filters (Unread, Inbox, Important, Sent, Drafts, + Trash), which compose with the account dropdown. Pinning a saved query to the + row is gone: nothing has to decide which of the user's queries get button + space, and every saved query is reached the same way. Each menu entry still + offers Run, Edit, Delete and, for a stored query, Create tagging rule. +- The message-pane bar shows **Edit draft** in place of Reply and Forward when + the message on display is a draft, and the bar itself now appears and + disappears with the pane's subject and details rows rather than sitting over + an empty pane. ### Fixed @@ -92,6 +102,12 @@ point at which they are stable. ### Upgrading +**Pinned saved queries are no longer buttons.** If any of your saved queries +sat on the query row, it is now in the "More queries" menu with the rest. The +`pinned` key is no longer read, and it is dropped from `queries.json` the next +time a query is saved, edited or deleted. Nothing else about a saved query +changes, and none are removed. + **To send, an account needs a `send_command`.** Without one it is receive-only: it still reads, tags and syncs exactly as before, and the compose actions are simply disabled for it. Nothing breaks by doing nothing. diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md index 0f7181b..7314700 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md @@ -7392,3 +7392,49 @@ draft stayed disabled on a draft selected that way, and the reply family had the same blind spot without a test that could see it. It is now connected to both signals. Reading `currentRowChanged` is safe here for the reason `CLAUDE.md` gives: it answers "which row is current", and no count is read. + +## 94. `pinned` has nothing left to decide once the buttons are built-in + +**Observed (user, 2026-08-15),** thinking past item 93 rather than from the +notes: + +> after we've migrated [...] we can drop my 4 redundant (by then) saved queries, +> and there won't be a need for pinning anymore. The buttons will be driven by +> the hardcoded queries, the menu will be the home for saved queries. + +**The end state this describes:** the query row is built-in filters ONLY, and +every saved query lives in the menu. No mixing, so nothing has to decide which +saved queries get button real estate, and `SavedQuery::pinned` is dead weight. + +**This also disposes of a problem item 93 would otherwise have to solve.** With +both tiers sharing one row, something must order the four filters against the +user's pinned queries. Under this end state the question does not arise. + +**Blocked on 93, and deliberately not part of it.** The user needs to live with +the four buttons first and confirm they cover what they actually use. If one is +wrong, pinning is the escape hatch, and it has to still be there to be used. +Closing 93 and this together would remove the fallback before it was needed. + +**This is a user-visible removal, not a cleanup.** `pinned` shipped in 0.18.0: +`SaveQueryDialog` offers "Show as a button" (`src/savequerydialog.cpp:104`) and +the right-click menu offers "Move to menu" / "Show as a button" +(`src/mainwindow.cpp:1797`). Anyone who put a saved query on the row loses that +permanently. Semver on the user-visible surface makes it a minor bump with an +`### Upgrading` note. + +**The stored field is a separate decision from the UI.** `pinned` is written to +queries.json (`src/config.cpp:603`) and read back (`:542`). Two options, and the +cheaper one is also the reversible one: + +- **Stop reading it, leave it in the file.** Harmless: an ignored key, preserved + by the unknown-field handling, and a build that reintroduces pinning would + find every user's setting intact. +- **Strip it on the next save.** Cleaner file, and irreversible for anyone who + had it set. + +Prefer leaving it unless the user asks otherwise. No `kQueriesFormatVersion` +bump either way: an ignored optional field is not a breaking change. + +**Size: S.** Removing a field, two UI affordances and their tests. + +**Closed 2026-08-24** (unreleased). See the status table row for the outcome. diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md index 63bd833..49b9613 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md @@ -161,7 +161,7 @@ taking that too literally. | 92 | Nothing distinguishes a tag written by a rule from one the user applied | information | M | **postponed** 2026-08-15 at the user's request: "I don't see the utility, so I don't really know how to answer." Needs per-MESSAGE provenance nothing records, a two-repo format change blank on all existing mail. Reopen only if the need appears in use | | 93 | The query buttons are whatever the user pinned, not a designed set of filters | workflow | M | **done** 2026-08-15, unreleased; see `specs/2026-08-15-builtin-filters-design.md`. Absorbs item 90. Four built-in filters composing with the account dropdown; the user's own queries unpinned, never deleted | | 95 | A query in the overflow menu cannot be run | defect | XS | **done** 2026-08-15, unreleased. Pre-existing and not caused by 93: the entry's action owned a submenu, and Qt emits no `triggered` for those, so the connection had never fired. Surfaced because 93 moved every query into the menu | -| 94 | `pinned` has nothing left to decide once the buttons are built-in | maintenance | S | open; **blocked on 93**, and deliberately not part of it. A user-visible removal: the row becomes built-ins only and every saved query lives in the menu | +| 94 | `pinned` has nothing left to decide once the buttons are built-in | maintenance | S | **done** 2026-08-24, unreleased. The query row is the six built-in filters only, every saved query is in the menu, and `SavedQuery::pinned` is gone from the struct, the reader, the writer, the save dialog's checkbox and the pin/unpin context action. The user chose to **strip** the stored key rather than leave it ignored, against this entry's own preference, so `pinned` stays named in the reader's `known` list precisely so it is NOT preserved as an unknown field and written back. Confirmed with the user first that the built-in set covers their use, since removing pinning removes the escape hatch this item was blocked on. Six tests reached saved queries through buttons that no longer exist and were converted to the menu, two more replaced outright (`onlyPinnedQueriesBecomeButtons` and friends), and `migrationPinsEveryEntry` / `aStoredGeneratedQueryIsUnpinnedNotDropped` were rewritten around the property that survives: an entry must be KEPT, which is what those assertions were really guarding. Three translated strings retired, `lrelease` reports 479 finished 0 unfinished | | 96 | A query returning the thread already on display opens onto the placeholder | defect | S | **done** 2026-08-15, unreleased. Split from 66's unverified half, which had a different cause. Reproduced from two screenshots after four measured eliminations | | 97 | An edit made during a sync is reverted in the list when the sync ends | defect | S | **done** 2026-08-15, unreleased. Found by hand-testing item 89's fix. The sync-end refresh ran BEFORE the held-edit flush, so it read a database that still carried the old tag | | 98 | "Important" adds the tag but cannot remove it, unlike every other toggle | defect | XS | **done** 2026-08-17, unreleased. Calls `everySelectedRowHasTag()`, as the entry required. Its reply test needed THREE different states (list-first thread, the reply's own thread, the reply) before it could tell the two wrong answers apart; with the reply defaulted to its thread's state the item 105 mutation stayed green, measured | @@ -432,51 +432,6 @@ Those are three different features. **Size: `?`, unspecified**, and out of scope until v2 exists. Ask before designing anything. -## 94. `pinned` has nothing left to decide once the buttons are built-in - -**Observed (user, 2026-08-15),** thinking past item 93 rather than from the -notes: - -> after we've migrated [...] we can drop my 4 redundant (by then) saved queries, -> and there won't be a need for pinning anymore. The buttons will be driven by -> the hardcoded queries, the menu will be the home for saved queries. - -**The end state this describes:** the query row is built-in filters ONLY, and -every saved query lives in the menu. No mixing, so nothing has to decide which -saved queries get button real estate, and `SavedQuery::pinned` is dead weight. - -**This also disposes of a problem item 93 would otherwise have to solve.** With -both tiers sharing one row, something must order the four filters against the -user's pinned queries. Under this end state the question does not arise. - -**Blocked on 93, and deliberately not part of it.** The user needs to live with -the four buttons first and confirm they cover what they actually use. If one is -wrong, pinning is the escape hatch, and it has to still be there to be used. -Closing 93 and this together would remove the fallback before it was needed. - -**This is a user-visible removal, not a cleanup.** `pinned` shipped in 0.18.0: -`SaveQueryDialog` offers "Show as a button" (`src/savequerydialog.cpp:104`) and -the right-click menu offers "Move to menu" / "Show as a button" -(`src/mainwindow.cpp:1797`). Anyone who put a saved query on the row loses that -permanently. Semver on the user-visible surface makes it a minor bump with an -`### Upgrading` note. - -**The stored field is a separate decision from the UI.** `pinned` is written to -queries.json (`src/config.cpp:603`) and read back (`:542`). Two options, and the -cheaper one is also the reversible one: - -- **Stop reading it, leave it in the file.** Harmless: an ignored key, preserved - by the unknown-field handling, and a build that reintroduces pinning would - find every user's setting intact. -- **Strip it on the next save.** Cleaner file, and irreversible for anyone who - had it set. - -Prefer leaving it unless the user asks otherwise. No `kQueriesFormatVersion` -bump either way: an ignored optional field is not a breaking change. - -**Size: S.** Removing a field, two UI affordances and their tests. - - ## 99. The unread action is labelled "Toggle unread" whichever way it will go **Observed (user, from the notes):** "the label for 'toggle unread' should be diff --git a/src/config.cpp b/src/config.cpp index cb6168f..23c7364 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -728,10 +728,6 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings) SavedQuery query; query.name = name; query.query = settings.value(name).toString(); - // Pinned, because these are buttons today. A migration that left - // them unpinned would empty the query row on the first launch - // after an upgrade, which reads as data loss. - query.pinned = true; m_savedQueries.append(query); } settings.endGroup(); @@ -804,7 +800,6 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings) SavedQuery query; query.name = object.value(QStringLiteral("name")).toString(); query.query = object.value(QStringLiteral("query")).toString(); - query.pinned = object.value(QStringLiteral("pinned")).toBool(false); query.account = object.value(QStringLiteral("account")).toString(); query.generated = object.value(QStringLiteral("generated")).toString(); // A generator carries its own view mode, so "sent" is flat whether or @@ -833,19 +828,12 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings) continue; } - // A stored entry naming a generator now duplicates a BUILT-IN filter of - // the same name, since item 93 ships all four rather than storing them. - // 0.19.0 migrated the hardcoded Sent button into exactly such an entry, - // so every existing install has one. - // - // Unpinned, never dropped: the row would otherwise carry two Sent - // buttons, one the user's to edit and one not. Deleting it would be - // data loss on a file whose readers are supposed to preserve what they - // do not own, and an unpin is reversible from the UI. - if (query.isGenerated() && isKnownGenerator(query.generated)) - query.pinned = false; - for (auto it = object.begin(); it != object.end(); ++it) { + // `pinned` is listed although nothing reads it any more (item + // 94). Dropping it from this list would make it an UNKNOWN field, + // which is preserved and written back, and the user chose to strip + // it rather than carry it forward. This is the one place a retired + // key has to stay named to be forgotten. static const QStringList known = { QStringLiteral("name"), QStringLiteral("query"), QStringLiteral("pinned"), QStringLiteral("account"), @@ -869,7 +857,7 @@ bool Config::saveSavedQueries() const // Only what carries information. The file is hand-editable, so a key // that always holds the same value, or one the generator already // implies, is just something the reader has to skip past. Same reason - // `pinned` and `account` are written only when set. + // `account` is written only when set. QJsonObject object; object.insert(QStringLiteral("name"), query.name); if (query.isGenerated()) { @@ -877,8 +865,6 @@ bool Config::saveSavedQueries() const } else { object.insert(QStringLiteral("query"), query.query); } - if (query.pinned) - object.insert(QStringLiteral("pinned"), true); if (!query.account.isEmpty()) object.insert(QStringLiteral("account"), query.account); // Skipped when the generator already implies it, which loadSavedQueries diff --git a/src/config.h b/src/config.h index 3ddd76a..4dcfbf1 100644 --- a/src/config.h +++ b/src/config.h @@ -165,11 +165,6 @@ struct SavedQuery QString name; QString query; - /// Renders as a button in the query row; otherwise it lives in the menu. - /// The two tiers are the point of the flag: a row that shows every saved - /// query does not scale past a handful. - bool pinned = false; - /// Account KEY, the INI group suffix ("work" from [account.work]), and /// empty for a query that spans every account. /// diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 42fe1bf..99eb2a3 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2782,11 +2782,10 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout) // from, since nothing looks them up by name. m_filterButtons.clear(); - // The built-in filters come first, in their own fixed order, and they are - // not saved queries: they are shipped, they are not in queries.json, and - // the user cannot edit or delete them (item 93). They are what the row is - // FOR; the pinned saved queries below them are the transitional half that - // item 94 removes. + // The built-in filters are the whole row: they are shipped, they are not + // in queries.json, and the user cannot edit or delete them (item 93). + // Item 94 removed the transitional half, so a saved query is never a + // button and the menu is its only home. for (const SavedQuery &filter : Config::builtinFilters()) { // Sent with no account configuring a sent folder finds nothing by // construction. Hidden rather than present and empty, which is what the @@ -2854,28 +2853,18 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout) box->addWidget(button); } - // The user's own saved queries. A pinned one is still a button, beside the - // filters, until item 94 makes the menu their only home. - QList<SavedQuery> unpinned; + // The user's own saved queries, every one of them in the menu (item 94). + // The row is built-in filters only, so nothing has to decide which saved + // queries get button real estate. + QList<SavedQuery> savedQueries; for (const SavedQuery &saved : m_config.savedQueries()) { - // A generator whose accounts configure nothing produces a button that + // A generator whose accounts configure nothing produces an entry that // always finds nothing. Skipped entirely, which is what the hardcoded // Sent button did and is worth keeping. if (saved.isGenerated() && m_config.resolvedQuery(saved).isEmpty()) continue; - if (!saved.pinned) { - unpinned.append(saved); - continue; - } - auto *button = new QPushButton(saved.name, row); - // No object name here any more. "sentButton" now belongs to the BUILT-IN - // Sent filter, and a migrated Sent entry claiming it too would give two - // buttons one name, so findChild() would return whichever came first. - connect(button, &QPushButton::clicked, this, - [this, saved]() { runSavedQuery(saved); }); - addSavedQueryActions(button, saved); - box->addWidget(button); + savedQueries.append(saved); } // Everything above is left-aligned; the stretch here pushes what follows @@ -2887,16 +2876,16 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout) // The overflow menu, and only when something is in it: an empty menu // button is a control that always does nothing. - if (!unpinned.isEmpty()) { + if (!savedQueries.isEmpty()) { auto *menuButton = new QPushButton(tr("More queries"), row); menuButton->setObjectName(QStringLiteral("savedQueryMenuButton")); auto *menu = new QMenu(menuButton); - for (const SavedQuery &saved : unpinned) { + for (const SavedQuery &saved : savedQueries) { QAction *action = menu->addAction(saved.name); // A menu entry has no context menu of its own, so its own submenu - // carries the same actions; an unpinned query would otherwise be - // the one thing that cannot be edited or deleted. + // carries the same actions; a saved query would otherwise be the + // one thing that cannot be edited or deleted. auto *entryMenu = new QMenu(menu); // Running the query is an item INSIDE that submenu, and must be: @@ -2905,7 +2894,8 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout) // only opens the submenu. That shipped, and went unnoticed while // the menu was the rarely-used half and the user's queries were // pinned buttons. Item 93 moved every query into the menu, and item - // 94 makes it their only home. + // 94 made it their only home, so this is now the ONLY way to run + // one. auto *run = new QAction(tr("Run"), entryMenu); run->setObjectName(QStringLiteral("runQuery")); connect(run, &QAction::triggered, this, @@ -2927,9 +2917,9 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout) // Nothing on either side of the stretch leaves an empty strip of padding, // so the row goes away rather than sitting there as a gap. Counted before - // the stretch was added, since the stretch is always there: an unpinned - // query with no pinned ones still needs the row for its menu. - if (contentCount == 0 && unpinned.isEmpty()) + // the stretch was added, since the stretch is always there: a saved query + // with no built-in filters shown still needs the row for its menu. + if (contentCount == 0 && savedQueries.isEmpty()) row->hide(); // Connected HERE rather than beside the query bar's other handlers, which @@ -2959,17 +2949,6 @@ void MainWindow::addSavedQueryActions(QWidget *target, const SavedQuery &saved) [this, saved]() { editSavedQuery(saved); }); target->addAction(edit); - auto *pin = new QAction(saved.pinned ? tr("Move to menu") - : tr("Show as a button"), - target); - pin->setObjectName(QStringLiteral("pinQuery")); - connect(pin, &QAction::triggered, this, [this, saved]() { - SavedQuery toggled = saved; - toggled.pinned = !saved.pinned; - replaceSavedQuery(saved.name, toggled); - }); - target->addAction(pin); - auto *separator = new QAction(target); separator->setSeparator(true); target->addAction(separator); @@ -3182,10 +3161,9 @@ void MainWindow::saveCurrentQuery() void MainWindow::rebuildSavedQueryRow() { - // The row is rebuilt wholesale rather than patched: a new query can be - // pinned, unpinned, or replace an existing one, and each moves a different - // widget. Deleting and rebuilding is a handful of buttons and cannot get - // the three cases wrong. + // The row is rebuilt wholesale rather than patched: a query can be added, + // deleted or replaced, and each moves a different entry. Deleting and + // rebuilding is a handful of widgets and cannot get the cases wrong. auto *old = findChild<QWidget *>(QStringLiteral("savedQueryRow")); if (!old) return; diff --git a/src/mainwindow.h b/src/mainwindow.h index 4b1ef8f..951eaa4 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -446,8 +446,8 @@ private: /// Builds the row of saved-query buttons, the overflow menu and Sent. /// /// Its own row since item 23: an unbounded list of buttons sharing the - /// query row squeezed the field, which is the whole reason for the - /// pinned/unpinned split. + /// query row squeezed the field. Item 94 settled the split the other way, + /// so the row is built-in filters and one menu. void buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout); /// Runs a saved query, taking its account scope through the dropdown. diff --git a/src/savequerydialog.cpp b/src/savequerydialog.cpp index 2d09986..43c226b 100644 --- a/src/savequerydialog.cpp +++ b/src/savequerydialog.cpp @@ -18,7 +18,6 @@ #include "savequerydialog.h" -#include <QCheckBox> #include <QComboBox> #include <QDialogButtonBox> #include <QFormLayout> @@ -45,7 +44,6 @@ SaveQueryDialog::SaveQueryDialog(const Config &config, const QString &query, SavedQuery initial; initial.query = query; initial.account = accountKey; - initial.pinned = true; setWindowTitle(tr("Save query")); build(initial); } @@ -101,11 +99,6 @@ void SaveQueryDialog::build(const SavedQuery &initial) m_account->setCurrentIndex(index >= 0 ? index : 0); form->addRow(tr("Account"), m_account); - m_pinned = new QCheckBox(tr("Show as a button"), this); - m_pinned->setObjectName(QStringLiteral("saveQueryPinned")); - m_pinned->setChecked(initial.pinned); - form->addRow(QString(), m_pinned); - layout->addLayout(form); // Says what is about to happen rather than refusing the name. Overwriting @@ -162,7 +155,6 @@ SavedQuery SaveQueryDialog::savedQuery() const saved.name = m_name->text().trimmed(); saved.query = m_query->text().trimmed(); saved.account = m_account->currentData().toString(); - saved.pinned = m_pinned->isChecked(); // Carried through rather than re-derived: an edit must not turn a // generated entry into a plain one holding a snapshot of what it happened // to resolve to today. diff --git a/src/savequerydialog.h b/src/savequerydialog.h index d859254..a91654e 100644 --- a/src/savequerydialog.h +++ b/src/savequerydialog.h @@ -22,7 +22,6 @@ #include "config.h" -class QCheckBox; class QComboBox; class QLabel; class QLineEdit; @@ -73,7 +72,6 @@ private: QLineEdit *m_name = nullptr; QLineEdit *m_query = nullptr; QComboBox *m_account = nullptr; - QCheckBox *m_pinned = nullptr; QPushButton *m_ok = nullptr; QLabel *m_notice = nullptr; }; diff --git a/tests/test_config.cpp b/tests/test_config.cpp index 4d56ec1..a5dce9a 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -57,7 +57,7 @@ private slots: void unknownFieldsSurviveARoundTrip(); void savedQueriesRoundTripUnchanged(); void migrationWritesJsonAndLeavesTheIniByteIdentical(); - void migrationPinsEveryEntry(); + void migrationKeepsEveryEntry(); void jsonWinsOnceItExists(); void malformedQueriesFileIsAProblemNotACrash(); void futureVersionIsRefusedAndReported(); @@ -96,7 +96,7 @@ private slots: void allSentQueryIsEmptyWhenNoAccountHasOne(); void allSentQuerySkipsAccountsWithoutTheKey(); void allSentQueryJoinsEveryConfiguredAccount(); - void aStoredGeneratedQueryIsUnpinnedNotDropped(); + void aStoredGeneratedQueryIsKeptNotDropped(); void theStartupAccountIsReadAndValidated(); void theStartupAccountTakesTheKeyNotTheSyncChannel(); void theStartupQueryCanNameABuiltinFilter(); @@ -1818,12 +1818,7 @@ void TestConfig::savedQueryFieldsAreRead() QCOMPARE(queries.at(0).name, QStringLiteral("Inbox")); QCOMPARE(queries.at(0).query, QStringLiteral("tag:inbox")); - QVERIFY(queries.at(0).pinned); QVERIFY(queries.at(0).account.isEmpty()); - - // pinned defaults to false, which is what puts a query in the menu rather - // than on the row. - QVERIFY(!queries.at(1).pinned); QCOMPARE(queries.at(1).account, QStringLiteral("work")); } @@ -1883,7 +1878,6 @@ void TestConfig::savedQueriesRoundTripUnchanged() for (int i = 0; i < a.size(); ++i) { QCOMPARE(b.at(i).name, a.at(i).name); QCOMPARE(b.at(i).query, a.at(i).query); - QCOMPARE(b.at(i).pinned, a.at(i).pinned); QCOMPARE(b.at(i).account, a.at(i).account); } } @@ -1929,9 +1923,11 @@ void TestConfig::migrationWritesJsonAndLeavesTheIniByteIdentical() QVERIFY(afterBytes.contains("; a comment the user wrote")); } -/// A migrated query that was not pinned would vanish from the query row, which -/// on the first launch after an upgrade looks like data loss. -void TestConfig::migrationPinsEveryEntry() +/// Every [queries] entry must survive the migration with its query intact. It +/// used to also assert they came across PINNED, which item 94 retired along +/// with the flag; losing an entry is the data loss that assertion was really +/// guarding against, so that half is kept. +void TestConfig::migrationKeepsEveryEntry() { QTemporaryDir dir; const QString path = writeIni(dir, QStringLiteral( @@ -1945,9 +1941,14 @@ void TestConfig::migrationPinsEveryEntry() const QList<SavedQuery> queries = config.savedQueries(); QCOMPARE(queries.size(), 2); + + QMap<QString, QString> byName; for (const SavedQuery &query : queries) - QVERIFY2(query.pinned, qPrintable( - QStringLiteral("migrated query '%1' is not pinned").arg(query.name))); + byName.insert(query.name, query.query); + QCOMPARE(byName.value(QStringLiteral("Inbox")), + QStringLiteral("tag:inbox")); + QCOMPARE(byName.value(QStringLiteral("Unread")), + QStringLiteral("tag:unread")); } /// Once the JSON exists, [queries] is dead. Two sources of truth was the @@ -1972,17 +1973,19 @@ void TestConfig::jsonWinsOnceItExists() QCOMPARE(queries.at(0).name, QStringLiteral("FromTheJson")); } -void TestConfig::aStoredGeneratedQueryIsUnpinnedNotDropped() +void TestConfig::aStoredGeneratedQueryIsKeptNotDropped() { // An existing install carries a Sent entry in queries.json: 0.19.0 migrated // the hardcoded button into one. Item 93 ships Sent as a built-in filter, - // so that stored entry is now a DUPLICATE and would put two Sent buttons on - // the row, one editable and one not. + // so that stored entry is now a DUPLICATE. // - // Unpinned rather than deleted. This file's whole design is that a reader - // preserves what it does not own, and the user's instruction for their own - // redundant queries was the same: fold them into the menu, do not drop - // them. An unpin is reversible from the UI; a delete is not. + // It used to be UNPINNED to keep it off the row beside the built-in of the + // same name. Item 94 removed the row for saved queries entirely, so the + // duplicate can no longer collide with anything and there is nothing to + // unpin. What must still hold is that it is KEPT: this file's whole design + // is that a reader preserves what it does not own, and the user's + // instruction for their own redundant queries was to fold them into the + // menu rather than drop them. QTemporaryDir dir; const QString path = writeIni(dir, QStringLiteral( "[account.work]\n" @@ -1991,8 +1994,8 @@ void TestConfig::aStoredGeneratedQueryIsUnpinnedNotDropped() writeQueries(dir, QStringLiteral(R"({ "version": 1, "queries": [ - { "name": "Sent", "generated": "sent", "pinned": true }, - { "name": "Mine", "query": "tag:todo", "pinned": true } + { "name": "Sent", "generated": "sent" }, + { "name": "Mine", "query": "tag:todo" } ] })")); @@ -2004,21 +2007,21 @@ void TestConfig::aStoredGeneratedQueryIsUnpinnedNotDropped() bool sawSent = false; for (const SavedQuery &query : queries) { - if (query.generated != QStringLiteral("sent")) - continue; - sawSent = true; - QVERIFY2(!query.pinned, - "the stored Sent entry is still a button beside the built-in " - "filter of the same name"); + if (query.generated == QStringLiteral("sent")) + sawSent = true; } - QVERIFY2(sawSent, "the stored Sent entry was DROPPED rather than unpinned"); + QVERIFY2(sawSent, "the stored Sent entry was DROPPED rather than kept"); - // The user's own query is untouched: only the entry duplicating a built-in - // filter is unpinned. + // And the user's own query beside it, so the assertion above is not passing + // on a reader that kept everything by doing nothing at all. + bool sawMine = false; for (const SavedQuery &query : queries) { - if (query.name == QStringLiteral("Mine")) - QVERIFY2(query.pinned, "an unrelated pinned query was unpinned"); + if (query.name == QStringLiteral("Mine")) { + sawMine = true; + QCOMPARE(query.query, QStringLiteral("tag:todo")); + } } + QVERIFY2(sawMine, "an unrelated saved query was lost"); } void TestConfig::theStartupQueryPrefersASavedQueryOverAFilterOfTheSameName() diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index c5e88db..f935cac 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -434,22 +434,20 @@ private slots: void theMessageBarSitsAboveTheBodyAndBelowTheHeader(); void theMessageBarIconsAreSmallerThanTheToolbars(); - void onlyPinnedQueriesBecomeButtons(); - void unpinnedQueriesReachTheMenu(); - void pinnedButtonsFollowTheDocumentOrder(); - void theSavedQueryMenuIsHiddenWhenEveryQueryIsPinned(); + void everySavedQueryLivesInTheMenu(); + void menuEntriesFollowTheDocumentOrder(); void aScopedSavedQuerySelectsItsAccount(); void anUnscopedSavedQueryClearsTheAccount(); void theSaveQueryActionIsDisabledOnAnEmptyQuery(); void thereIsASaveButtonBesideTheQueryBar(); void theMenuIsRightAlignedAwayFromTheButtons(); - void theRowSurvivesWithNothingButUnpinnedQueries(); + void theRowSurvivesWithNothingButAMenu(); void aStoredGeneratedQueryRunsFlatAndComposed(); void aRenamedSentEntryKeepsWorking(); void aGeneratedQueryWithNothingToShowIsSkipped(); - void aSavedQueryButtonOffersEditUnpinAndDelete(); + void aSavedQueryEntryOffersRunEditAndDelete(); void onlyAStoredQueryOffersToBecomeATaggingRule(); - void unpinningMovesAQueryToTheMenu(); + void savingStripsTheRetiredPinnedField(); void deletingRemovesTheQueryFromTheFile(); void anEditedQueryKeepsItsUnknownFields(); void renamingReplacesRatherThanDuplicating(); @@ -7637,46 +7635,44 @@ static QStringList savedQueryButtonLabels(MainWindow &window) /// Positional lookup does not work any more: the built-in filters occupy the /// first four places on the row, so row->findChild<QPushButton *>() returns /// Unread rather than the query a test means. -static QPushButton *savedQueryButton(MainWindow &window, const QString &label) +/// The submenu carrying one saved query's actions, which since item 94 is where +/// every saved query lives: the row is built-in filters only, so there is no +/// button to right-click and the entry's own submenu is the whole affordance. +static QMenu *savedQueryEntryMenu(MainWindow &window, const QString &label) { - auto *row = window.findChild<QWidget *>(QStringLiteral("savedQueryRow")); - if (!row) + auto *menuButton = + window.findChild<QPushButton *>(QStringLiteral("savedQueryMenuButton")); + if (!menuButton || !menuButton->menu()) return nullptr; - const QList<QPushButton *> buttons = - row->findChildren<QPushButton *>(QString(), Qt::FindDirectChildrenOnly); - for (QPushButton *button : buttons) { - if (isBuiltinFilterButton(button)) - continue; - if (button->text() == label) - return button; + for (QAction *action : menuButton->menu()->actions()) { + if (action->text() == label) + return action->menu(); } return nullptr; } -void TestMainWindow::onlyPinnedQueriesBecomeButtons() +/// One action from a saved query's submenu, by object name. The counterpart of +/// contextActionNamed() for the menu, which holds its actions directly rather +/// than as a context menu on a widget. +static QAction *savedQueryActionNamed(MainWindow &window, const QString &label, + const QString &objectName) { - QTemporaryDir dir; - QVERIFY(dir.isValid()); - Config config; - loadWithQueries(config, dir, QStringLiteral(R"({ - "version": 1, - "queries": [ - { "name": "Inbox", "query": "tag:inbox", "pinned": true }, - { "name": "Buried", "query": "tag:buried" } - ] - })")); - - MainWindow window(config); - const QStringList labels = savedQueryButtonLabels(window); - - QVERIFY2(labels.contains(QStringLiteral("Inbox")), - "a pinned query must have a button"); - QVERIFY2(!labels.contains(QStringLiteral("Buried")), - "an unpinned query must NOT have a button"); + QMenu *menu = savedQueryEntryMenu(window, label); + if (!menu) + return nullptr; + for (QAction *action : menu->actions()) { + if (action->objectName() == objectName) + return action; + } + return nullptr; } -void TestMainWindow::unpinnedQueriesReachTheMenu() +void TestMainWindow::everySavedQueryLivesInTheMenu() { + // Item 94. The row is built-in filters ONLY, and every saved query is in + // the menu whatever queries.json says: `pinned` is no longer read, so a + // file still carrying it from before the removal must not put a button + // back on the row. QTemporaryDir dir; QVERIFY(dir.isValid()); Config config; @@ -7689,48 +7685,35 @@ void TestMainWindow::unpinnedQueriesReachTheMenu() })")); MainWindow window(config); + + // No saved query is a button any more, INCLUDING the one the file pins. + // That entry is the whole point of the fixture: a test with nothing + // pinned passes against `pinned` still being honoured. + const QStringList labels = savedQueryButtonLabels(window); + QVERIFY2(!labels.contains(QStringLiteral("Inbox")), + "a saved query marked pinned in the file still became a button"); + QVERIFY2(!labels.contains(QStringLiteral("Buried")), + "a saved query became a button"); + auto *menuButton = window.findChild<QPushButton *>(QStringLiteral("savedQueryMenuButton")); - QVERIFY2(menuButton, "an unpinned query needs a menu to live in"); + QVERIFY2(menuButton, "the saved queries have no menu to live in"); QVERIFY(menuButton->menu()); QStringList entries; - const QList<QAction *> actions = menuButton->menu()->actions(); - for (QAction *action : actions) + for (QAction *action : menuButton->menu()->actions()) entries.append(action->text()); + QVERIFY2(entries.contains(QStringLiteral("Inbox")), + "the formerly pinned query is missing from the menu"); QVERIFY2(entries.contains(QStringLiteral("Buried")), "the unpinned query is missing from the menu"); - // A pinned query is already a button; listing it twice is the duplicate - // this asserts against. - QVERIFY2(!entries.contains(QStringLiteral("Inbox")), - "a pinned query must not also appear in the menu"); -} - -/// The property the whole storage change was made for. "Zebra" is written -/// first and must stay first; alphabetical order would put it last. -void TestMainWindow::pinnedButtonsFollowTheDocumentOrder() -{ - QTemporaryDir dir; - QVERIFY(dir.isValid()); - Config config; - loadWithQueries(config, dir, QStringLiteral(R"({ - "version": 1, - "queries": [ - { "name": "Zebra", "query": "tag:zebra", "pinned": true }, - { "name": "Apple", "query": "tag:apple", "pinned": true } - ] - })")); - - MainWindow window(config); - const QStringList labels = savedQueryButtonLabels(window); - - QCOMPARE(labels.size(), 2); - QCOMPARE(labels.at(0), QStringLiteral("Zebra")); - QCOMPARE(labels.at(1), QStringLiteral("Apple")); } -void TestMainWindow::theSavedQueryMenuIsHiddenWhenEveryQueryIsPinned() +/// Document order, not alphabetical: "Zebra" is written first and must stay +/// first. The property the storage change was made for, kept from the deleted +/// pinnedButtonsFollowTheDocumentOrder now that the menu is the only home. +void TestMainWindow::menuEntriesFollowTheDocumentOrder() { QTemporaryDir dir; QVERIFY(dir.isValid()); @@ -7738,23 +7721,23 @@ void TestMainWindow::theSavedQueryMenuIsHiddenWhenEveryQueryIsPinned() loadWithQueries(config, dir, QStringLiteral(R"({ "version": 1, "queries": [ - { "name": "Inbox", "query": "tag:inbox", "pinned": true } + { "name": "Zebra", "query": "tag:zebra" }, + { "name": "Apple", "query": "tag:apple" } ] })")); MainWindow window(config); - - // The guard the assertion below needs. Asserting only that the menu button - // is absent passed against NO implementation at all, before any of this - // was built, so it has to prove first that the row it is looking in was - // populated and that a button was found. - const QStringList labels = savedQueryButtonLabels(window); - QCOMPARE(labels, QStringList{ QStringLiteral("Inbox") }); - auto *menuButton = window.findChild<QPushButton *>(QStringLiteral("savedQueryMenuButton")); - QVERIFY2(!menuButton, - "an empty menu button is a control that always does nothing"); + QVERIFY(menuButton && menuButton->menu()); + + QStringList entries; + for (QAction *action : menuButton->menu()->actions()) + entries.append(action->text()); + + QCOMPARE(entries.size(), 2); + QCOMPARE(entries.at(0), QStringLiteral("Zebra")); + QCOMPARE(entries.at(1), QStringLiteral("Apple")); } /// The scope goes through the account dropdown rather than being baked into @@ -7796,11 +7779,12 @@ void TestMainWindow::aScopedSavedQuerySelectsItsAccount() QStringLiteral("personal"))); QCOMPARE(accountBox->currentData().toString(), QStringLiteral("personal")); - auto *row = window.findChild<QWidget *>(QStringLiteral("savedQueryRow")); - QVERIFY(row); - auto *button = savedQueryButton(window, QStringLiteral("Billing")); - QVERIFY(button); - button->click(); + // Through the menu's Run entry: since item 94 a saved query is never a + // button, and Qt emits no triggered for the entry that owns the submenu. + QAction *run = savedQueryActionNamed(window, QStringLiteral("Billing"), + QStringLiteral("runQuery")); + QVERIFY(run); + run->trigger(); QCOMPARE(accountBox->currentData().toString(), QStringLiteral("work")); // The text is the bare query. The path scope is applied once, by @@ -7840,9 +7824,10 @@ void TestMainWindow::anUnscopedSavedQueryClearsTheAccount() auto *row = window.findChild<QWidget *>(QStringLiteral("savedQueryRow")); QVERIFY(row); - auto *button = savedQueryButton(window, QStringLiteral("Everywhere")); - QVERIFY(button); - button->click(); + QAction *run = savedQueryActionNamed(window, QStringLiteral("Everywhere"), + QStringLiteral("runQuery")); + QVERIFY(run); + run->trigger(); QVERIFY2(accountBox->currentData().toString().isEmpty(), "an unscoped saved query must clear the account selection"); @@ -7970,9 +7955,9 @@ void TestMainWindow::theMenuIsRightAlignedAwayFromTheButtons() "the menu must come AFTER the stretch to sit at the right edge"); } -/// The row must not vanish when every saved query is unpinned: the menu is -/// then the only way to reach any of them, and hiding the row buries it. -void TestMainWindow::theRowSurvivesWithNothingButUnpinnedQueries() +/// The row must not vanish when the only content is the menu, which since item +/// 94 is where every saved query lives: hiding the row buries all of them. +void TestMainWindow::theRowSurvivesWithNothingButAMenu() { QTemporaryDir dir; QVERIFY(dir.isValid()); @@ -8077,9 +8062,8 @@ void TestMainWindow::aRenamedSentEntryKeepsWorking() found = true; QVERIFY2(saved.isGenerated(), "the entry lost its generator when renamed"); - QVERIFY2(!saved.pinned, "the entry was not unpinned"); } - QVERIFY2(found, "the renamed entry was DROPPED rather than unpinned"); + QVERIFY2(found, "the renamed entry was DROPPED rather than kept"); // The built-in Sent still resolves the same query, so nothing the user // could reach before became unreachable. @@ -8115,12 +8099,12 @@ void TestMainWindow::aGeneratedQueryWithNothingToShowIsSkipped() MainWindow window(config); - // The guard: the row was built and the other entry did get a button, so a - // missing Sent means it was skipped rather than that nothing was built. - QCOMPARE(savedQueryButtonLabels(window), - QStringList{ QStringLiteral("Inbox") }); - QVERIFY2(!window.findChild<QAbstractButton *>(QStringLiteral("sentButton")), - "a generated query with nothing to show must not get a button"); + // The guard: the menu was built and the other entry is in it, so a missing + // Sent means it was skipped rather than that nothing was built at all. + QVERIFY2(savedQueryEntryMenu(window, QStringLiteral("Inbox")), + "the menu was not built, so the assertion below proves nothing"); + QVERIFY2(!savedQueryEntryMenu(window, QStringLiteral("Sent")), + "a generated query with nothing to show must not get an entry"); } /// Reads queries.json back from disk, which is what "it was saved" means. @@ -8145,29 +8129,38 @@ static QAction *contextActionNamed(MainWindow &window, QWidget *target, return nullptr; } -void TestMainWindow::aSavedQueryButtonOffersEditUnpinAndDelete() +void TestMainWindow::aSavedQueryEntryOffersRunEditAndDelete() { + // Item 94 removed the pin action along with the row, so the entry offers + // three things rather than four. Run is among them and is load-bearing: + // Qt emits no triggered for the action owning the submenu, so without an + // item INSIDE it the query could not be run at all. QTemporaryDir dir; QVERIFY(dir.isValid()); Config config; loadWithQueries(config, dir, QStringLiteral(R"({ "version": 1, "queries": [ - { "name": "Inbox", "query": "tag:inbox", "pinned": true } + { "name": "Inbox", "query": "tag:inbox" } ] })")); MainWindow window(config); - auto *row = window.findChild<QWidget *>(QStringLiteral("savedQueryRow")); - QVERIFY(row); - auto *button = savedQueryButton(window, QStringLiteral("Inbox")); - QVERIFY(button); + QVERIFY2(savedQueryEntryMenu(window, QStringLiteral("Inbox")), + "the saved query has no menu entry to carry its actions"); - // A context menu, so the actions live on the widget itself. - QCOMPARE(button->contextMenuPolicy(), Qt::ActionsContextMenu); - QVERIFY(contextActionNamed(window, button, QStringLiteral("editQuery"))); - QVERIFY(contextActionNamed(window, button, QStringLiteral("pinQuery"))); - QVERIFY(contextActionNamed(window, button, QStringLiteral("deleteQuery"))); + QVERIFY(savedQueryActionNamed(window, QStringLiteral("Inbox"), + QStringLiteral("runQuery"))); + QVERIFY(savedQueryActionNamed(window, QStringLiteral("Inbox"), + QStringLiteral("editQuery"))); + QVERIFY(savedQueryActionNamed(window, QStringLiteral("Inbox"), + QStringLiteral("deleteQuery"))); + + // The retired affordance, asserted absent rather than merely unused: item + // 94 removed pinning, and an action left behind would still be clickable. + QVERIFY2(!savedQueryActionNamed(window, QStringLiteral("Inbox"), + QStringLiteral("pinQuery")), + "the retired pin action is still offered"); } void TestMainWindow::onlyAStoredQueryOffersToBecomeATaggingRule() @@ -8200,41 +8193,41 @@ void TestMainWindow::onlyAStoredQueryOffersToBecomeATaggingRule() "sent=work-mail/Sent\n")); MainWindow window(config); - auto *row = window.findChild<QWidget *>(QStringLiteral("savedQueryRow")); - QVERIFY(row); - - // By object name, which rebuildSavedQueryRow assigns precisely so a test - // need not depend on a label the user can rename. - auto *generated = row->findChild<QAbstractButton *>( - QStringLiteral("sentButton")); - - // savedQueryButton(), not a scan for the label: item 93 puts a BUILT-IN - // Inbox filter on the row too, and it carries no context actions by design, - // so a scan finds that one and the assertion below fails against correct - // code. - QPushButton *stored = savedQueryButton(window, QStringLiteral("Inbox")); - QVERIFY2(stored, "no button was built for the stored query"); - QVERIFY2(generated, "no button was built for the generated query"); + // Both are menu entries since item 94, so the comparison is between two + // entries rather than between a button and a built-in filter. The stored + // one is found by name, which the user can rename, but nothing else + // distinguishes an entry in the menu. + QVERIFY2(savedQueryEntryMenu(window, QStringLiteral("Inbox")), + "no menu entry was built for the stored query"); + QVERIFY2(savedQueryEntryMenu(window, QStringLiteral("Sent")), + "no menu entry was built for the generated query"); - QVERIFY2(contextActionNamed(window, stored, QStringLiteral("queryToRule")), + QVERIFY2(savedQueryActionNamed(window, QStringLiteral("Inbox"), + QStringLiteral("queryToRule")), "a stored query must offer Create tagging rule"); - QVERIFY2(!contextActionNamed(window, generated, - QStringLiteral("queryToRule")), + QVERIFY2(!savedQueryActionNamed(window, QStringLiteral("Sent"), + QStringLiteral("queryToRule")), "a generated query must not: its query is a snapshot"); - // The guard, and it has moved since item 93. It used to prove the generated - // button HAS a menu, so the assertion above was about one action rather - // than about a button with none. `generated` is now the BUILT-IN Sent - // filter, which correctly carries no actions at all, so proving the - // machinery works has to happen on the button that does have them. - QVERIFY2(contextActionNamed(window, stored, QStringLiteral("deleteQuery")), - "the stored button lost its other actions, so the assertion above " - "is not about queryToRule in particular"); + // The guard: without it, an entry carrying NO actions at all would pass the + // assertion above. Asserted on the generated entry, since that is the one + // the absence is claimed of. + QVERIFY2(savedQueryActionNamed(window, QStringLiteral("Sent"), + QStringLiteral("deleteQuery")), + "the generated entry lost its other actions, so the assertion " + "above is not about queryToRule in particular"); } -void TestMainWindow::unpinningMovesAQueryToTheMenu() +void TestMainWindow::savingStripsTheRetiredPinnedField() { + // Item 94, and the half that is NOT just deleting UI: the user chose to + // strip `pinned` rather than leave it ignored in the file. A key that is + // no longer read but still written back is the shape that makes a later + // reader disagree with this one. + // + // The fixture pins BOTH entries, so the assertion cannot pass by the field + // having been absent all along. QTemporaryDir dir; QVERIFY(dir.isValid()); Config config; @@ -8247,32 +8240,38 @@ void TestMainWindow::unpinningMovesAQueryToTheMenu() })")); MainWindow window(config); - auto *row = window.findChild<QWidget *>(QStringLiteral("savedQueryRow")); - QVERIFY(row); - QCOMPARE(savedQueryButtonLabels(window).size(), 2); - QVERIFY(!window.findChild<QAbstractButton *>( - QStringLiteral("savedQueryMenuButton"))); - auto *button = savedQueryButton(window, QStringLiteral("Inbox")); - QVERIFY(button); - QAction *pin = contextActionNamed(window, button, QStringLiteral("pinQuery")); - QVERIFY(pin); - pin->trigger(); + // Neither is a button any more, which is what makes the file the only + // place the retired field could still be observed. + QVERIFY(savedQueryButtonLabels(window).isEmpty()); - // Off the row, into the menu, and written to the file: an unpin that only - // redrew would come back pinned on the next launch. - QCOMPARE(savedQueryButtonLabels(window), QStringList{ QStringLiteral("Other") }); - auto *menuButton = - window.findChild<QPushButton *>(QStringLiteral("savedQueryMenuButton")); - QVERIFY(menuButton); - QCOMPARE(menuButton->menu()->actions().size(), 1); + // Any save rewrites the whole array, so editing one entry puts BOTH through + // the writer, including the one that was not touched. Through the testing + // seam rather than the Delete action, which raises a confirmation modal + // with nothing to dismiss it under the offscreen platform. + SavedQuery edited; + edited.name = QStringLiteral("Inbox"); + edited.query = QStringLiteral("tag:inbox and not tag:muted"); + window.replaceSavedQueryForTesting(QStringLiteral("Inbox"), edited); const QJsonArray stored = storedQueries(dir); QCOMPARE(stored.size(), 2); - QCOMPARE(stored.at(0).toObject().value(QStringLiteral("name")).toString(), - QStringLiteral("Inbox")); - QVERIFY2(!stored.at(0).toObject().contains(QStringLiteral("pinned")), - "the unpin did not reach the file"); + for (const QJsonValue &value : stored) { + const QJsonObject entry = value.toObject(); + QVERIFY2(!entry.contains(QStringLiteral("pinned")), + qPrintable(QStringLiteral("the retired pinned field was " + "written back for '%1'") + .arg(entry.value(QStringLiteral("name")) + .toString()))); + } + + // The untouched entry is otherwise intact: a strip that took the query with + // it would pass the loop above and lose the user's data. + const QJsonObject other = stored.at(1).toObject(); + QCOMPARE(other.value(QStringLiteral("name")).toString(), + QStringLiteral("Other")); + QCOMPARE(other.value(QStringLiteral("query")).toString(), + QStringLiteral("tag:other")); } void TestMainWindow::deletingRemovesTheQueryFromTheFile() @@ -8289,22 +8288,21 @@ void TestMainWindow::deletingRemovesTheQueryFromTheFile() })")); MainWindow window(config); - auto *row = window.findChild<QWidget *>(QStringLiteral("savedQueryRow")); - QVERIFY(row); - auto *button = savedQueryButton(window, QStringLiteral("Doomed")); - QVERIFY(button); - QCOMPARE(button->text(), QStringLiteral("Doomed")); + QVERIFY2(savedQueryEntryMenu(window, QStringLiteral("Doomed")), + "the query to be deleted is not in the menu"); - QAction *del = - contextActionNamed(window, button, QStringLiteral("deleteQuery")); + QAction *del = savedQueryActionNamed(window, QStringLiteral("Doomed"), + QStringLiteral("deleteQuery")); QVERIFY(del); // Destructive and not on the undo stack, so it confirms. Suppressed here // rather than driven through the modal dialog, which would hang the test. window.setConfirmDeleteForTesting(false); del->trigger(); - QCOMPARE(savedQueryButtonLabels(window), - QStringList{ QStringLiteral("Keeper") }); + QVERIFY2(!savedQueryEntryMenu(window, QStringLiteral("Doomed")), + "the deleted query is still in the menu"); + QVERIFY2(savedQueryEntryMenu(window, QStringLiteral("Keeper")), + "the surviving query was removed too"); const QJsonArray stored = storedQueries(dir); QCOMPARE(stored.size(), 1); @@ -8337,7 +8335,6 @@ void TestMainWindow::anEditedQueryKeepsItsUnknownFields() SavedQuery edited; edited.name = QStringLiteral("Inbox"); edited.query = QStringLiteral("tag:inbox and not tag:muted"); - edited.pinned = true; QVERIFY(edited.unknown.isEmpty()); window.replaceSavedQueryForTesting(QStringLiteral("Inbox"), edited); @@ -8371,14 +8368,19 @@ void TestMainWindow::renamingReplacesRatherThanDuplicating() SavedQuery renamed; renamed.name = QStringLiteral("New"); renamed.query = QStringLiteral("tag:old"); - renamed.pinned = true; window.replaceSavedQueryForTesting(QStringLiteral("Old"), renamed); const QJsonArray stored = storedQueries(dir); QCOMPARE(stored.size(), 1); QCOMPARE(stored.at(0).toObject().value(QStringLiteral("name")).toString(), QStringLiteral("New")); - QCOMPARE(savedQueryButtonLabels(window), QStringList{ QStringLiteral("New") }); + + // And the menu followed the rename rather than keeping both, which is the + // duplication this asserts against. + QVERIFY2(savedQueryEntryMenu(window, QStringLiteral("New")), + "the renamed query is missing from the menu"); + QVERIFY2(!savedQueryEntryMenu(window, QStringLiteral("Old")), + "the old name is still in the menu, so the rename duplicated"); } void TestMainWindow::aMalformedAccountIsReportedWithoutBlockingTheConstructor() @@ -9999,19 +10001,14 @@ void TestMainWindow::aSavedQueryStillClearsTheAccountSelection() MainWindow window(config); window.selectAccountForTesting(QStringLiteral("work")); - // The migrated [queries] entry is pinned, so it is a button beside the - // filters. Found by its label, since only the filters have stable object - // names. - QPushButton *mine = nullptr; - auto *row = window.findChild<QWidget *>(QStringLiteral("savedQueryRow")); - QVERIFY(row); - for (QPushButton *button : row->findChildren<QPushButton *>()) { - if (button->text() == QStringLiteral("Mine")) - mine = button; - } - QVERIFY2(mine, "the user's own pinned query is not on the row"); + // The migrated [queries] entry lives in the menu since item 94, like every + // saved query. Run rather than the entry itself: Qt emits no triggered for + // an action that owns a submenu. + QAction *run = savedQueryActionNamed(window, QStringLiteral("Mine"), + QStringLiteral("runQuery")); + QVERIFY2(run, "the user's own query is not in the menu"); - mine->click(); + run->trigger(); QVERIFY2(window.selectedAccountForTesting().isEmpty(), "an unscoped saved query no longer clears the account selection"); } diff --git a/translations/qtmaildir_it_IT.ts b/translations/qtmaildir_it_IT.ts index f34b493..1b0e756 100644 --- a/translations/qtmaildir_it_IT.ts +++ b/translations/qtmaildir_it_IT.ts @@ -1028,14 +1028,6 @@ Il messaggio È stato inviato. Non inviarlo di nuovo.</translation> <translation>Modifica...</translation> </message> <message> - <source>Move to menu</source> - <translation>Sposta nel menu</translation> - </message> - <message> - <source>Show as a button</source> - <translation>Mostra come pulsante</translation> - </message> - <message> <source>Create tagging rule...</source> <translation>Crea regola di etichettatura...</translation> </message> @@ -1620,10 +1612,6 @@ Il messaggio È stato inviato. Non inviarlo di nuovo.</translation> <translation>Account</translation> </message> <message> - <source>Show as a button</source> - <translation>Mostra come pulsante</translation> - </message> - <message> <source>A saved query named '%1' already exists and will be replaced.</source> <translation>Esiste già una ricerca salvata di nome '%1' e verrà sostituita.</translation> </message> |
