diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-19 11:34:58 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-19 11:34:58 +0200 |
| commit | e187db1b84fadaf67bb8bee91460fbaea6ce8efb (patch) | |
| tree | c28d3b866f9289afab83254619970f25d32643f4 /tests | |
| parent | 41d0b94dcd2b4208e5d3e7483c88ce9b664ce565 (diff) | |
| download | qtmaildir-e187db1b84fadaf67bb8bee91460fbaea6ce8efb.tar.gz qtmaildir-e187db1b84fadaf67bb8bee91460fbaea6ce8efb.zip | |
feat(pane): move the copy confirmation into the message pane
The user's preference after seeing item 115 ship: a small transient in the
bottom right of the pane with a checkmark, rather than a status bar message at
the far end of the window. A copy happens in the pane, so the confirmation
belongs there.
Three properties are load-bearing and each has a mutation that fails. The toast
is a hand-placed CHILD rather than a layout item, because it floats over the
message instead of taking a strip away from it: nothing reflows when it appears
and the text just copied does not jump. That is why resizeEvent() is overridden,
since a hand-placed child does not follow its parent. It is autoFillBackground
and painted from the theme's ToolTipBase/ToolTipText, so it stays readable over
a rendered message and follows the desktop theme the way the document already
does. And its timer is restarted rather than started, so a second copy gets its
own full reading time instead of inheriting what is left of the first.
The resize test was wrong on its first draft and passed against the mutation it
exists to catch. It grew the pane, which moves the right and bottom edges away,
so a toast left at its old position still satisfied "inside the pane"; measured
green with the reposition deleted. It shrinks now, where a stale position lands
outside the new rect, which is also what the user would see.
No new strings: the four messages are unchanged, only where they appear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_messageview.cpp | 126 |
1 files changed, 119 insertions, 7 deletions
diff --git a/tests/test_messageview.cpp b/tests/test_messageview.cpp index df41dd1..3d21536 100644 --- a/tests/test_messageview.cpp +++ b/tests/test_messageview.cpp @@ -60,6 +60,8 @@ private slots: void theBodyMenuDropsTheBrowsersOwnActions(); void theBodyMenuOffersSelectAll(); void aCopyFromThePaneReportsWhatWasCopied(); + void theCopyToastAppearsOverThePaneAndFades(); + void theCopyToastStaysAnchoredWhenThePaneResizes(); void aSearchFromTheDetailsDialogClosesIt(); private: @@ -825,7 +827,13 @@ void TestMessageView::aCopyFromThePaneReportsWhatWasCopied() { // Item 115. Copy link address, Copy image address and Copy image all work // and none of them said so. Chromium does not report success, so the pane - // listens to its actions and emits the pane's own status message. + // listens to its actions and shows its own confirmation. + // + // Reported through the in-pane TOAST since the user asked for it there + // rather than in the status bar: a copy happens in the pane, and the + // status bar is at the other end of the window. This test is about the + // four entries each saying something DIFFERENT; where it is displayed is + // theCopyToastAppearsOverThePaneAndFades(). // // Unlike item 117's entry, this IS fully testable: the connections are made // to the page's own QActions in the constructor, so triggering one runs the @@ -834,8 +842,8 @@ void TestMessageView::aCopyFromThePaneReportsWhatWasCopied() auto *page = view.findChild<QWebEnginePage *>(); QVERIFY2(page, "no page, so this test would assert nothing"); - QSignalSpy spy(&view, &MessageView::statusMessage); - QVERIFY(spy.isValid()); + auto *toast = view.findChild<QLabel *>(QStringLiteral("copyToast")); + QVERIFY2(toast, "there is no copy toast at all"); // Each entry names WHAT was copied. "Copied" alone is worse than nothing // when three entries sit together in one menu, which the item states as a @@ -858,12 +866,11 @@ void TestMessageView::aCopyFromThePaneReportsWhatWasCopied() // nothing while looking thorough. action->setEnabled(true); - spy.clear(); + toast->clear(); action->trigger(); - QTRY_VERIFY_WITH_TIMEOUT(spy.count() == 1, 5000); - const QString message = spy.takeFirst().at(0).toString(); - QVERIFY2(!message.isEmpty(), "a copy reported an empty status message"); + QTRY_VERIFY_WITH_TIMEOUT(!toast->text().isEmpty(), 5000); + const QString message = toast->text(); seen.append(message); } @@ -872,6 +879,111 @@ void TestMessageView::aCopyFromThePaneReportsWhatWasCopied() QCOMPARE(QSet<QString>(seen.cbegin(), seen.cend()).size(), copies.size()); } +void TestMessageView::theCopyToastAppearsOverThePaneAndFades() +{ + // The user's preference, given after item 115 shipped the status-bar + // version: "a small transient with a checkmark in the bottom right of the + // message pane". A copy happens IN the pane, and the status bar is at the + // other end of the window, so the confirmation was landing far from the + // gesture that caused it. + MessageView view; + view.resize(600, 400); + view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + auto *page = view.findChild<QWebEnginePage *>(); + auto *toast = view.findChild<QLabel *>(QStringLiteral("copyToast")); + QVERIFY2(page, "no page, so this test would assert nothing"); + QVERIFY2(toast, "there is no copy toast at all"); + + // Hidden until something is copied: a confirmation that is always visible + // confirms nothing. + QVERIFY2(!toast->isVisible(), "the toast is showing before anything was copied"); + + QAction *copy = page->action(QWebEnginePage::CopyLinkToClipboard); + QVERIFY(copy); + // Chromium disables a copy action when there is nothing of that kind under + // the cursor, and trigger() on a disabled QAction emits nothing at all. + copy->setEnabled(true); + copy->trigger(); + + QTRY_VERIFY_WITH_TIMEOUT(toast->isVisible(), 5000); + // It says WHAT was copied, not merely that something was, which is the + // constraint item 115 already carried: three copy entries sit together in + // one menu. + QVERIFY2(toast->text().contains(QStringLiteral("link")), + qPrintable(QStringLiteral("the toast says '%1'").arg(toast->text()))); + + // Opaque and theme-coloured. A transparent label over a rendered message + // is unreadable against exactly the content it is confirming, and a + // geometry assertion cannot see that: the rect is correct either way. + QVERIFY2(toast->autoFillBackground(), + "the toast is transparent, so it reads over the message body"); + QCOMPARE(toast->palette().color(QPalette::Window), + view.palette().color(QPalette::ToolTipBase)); + + // Bottom right of the pane, inside it rather than beside it. + const QRect paneRect = view.rect(); + const QRect toastRect = toast->geometry(); + QVERIFY2(toastRect.right() <= paneRect.right(), + "the toast hangs off the right edge of the pane"); + QVERIFY2(toastRect.bottom() <= paneRect.bottom(), + "the toast hangs off the bottom edge of the pane"); + QVERIFY2(toastRect.center().x() > paneRect.center().x(), + "the toast is not in the right half of the pane"); + QVERIFY2(toastRect.center().y() > paneRect.center().y(), + "the toast is not in the bottom half of the pane"); + + // And it goes away on its own. Transient is the whole point: a + // confirmation the user has to dismiss is worse than none. + QTRY_VERIFY_WITH_TIMEOUT(!toast->isVisible(), + int(MessageView::kToastMs) + 4000); +} + +void TestMessageView::theCopyToastStaysAnchoredWhenThePaneResizes() +{ + // A manually positioned child does not follow its parent, unlike a widget + // in a layout. The toast cannot BE in the layout, since it floats over the + // web view rather than taking space from it, so the anchoring is this + // class's job and a resize is where that breaks. + MessageView view; + view.resize(600, 400); + view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + auto *page = view.findChild<QWebEnginePage *>(); + auto *toast = view.findChild<QLabel *>(QStringLiteral("copyToast")); + QVERIFY(page && toast); + + QAction *copy = page->action(QWebEnginePage::Copy); + QVERIFY(copy); + copy->setEnabled(true); + copy->trigger(); + QTRY_VERIFY_WITH_TIMEOUT(toast->isVisible(), 5000); + + // SHRUNK, not grown, and that distinction is the whole test. Growing the + // pane moves its right and bottom edges AWAY, so a toast left at the old + // position still satisfies "inside the pane" and the assertions below pass + // against a toast that never moved. Measured: with the reposition deleted, + // a 600x400 -> 900x700 resize left this test green. + // + // Shrinking puts the stale position outside the new rect, which is also + // what the user would actually see: a confirmation half off the pane. + view.resize(360, 240); + // The guard: the pane really did change size, so the assertion below is + // about the toast following rather than about nothing having moved. + QTRY_COMPARE_WITH_TIMEOUT(view.width(), 360, 5000); + + const QRect paneRect = view.rect(); + const QRect toastRect = toast->geometry(); + QVERIFY2(toastRect.right() <= paneRect.right(), + "the toast did not follow the pane's right edge"); + QVERIFY2(toastRect.center().x() > paneRect.center().x(), + "the toast is stranded in the left half after a resize"); + QVERIFY2(toastRect.center().y() > paneRect.center().y(), + "the toast is stranded in the top half after a resize"); +} + void TestMessageView::aSearchFromTheDetailsDialogClosesIt() { // The dialog is modal. Without closing it, the query runs and the thread |
