aboutsummaryrefslogtreecommitdiffstats
path: root/src/m_email/test/EMailViewLayoutTest.cpp
diff options
context:
space:
mode:
authorsaturneric <[email protected]>2026-09-29 19:51:52 +0200
committersaturneric <[email protected]>2026-09-29 19:51:52 +0200
commit391168fb8e5f353a318026278c890981c440ad5d (patch)
tree62edd8da294dceefd1f205990f83e70e309329db /src/m_email/test/EMailViewLayoutTest.cpp
parentstyle(i18n): normalize instant messaging catalogs (diff)
downloadmodules-391168fb8e5f353a318026278c890981c440ad5d.tar.gz
modules-391168fb8e5f353a318026278c890981c440ad5d.zip
feat(email): redesign action and attachment bars
- centralize message commands as reusable actions with shared shortcuts - add compact attachment summaries, menus, and expandable lists - move secondary commands into More and show read-only status separately - centralize action, attachment, security, and surface layout decisions - enable compact Status Panel behavior for the email editor
Diffstat (limited to 'src/m_email/test/EMailViewLayoutTest.cpp')
-rw-r--r--src/m_email/test/EMailViewLayoutTest.cpp228
1 files changed, 228 insertions, 0 deletions
diff --git a/src/m_email/test/EMailViewLayoutTest.cpp b/src/m_email/test/EMailViewLayoutTest.cpp
index 65fe753..beae5ce 100644
--- a/src/m_email/test/EMailViewLayoutTest.cpp
+++ b/src/m_email/test/EMailViewLayoutTest.cpp
@@ -28,6 +28,7 @@
#include <gtest/gtest.h>
+#include "EMailModel.h"
#include "EMailViewLayout.h"
// The details inspector's rules, checked without a widget. The view itself
@@ -99,4 +100,231 @@ TEST(EMailViewLayoutTest, SecurityIsOnlyReDerivedWhereItCanBeSeen) {
EXPECT_FALSE(EMailNeedsSecurityRefresh(false, false));
}
+// ------------------------------------------------------ attachment section
+
+auto Bar(int count, int selected, bool editable, bool expanded) {
+ return EMailAttachmentBar({count, selected, editable, expanded});
+}
+
+TEST(EMailViewLayoutTest, NoAttachmentsTakeNoRoomInEitherMode) {
+ // Composing, Attach is on the action row; the bar has nothing to say.
+ for (const bool editable : {false, true}) {
+ const auto s = Bar(0, 0, editable, true);
+ EXPECT_FALSE(s.section_visible);
+ EXPECT_FALSE(s.list_visible);
+ EXPECT_FALSE(s.toggle_visible);
+ }
+}
+
+TEST(EMailViewLayoutTest, OneAttachmentIsACompleteCompactRow) {
+ const auto s = Bar(1, 0, false, false);
+ EXPECT_TRUE(s.section_visible);
+ EXPECT_FALSE(s.list_visible) << "one row, not a table";
+ EXPECT_TRUE(s.toggle_visible);
+ // The one part is the target without a selection.
+ EXPECT_TRUE(s.can_open);
+ EXPECT_TRUE(s.can_save);
+}
+
+TEST(EMailViewLayoutTest, SeveralAttachmentsExpandIntoTheList) {
+ EXPECT_FALSE(Bar(3, 0, false, false).list_visible);
+ EXPECT_TRUE(Bar(3, 0, false, false).toggle_visible);
+ EXPECT_TRUE(Bar(3, 0, false, true).list_visible);
+}
+
+TEST(EMailViewLayoutTest, ReadOnlyMenusLeaveOutEditActions) {
+ for (const auto& s : {Bar(1, 1, false, true), Bar(4, 2, false, true),
+ Bar(4, 0, false, false)}) {
+ EXPECT_FALSE(s.show_attach) << "left out, not greyed";
+ EXPECT_FALSE(s.show_remove);
+ EXPECT_TRUE(s.show_save_all);
+ EXPECT_TRUE(s.can_save_all) << "saving out is what a received part is for";
+ EXPECT_TRUE(s.show_open);
+ EXPECT_TRUE(s.show_save);
+ }
+}
+
+TEST(EMailViewLayoutTest, ComposingMenusOfferAttachAndRemove) {
+ const auto s = Bar(2, 0, true, false);
+ EXPECT_TRUE(s.show_attach);
+ EXPECT_TRUE(s.can_attach);
+ EXPECT_TRUE(s.show_remove);
+ EXPECT_FALSE(s.can_remove) << "a selection in a collapsed list is hidden";
+ EXPECT_TRUE(Bar(2, 1, true, true).can_remove);
+ EXPECT_TRUE(Bar(1, 0, true, false).can_remove)
+ << "the one part is the target without a selection";
+ EXPECT_FALSE(Bar(0, 0, true, false).show_remove) << "nothing to remove";
+ EXPECT_TRUE(Bar(0, 0, true, false).show_attach);
+}
+
+TEST(EMailViewLayoutTest, MenuEntriesFollowTheTargets) {
+ const auto none = Bar(3, 0, true, true);
+ EXPECT_FALSE(none.can_open);
+ EXPECT_FALSE(none.can_save);
+ EXPECT_FALSE(none.can_remove);
+ EXPECT_TRUE(none.can_save_all);
+
+ const auto one = Bar(3, 1, true, true);
+ EXPECT_TRUE(one.can_open);
+ EXPECT_TRUE(one.can_save);
+ EXPECT_TRUE(one.can_remove);
+
+ const auto two = Bar(3, 2, true, true);
+ EXPECT_FALSE(two.can_open) << "Open takes exactly one";
+ EXPECT_TRUE(two.can_save);
+ EXPECT_TRUE(two.can_remove);
+}
+
+// ------------------------------------------------------ action row
+
+/// is_message, draft, editable, lock offered, lock on, attachments.
+auto Actions(bool is_message, bool draft, bool editable, bool offered,
+ bool read_only, int attachments) {
+ return EMailActionBar(
+ {is_message, draft, editable, offered, read_only, attachments});
+}
+
+TEST(EMailViewLayoutTest, AReceivedMessageIsAnsweredFromTheRow) {
+ const auto s = Actions(true, false, false, false, false, 0);
+ EXPECT_TRUE(s.reply_visible) << "Reply, Reply All and Forward";
+ EXPECT_FALSE(s.send_visible);
+ EXPECT_FALSE(s.attach_visible);
+ EXPECT_FALSE(s.more_reply) << "not twice";
+ EXPECT_TRUE(s.more_send) << "still reachable, in More";
+ EXPECT_TRUE(s.more_details);
+}
+
+TEST(EMailViewLayoutTest, ANewDraftIsSentFromTheRow) {
+ const auto s = Actions(false, true, true, false, false, 0);
+ EXPECT_FALSE(s.reply_visible) << "nothing to reply to: hidden, not greyed";
+ EXPECT_FALSE(s.more_reply) << "not even in More";
+ EXPECT_TRUE(s.send_visible);
+ EXPECT_TRUE(s.attach_visible);
+ EXPECT_FALSE(s.more_send) << "not twice";
+ EXPECT_FALSE(s.more_read_only);
+ EXPECT_TRUE(s.more_details);
+}
+
+TEST(EMailViewLayoutTest, AReplyBeingWrittenIsADraftThatCanStillBeAnswered) {
+ // Parsed as a message, but never sent: a reply or a forward in progress.
+ const auto s = Actions(true, true, true, true, false, 0);
+ EXPECT_TRUE(s.send_visible);
+ EXPECT_TRUE(s.attach_visible);
+ EXPECT_FALSE(s.reply_visible);
+ EXPECT_TRUE(s.more_reply) << "reachable, out of the way";
+ EXPECT_FALSE(s.more_read_only) << "there is no evidence in a draft to lock";
+}
+
+TEST(EMailViewLayoutTest, ALockedDraftHasNoAttachButStillSends) {
+ // Signed or encrypted by its author, before sending.
+ const auto s = Actions(true, true, false, false, false, 1);
+ EXPECT_TRUE(s.send_visible);
+ EXPECT_FALSE(s.attach_visible);
+ EXPECT_TRUE(s.attachments_menu_visible);
+}
+
+TEST(EMailViewLayoutTest, TheAttachmentsMenuExistsOnlyWithAttachments) {
+ EXPECT_FALSE(
+ Actions(true, false, false, false, false, 0).attachments_menu_visible);
+ EXPECT_TRUE(
+ Actions(true, false, false, false, false, 2).attachments_menu_visible);
+ EXPECT_TRUE(
+ Actions(false, true, true, false, false, 1).attachments_menu_visible);
+}
+
+TEST(EMailViewLayoutTest, ReadOnlyIsOfferedWhereItChangesSomething) {
+ EXPECT_TRUE(Actions(true, false, true, true, false, 0).more_read_only);
+ EXPECT_FALSE(Actions(true, false, false, false, false, 0).more_read_only)
+ << "a signed or encrypted message is protected already";
+ EXPECT_TRUE(Actions(true, false, false, false, true, 0).more_read_only)
+ << "while it is on, it can always be turned off";
+}
+
+TEST(EMailViewLayoutTest, ReadOnlyIsAStatusLabelWhileOn) {
+ EXPECT_TRUE(Actions(true, false, false, true, true, 0).read_only_label);
+ EXPECT_FALSE(Actions(true, false, true, true, false, 0).read_only_label);
+}
+
+TEST(EMailViewLayoutTest, EveryCommandIsReachableInEveryMode) {
+ for (const bool is_message : {false, true}) {
+ for (const bool draft : {false, true}) {
+ if (!is_message && !draft) continue; // not a message is always a draft
+ for (const bool editable : {false, true}) {
+ const auto s = Actions(is_message, draft, editable, true, false, 1);
+ EXPECT_TRUE(s.send_visible || s.more_send) << "Send";
+ EXPECT_TRUE(s.more_details) << "Details";
+ if (is_message) {
+ EXPECT_TRUE(s.reply_visible || s.more_reply) << "Reply";
+ }
+ EXPECT_FALSE(s.reply_visible && s.more_reply) << "never twice";
+ EXPECT_FALSE(s.send_visible && s.more_send) << "never twice";
+ }
+ }
+ }
+}
+
+TEST(EMailViewLayoutTest, TheSecurityStatusSaysOnlyWhatIsKnown) {
+ EXPECT_EQ(EMailSecurityStatus(EMailBadgeState::kENCRYPTED_ONLY, true).text,
+ QStringLiteral("Encrypted"));
+ EXPECT_EQ(
+ EMailSecurityStatus(EMailBadgeState::kSIGNED_UNVERIFIED, false).text,
+ QStringLiteral("Signature not checked"));
+ EXPECT_EQ(EMailSecurityStatus(EMailBadgeState::kSIGNED_UNVERIFIED, true).text,
+ QStringLiteral("Encrypted, signature not checked"));
+ EXPECT_EQ(EMailSecurityStatus(EMailBadgeState::kSIGNED_GOOD, false).text,
+ QStringLiteral("Signature verified"));
+ EXPECT_EQ(EMailSecurityStatus(EMailBadgeState::kSIGNED_BAD, false).text,
+ QStringLiteral("Bad signature"));
+ EXPECT_EQ(EMailSecurityStatus(EMailBadgeState::kNOT_PROTECTED, false).text,
+ QStringLiteral("Not signed or encrypted"));
+ // A verdict against the signature never wears the signature's icon.
+ for (const auto badge :
+ {EMailBadgeState::kSIGNED_BAD, EMailBadgeState::kSIGNED_ERROR,
+ EMailBadgeState::kSIGNED_MISMATCH, EMailBadgeState::kMALFORMED}) {
+ EXPECT_EQ(EMailSecurityStatus(badge, false).icon,
+ QStringLiteral(":/icons/warning.png"));
+ }
+}
+
+TEST(EMailViewLayoutTest, TargetsAreWhatTheUserCanSee) {
+ EXPECT_EQ(EMailAttachmentTargets(1, {}, false), QList<int>{0});
+ EXPECT_EQ(EMailAttachmentTargets(1, {}, true), QList<int>{0});
+ EXPECT_TRUE(EMailAttachmentTargets(3, {0, 2}, false).isEmpty())
+ << "a hidden selection is kept, not acted on";
+ EXPECT_EQ(EMailAttachmentTargets(3, {2, 0}, true), (QList<int>{2, 0}));
+ EXPECT_EQ(EMailAttachmentTargets(3, {1, 1, 7, -1}, true), QList<int>{1})
+ << "duplicates and stale rows are dropped";
+ EXPECT_TRUE(EMailAttachmentTargets(0, {0}, true).isEmpty());
+}
+
+TEST(EMailViewLayoutTest, CollapsingChangesOnlyWhatIsShown) {
+ // The same counts and selection, collapsed and expanded: nothing about the
+ // attachments themselves is an input, so nothing about them can change.
+ const auto open = Bar(3, 2, true, true);
+ const auto shut = Bar(3, 2, true, false);
+ EXPECT_TRUE(open.list_visible);
+ EXPECT_FALSE(shut.list_visible);
+ EXPECT_EQ(open.section_visible, shut.section_visible);
+ EXPECT_EQ(open.can_save_all, shut.can_save_all);
+ EXPECT_EQ(EMailAttachmentTargets(3, {0, 1}, true).size(), 2)
+ << "reopening brings the kept selection back as the target";
+}
+
+// ------------------------------------------------------ surface order
+
+TEST(EMailViewLayoutTest, TheNoticeSitsBelowTheBodyAndAttachments) {
+ const auto at = [](EMailSurfacePart part) {
+ for (size_t i = 0; i < kEMailSurfaceOrder.size(); ++i) {
+ if (kEMailSurfaceOrder[i] == part) return static_cast<int>(i);
+ }
+ return -1;
+ };
+ ASSERT_NE(at(EMailSurfacePart::kNOTICE), -1);
+ EXPECT_LT(at(EMailSurfacePart::kBODY), at(EMailSurfacePart::kATTACHMENTS));
+ EXPECT_LT(at(EMailSurfacePart::kATTACHMENTS), at(EMailSurfacePart::kNOTICE));
+ EXPECT_EQ(at(EMailSurfacePart::kNOTICE),
+ static_cast<int>(kEMailSurfaceOrder.size()) - 1)
+ << "last in the view, directly above the Status Panel";
+}
+
} // namespace