diff --git a/integration_test/robots/setting_robot.dart b/integration_test/robots/setting_robot.dart index 7390865ff..5226b1443 100644 --- a/integration_test/robots/setting_robot.dart +++ b/integration_test/robots/setting_robot.dart @@ -1,4 +1,6 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import '../base/core_robot.dart'; @@ -8,4 +10,25 @@ class SettingRobot extends CoreRobot { Future openLanguageMenuItem() async { await $(#setting_language_region).tap(); } + + Future openPreferencesMenuItem() async { + await $(#setting_preferences).tap(); + } + + Future switchOnThreadSetting() async { + final threadSettingOn = $(ValueKey(AppLocalizations().thread)) + .$(#setting_option_switch_on) + .visible; + if (!threadSettingOn) { + await $(ValueKey(AppLocalizations().thread)).tap(); + } + } + + Future backToSettingsFromFirstLevel() async { + await $(#settings_first_level_close_button).tap(); + } + + Future closeSettings() async { + await $(#settings_close_button).tap(); + } } \ No newline at end of file diff --git a/integration_test/scenarios/thread_detail/thread_detail_reply_real_time_update_scenario.dart b/integration_test/scenarios/thread_detail/thread_detail_reply_real_time_update_scenario.dart new file mode 100644 index 000000000..ab4b18006 --- /dev/null +++ b/integration_test/scenarios/thread_detail/thread_detail_reply_real_time_update_scenario.dart @@ -0,0 +1,53 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart'; + +import '../../base/base_test_scenario.dart'; +import '../../robots/composer_robot.dart'; +import '../../robots/email_robot.dart'; +import '../../robots/mailbox_menu_robot.dart'; +import '../../robots/search_robot.dart'; +import '../../robots/setting_robot.dart'; +import '../../robots/thread_robot.dart'; + +class ThreadDetailReplyRealTimeUpdateScenario extends BaseTestScenario { + ThreadDetailReplyRealTimeUpdateScenario(super.$); + + @override + Future runTestLogic() async { + const subject = 'Reply thread'; + const replyContent = 'reply thread detail'; + + final threadRobot = ThreadRobot($); + final mailboxMenuRobot = MailboxMenuRobot($); + final settingRobot = SettingRobot($); + final searchRobot = SearchRobot($); + final emailRobot = EmailRobot($); + final composerRobot = ComposerRobot($); + final imagePaths = ImagePaths(); + + await threadRobot.openMailbox(); + await mailboxMenuRobot.openSetting(); + await settingRobot.openPreferencesMenuItem(); + await settingRobot.switchOnThreadSetting(); + await settingRobot.backToSettingsFromFirstLevel(); + await settingRobot.closeSettings(); + + await threadRobot.openSearchView(); + await searchRobot.enterQueryString(subject); + await searchRobot.tapOnShowAllResultsText(); + await searchRobot.openEmailWithSubject(subject); + + await emailRobot.onTapReplyEmail(); + await composerRobot.grantContactPermission(); + await composerRobot.addContent(replyContent); + await composerRobot.sendEmail(imagePaths); + await $.waitUntilVisible($(ThreadDetailCollapsedEmail)); + expect( + $(ThreadDetailCollapsedEmail).which((widget) { + return widget.preview.contains(replyContent); + }), + findsOneWidget, + ); + } +} \ No newline at end of file diff --git a/integration_test/tests/thread_detail/thread_detail_reply_real_time_update_test.dart b/integration_test/tests/thread_detail/thread_detail_reply_real_time_update_test.dart new file mode 100644 index 000000000..5a8798575 --- /dev/null +++ b/integration_test/tests/thread_detail/thread_detail_reply_real_time_update_test.dart @@ -0,0 +1,9 @@ +import '../../base/test_base.dart'; +import '../../scenarios/thread_detail/thread_detail_reply_real_time_update_scenario.dart'; + +void main() { + TestBase().runPatrolTest( + description: 'Should see reply of thread detail', + scenarioBuilder: ($) => ThreadDetailReplyRealTimeUpdateScenario($), + ); +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/get_email_content_interactor.dart b/lib/features/email/domain/usecases/get_email_content_interactor.dart index f7f9d5cb0..9ff1276be 100644 --- a/lib/features/email/domain/usecases/get_email_content_interactor.dart +++ b/lib/features/email/domain/usecases/get_email_content_interactor.dart @@ -144,6 +144,8 @@ class GetEmailContentInteractor { emailCurrent: emailCache.copyWith( headers: detailedEmail.headers, sMimeStatusHeader: detailedEmail.sMimeStatusHeader, + references: detailedEmail.references, + messageId: detailedEmail.messageId, ) )); } catch (e) { @@ -183,6 +185,8 @@ class GetEmailContentInteractor { headers: detailedEmail.headers, sMimeStatusHeader: detailedEmail.sMimeStatusHeader, identityHeader: detailedEmail.identityHeader, + references: detailedEmail.references, + messageId: detailedEmail.messageId, ) )); } catch (e) { diff --git a/lib/features/manage_account/presentation/menu/settings/setting_first_level_app_bar.dart b/lib/features/manage_account/presentation/menu/settings/setting_first_level_app_bar.dart index 917c9280a..b768d00ad 100644 --- a/lib/features/manage_account/presentation/menu/settings/setting_first_level_app_bar.dart +++ b/lib/features/manage_account/presentation/menu/settings/setting_first_level_app_bar.dart @@ -33,6 +33,7 @@ class SettingFirstLevelAppBar extends StatelessWidget { PositionedDirectional( start: 0, child: TMailButtonWidget.fromIcon( + key: const ValueKey('settings_first_level_close_button'), icon: imagePaths.icArrowBack, tooltipMessage: appLocalizations.back, backgroundColor: Colors.transparent, diff --git a/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart b/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart index 2349e43b5..2798d3609 100644 --- a/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart +++ b/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart @@ -72,6 +72,7 @@ class SettingsFirstLevelView extends GetWidget { if (controller.manageAccountDashboardController.isServerSettingsCapabilitySupported) { return Column(children: [ SettingFirstLevelTileBuilder( + key: const ValueKey('setting_preferences'), AccountMenuItem.preferences.getName(AppLocalizations.of(context)), AccountMenuItem.preferences.getIcon(controller.imagePaths), subtitle: AppLocalizations.of(context).emailReadReceiptsSettingExplanation, diff --git a/lib/features/manage_account/presentation/menu/settings/universal_setting_app_bar.dart b/lib/features/manage_account/presentation/menu/settings/universal_setting_app_bar.dart index da7808e46..293a8397a 100644 --- a/lib/features/manage_account/presentation/menu/settings/universal_setting_app_bar.dart +++ b/lib/features/manage_account/presentation/menu/settings/universal_setting_app_bar.dart @@ -37,6 +37,7 @@ class UniversalSettingAppBar extends StatelessWidget { PositionedDirectional( start: 24, child: TMailButtonWidget.fromIcon( + key: const ValueKey('settings_close_button'), icon: imagePaths.icClose, iconSize: 28, padding: const EdgeInsets.all(5), diff --git a/lib/features/manage_account/presentation/preferences/widgets/setting_option_item.dart b/lib/features/manage_account/presentation/preferences/widgets/setting_option_item.dart index 3f45b0431..e4eeaeb53 100644 --- a/lib/features/manage_account/presentation/preferences/widgets/setting_option_item.dart +++ b/lib/features/manage_account/presentation/preferences/widgets/setting_option_item.dart @@ -59,11 +59,17 @@ class SettingOptionItem extends StatelessWidget { Row( children: [ InkWell( + key: ValueKey(optionType.getTitle(appLocalizations)), onTap: () => onTapSettingOptionAction( optionType, optionType.isEnabled(settingOption, localSettings), ), child: SvgPicture.asset( + key: ValueKey( + optionType.isEnabled(settingOption, localSettings) + ? 'setting_option_switch_on' + : 'setting_option_switch_off', + ), optionType.isEnabled(settingOption, localSettings) ? imagePaths.icSwitchOn : imagePaths.icSwitchOff, diff --git a/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart b/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart index 0a716d9fd..f692bd0d1 100644 --- a/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart +++ b/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart @@ -104,11 +104,14 @@ extension HandleRefreshThreadDetailAction on ThreadDetailController { Email email, ThreadId currentThreadId, ) { - return email.threadId == currentThreadId && - sentMailboxId != null && - !email.inSentMailbox(sentMailboxId!) && - ownEmailAddress != null && - !email.fromMe(ownEmailAddress!) && - !email.recipientsHasMe(ownEmailAddress!); + if (email.threadId != currentThreadId) return false; + + if (sentMailboxId != null && !email.inSentMailbox(sentMailboxId!)) { + return true; + } + + return ownEmailAddress != null && + (!email.fromMe(ownEmailAddress!) || + !email.recipientsHasMe(ownEmailAddress!)); } } \ No newline at end of file diff --git a/provisioning/integration_test/eml/reply_email/reply-thread.eml b/provisioning/integration_test/eml/reply_email/reply-thread.eml new file mode 100644 index 000000000..2d5b620ab --- /dev/null +++ b/provisioning/integration_test/eml/reply_email/reply-thread.eml @@ -0,0 +1,26 @@ +Return-Path: +MIME-Version: 1.0 +References: +Subject: Reply thread +From: emma@example.com +To: "bob" +Cc: "alice" +Bcc: "brian" +Reply-To: emma-reply-to@example.com +Date: Tue, 17 Dec 2024 16:31:00 +0000 +Message-ID: +User-Agent: Twake-Mail/0.13.2 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; + rv:131.0) Gecko/20100101 Firefox/131.0 +Content-Type: multipart/alternative; + boundary="-=Part.4f.450e1cfbcd355387.192d67ae03d.c367d8042fea24dd=-" +List-Post: + +---=Part.4f.450e1cfbcd355387.192d67ae03d.c367d8042fea24dd=- +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable +Accept-Language: fr-FR, en-US, vi-VN, ru-RU, ar-TN, it-IT +Content-Language: en-US + +Reply thread + +---=Part.4f.450e1cfbcd355387.192d67ae03d.c367d8042fea24dd=--- diff --git a/provisioning/integration_test/provisioning.sh b/provisioning/integration_test/provisioning.sh index d9d6aa178..40cf02e72 100755 --- a/provisioning/integration_test/provisioning.sh +++ b/provisioning/integration_test/provisioning.sh @@ -70,7 +70,7 @@ james-cli ImportEml \#private "bob@example.com" "Calendar" "/root/conf/integrati # For test reply email # Import emails into 'Reply Emails' folder for user Bob -replyEmailsEML=("reply-all.eml" "reply-to-list.eml" "with-reply-to.eml" "without-reply-to.eml") +replyEmailsEML=("reply-all.eml" "reply-to-list.eml" "with-reply-to.eml" "without-reply-to.eml" "reply-thread.eml") for eml in "${replyEmailsEML[@]}"; do echo "Importing $eml into 'Reply Emails' folder for user bob" diff --git a/test/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action_test.dart b/test/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action_test.dart index 760563b91..44b779d98 100644 --- a/test/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action_test.dart +++ b/test/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -129,4 +130,131 @@ void main() { }, ); }); + + group('validateNewCreatedEmailForCurrentThread test:', () { + test( + 'should return false ' + 'when email is not in same thread as current thread', + () { + // arrange + final email = Email(threadId: ThreadId(Id('other-thread-id'))); + final threadId = ThreadId(Id('thread-id')); + + // act + final result = threadDetailController.validateNewCreatedEmailForCurrentThread( + email, + threadId, + ); + + // assert + expect(result, false); + }); + + test( + 'should return true ' + 'when email is in same thread as current thread ' + 'and email is not in sent mailbox', + () { + // arrange + final threadId = ThreadId(Id('thread-id')); + final sentMailboxId = MailboxId(Id('sent-mailbox-id')); + final email = Email(threadId: threadId); + when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId); + + // act + final result = threadDetailController.validateNewCreatedEmailForCurrentThread( + email, + threadId, + ); + + // assert + expect(result, true); + }); + + test( + 'should return true ' + 'when email is in same thread as current thread ' + 'and email is in sent mailbox ' + 'and email is not sent by current user', + () { + // arrange + final threadId = ThreadId(Id('thread-id')); + final sentMailboxId = MailboxId(Id('sent-mailbox-id')); + final email = Email( + threadId: threadId, + mailboxIds: {sentMailboxId: true}, + ); + const ownEmailAddress = '9jEYK@example.com'; + when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId); + when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress); + + // act + final result = threadDetailController.validateNewCreatedEmailForCurrentThread( + email, + threadId, + ); + + // assert + expect(result, true); + }); + + test( + 'should return true ' + 'when email is in same thread as current thread ' + 'and email is in sent mailbox ' + 'and email is sent by current user ' + 'and email is not sent to current user', + () { + // arrange + final threadId = ThreadId(Id('thread-id')); + final sentMailboxId = MailboxId(Id('sent-mailbox-id')); + const ownEmailAddress = '9jEYK@example.com'; + final email = Email( + threadId: threadId, + mailboxIds: {sentMailboxId: true}, + from: {EmailAddress(null, ownEmailAddress)}, + ); + when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId); + when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress); + + // act + final result = threadDetailController.validateNewCreatedEmailForCurrentThread( + email, + threadId, + ); + + // assert + expect(result, true); + }); + + test( + 'should return false ' + 'when email is in same thread as current thread ' + 'and email is in sent mailbox ' + 'and email is sent by current user ' + 'and email is sent to current user', + () { + // arrange + final threadId = ThreadId(Id('thread-id')); + final sentMailboxId = MailboxId(Id('sent-mailbox-id')); + const ownEmailAddress = '9jEYK@example.com'; + final email = Email( + threadId: threadId, + mailboxIds: {sentMailboxId: true}, + from: {EmailAddress(null, ownEmailAddress)}, + to: {EmailAddress(null, ownEmailAddress)}, + ); + when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId); + when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress); + + // act + final result = threadDetailController.validateNewCreatedEmailForCurrentThread( + email, + threadId, + ); + + // assert + expect(result, false); + }); + }); }