aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_mainwindow.cpp164
1 files changed, 164 insertions, 0 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index a6aa7f5..c5e88db 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -488,6 +488,7 @@ private slots:
void ctrlWClosesTheComposer();
void aDraftReopensWithItsOwnContent();
void editDraftIsOfferedOnlyForADraft();
+ void theMessageBarOffersEditOnADraft();
void doubleClickingADraftOpensTheComposer();
void aResumedDraftReplacesItsFileRatherThanAddingOne();
void aResumedDraftKeepsItsBlindRecipients();
@@ -12595,6 +12596,169 @@ void TestMainWindow::editDraftIsOfferedOnlyForADraft()
"Edit draft is not offered on a message in the drafts folder");
}
+void TestMainWindow::theMessageBarOffersEditOnADraft()
+{
+ // Item 157, and the half item 153 did not close. A draft was editable by
+ // double-click and by a Message-menu entry, neither of which is where the
+ // user looks while reading one. The pane's own bar carries Reply and
+ // Forward, which are the two things a draft cannot do: it has no sender to
+ // answer and is not finished enough to pass on.
+ WorkerComposeFixture fixture;
+ QVERIFY(fixture.backed.fixture().addMessage(
+ QStringLiteral("acct/Drafts"), QStringLiteral("draft1@example.org"),
+ QStringLiteral("Half written"), QStringLiteral("you@example.org"),
+ QStringLiteral("Fri, 14 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("Body.")));
+ QVERIFY2(fixture.seed({ { QStringLiteral("acct"), QStringLiteral("acct"),
+ QStringLiteral("Trash"),
+ QStringLiteral("/bin/true"),
+ QStringLiteral("you@example.org"),
+ QStringLiteral("Drafts") } },
+ QStringLiteral("acct/inbox")),
+ qPrintable(fixture.backed.error()));
+
+ MainWindow window(fixture.backed.config());
+ auto *model = window.findChild<ThreadListModel *>();
+ auto *view = window.findChild<ThreadListView *>();
+ auto *queryEdit =
+ window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
+ // Pinned by name: since item 141 the window holds two toolbars, and an
+ // unnamed findChild would assert against whichever came first.
+ auto *bar = window.findChild<QToolBar *>(QStringLiteral("message_toolbar"));
+ auto *pane = window.findChild<MessageView *>();
+ QVERIFY(model && view && queryEdit && bar && pane);
+
+ const auto selectById = [&](const QString &id) {
+ queryEdit->setText(QStringLiteral("id:") + id);
+ queryEdit->returnPressed();
+ bool ready = false;
+ for (int attempt = 0; attempt < 150 && !ready; ++attempt) {
+ ready = model->rowCount(QModelIndex()) == 1
+ && !window.mailRootForTesting().isEmpty();
+ if (!ready)
+ QTest::qWait(100);
+ }
+ if (!ready)
+ return false;
+ view->setCurrentIndex(model->index(0, 0, QModelIndex()));
+ return true;
+ };
+
+ const auto barHolds = [&](const QString &name) {
+ const auto actions = bar->actions();
+ return std::any_of(actions.cbegin(), actions.cend(),
+ [&](const QAction *action) {
+ return action && action->objectName() == name;
+ });
+ };
+
+ // Ordinary mail first, so the assertions below mean something: a bar that
+ // never holds Edit passes the draft check by accident if the reply pair is
+ // simply absent everywhere.
+ QVERIFY2(selectById(QStringLiteral("compose1@example.org")),
+ "the inbox message was not found");
+ QVERIFY2(barHolds(QStringLiteral("reply")),
+ "the message bar lost Reply on ordinary mail");
+ QVERIFY2(barHolds(QStringLiteral("forward")),
+ "the message bar lost Forward on ordinary mail");
+ QVERIFY2(!barHolds(QStringLiteral("edit_draft")),
+ "Edit draft is on the message bar for a message in the inbox");
+
+ // toggle_html is the view-control half, on the far side of the stretch. It
+ // describes how the pane renders and not what the message is, so it must
+ // survive the swap: a draft can be read as HTML like anything else.
+ QVERIFY2(barHolds(QStringLiteral("toggle_html")),
+ "the view controls were lost from the message bar");
+
+ QVERIFY2(selectById(QStringLiteral("draft1@example.org")),
+ "the draft was not found");
+ QVERIFY2(barHolds(QStringLiteral("edit_draft")),
+ "Edit draft is missing from the message bar on a draft");
+ QVERIFY2(!barHolds(QStringLiteral("reply")),
+ "Reply is still offered on a draft, which has nobody to answer");
+ QVERIFY2(!barHolds(QStringLiteral("forward")),
+ "Forward is still offered on a draft");
+ QVERIFY2(barHolds(QStringLiteral("toggle_html")),
+ "the view controls were lost when the bar swapped to a draft");
+
+ // And back, because a one-way swap is the plausible defect: the bar is
+ // refilled on every selection change, so returning to ordinary mail has to
+ // restore the pair rather than leaving Edit behind on mail it must not act
+ // on.
+ QVERIFY2(selectById(QStringLiteral("compose1@example.org")),
+ "the inbox message was not found on the way back");
+ QVERIFY2(barHolds(QStringLiteral("reply")),
+ "Reply did not come back after leaving a draft");
+ QVERIFY2(!barHolds(QStringLiteral("edit_draft")),
+ "Edit draft stayed on the bar after leaving the draft");
+
+ // And the gesture the first version of this test could not see. Running a
+ // query blanks the pane WITHOUT moving the selection, so currentIndex()
+ // stays valid on a row from the previous result and a bar keyed on it
+ // describes a message that is no longer displayed. Both directions were
+ // reported: Edit draft left over an empty pane after leaving Drafts, and
+ // the reply pair left over one after arriving.
+ const auto runQuery = [&](const QString &query) {
+ queryEdit->setText(query);
+ queryEdit->returnPressed();
+ QTest::qWait(300);
+ };
+
+ QVERIFY2(selectById(QStringLiteral("draft1@example.org")),
+ "the draft was not found before the query change");
+ QVERIFY2(barHolds(QStringLiteral("edit_draft")), "precondition: on a draft");
+
+ // A query with RESULTS, which is what the user reported and what a query
+ // matching nothing cannot reproduce: with no rows the selection goes
+ // invalid and the stale-index answer is accidentally right. Here the list
+ // repopulates, currentIndex() lands on a row of the NEW result, and the
+ // pane is still blank because nothing has been selected by hand.
+ runQuery(QStringLiteral("tag:inbox"));
+ QVERIFY2(model->rowCount(QModelIndex()) > 0,
+ "the blanking query returned nothing, which is the case that "
+ "cannot reproduce the defect");
+
+ // The whole bar goes with the pane, which is the shape the user settled on
+ // after looking at the greyed-out one: the subject and the details button
+ // already vanish when the pane is cleared, and a persisting action bar was
+ // the only piece of header furniture that did not.
+ QVERIFY2(bar->isHidden(),
+ "the message bar is still shown over a blank pane");
+
+ // Enablement is a separate property from visibility and was ALSO stale:
+ // updateComposeActions() ran only from the selection handlers, so a query
+ // that blanked the pane left Reply enabled. A hidden bar would hide that,
+ // but the Message menu shows the same QAction.
+ auto *reply = window.findChild<QAction *>(QStringLiteral("reply"));
+ QVERIFY(reply);
+ QVERIFY2(!reply->isEnabled(), "Reply is enabled over a blank pane");
+
+ // The other direction, and the one the user reported second: arriving at
+ // Drafts from the inbox left the reply pair over the blank pane. Selecting
+ // a draft after the query must reach Edit draft, which it cannot if the
+ // bar is only refilled on a selection change.
+ QVERIFY2(selectById(QStringLiteral("draft1@example.org")),
+ "the draft was not found after the blanking query");
+
+ // The FIRST message opened after a blanking, which is the gesture the
+ // hiding half broke: nothing refills the bar when a message arrives, so a
+ // bar hidden by the query stayed hidden until a SECOND selection, where
+ // m_items still held the first message and the guard passed one behind.
+ // Asserted before the membership checks below, since a bar that is filled
+ // correctly and invisible passes every one of them.
+ // The pane loads through the worker, so the render lands a turn or more
+ // after the selection. Waiting on the pane itself rather than on a fixed
+ // delay, which passes when the render never arrives.
+ QTRY_VERIFY_WITH_TIMEOUT(!pane->showingPlaceholder(), 15000);
+ QVERIFY2(!bar->isHidden(),
+ "the message bar stayed hidden for the first message opened "
+ "after the pane was blanked");
+ QVERIFY2(barHolds(QStringLiteral("edit_draft")),
+ "Edit draft did not return after the pane was blanked");
+ QVERIFY2(!barHolds(QStringLiteral("reply")),
+ "Reply is offered on a draft reached through a blank pane");
+}
+
void TestMainWindow::aDraftReopensWithItsOwnContent()
{
// Item 153. A draft was write-only: DraftStore had a write() and no