From ef1f979c4b5e4e654483e522bcb9a7b3a7247857 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 23 Apr 2025 11:40:59 +0700 Subject: [PATCH] TF-3685 Add integration test for case change identity in draft email Signed-off-by: dab246 --- .../mixin/scenario_utils_mixin.dart | 39 ++++ .../models/provisioning_identity.dart | 11 + integration_test/robots/composer_robot.dart | 14 ++ .../robots/identities_list_menu_robot.dart | 12 + ...ange_identity_in_draft_email_scenario.dart | 108 +++++++++ .../change_identity_in_draft_email_test.dart | 9 + .../composer/presentation/composer_view.dart | 3 +- .../setup_email_content_extension.dart | 9 +- .../view/mobile/mobile_editor_view.dart | 217 +++++++++--------- .../domain/state/get_email_content_state.dart | 2 + 10 files changed, 312 insertions(+), 112 deletions(-) create mode 100644 integration_test/models/provisioning_identity.dart create mode 100644 integration_test/robots/identities_list_menu_robot.dart create mode 100644 integration_test/scenarios/composer/change_identity_in_draft_email_scenario.dart create mode 100644 integration_test/tests/compose/change_identity_in_draft_email_test.dart diff --git a/integration_test/mixin/scenario_utils_mixin.dart b/integration_test/mixin/scenario_utils_mixin.dart index d322129be..b24973d58 100644 --- a/integration_test/mixin/scenario_utils_mixin.dart +++ b/integration_test/mixin/scenario_utils_mixin.dart @@ -6,6 +6,7 @@ import 'package:core/utils/app_logger.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/http/http_client.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/jmap_request.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; @@ -19,13 +20,19 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachmen import 'package:tmail_ui_user/features/composer/presentation/composer_bindings.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_default_identity_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_identity_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identity_interactors_bindings.dart'; import 'package:tmail_ui_user/features/thread/presentation/thread_controller.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; +import 'package:uuid/uuid.dart'; import '../models/provisioning_email.dart'; +import '../models/provisioning_identity.dart'; mixin ScenarioUtilsMixin { Future provisionEmail( @@ -212,4 +219,36 @@ mixin ScenarioUtilsMixin { return emails.length == allowedEmails.length && emails.containsAll(allowedEmails); } + + Future provisionIdentities(List provisioningIdentities) async { + IdentityInteractorsBindings().dependencies(); + + final mailboxDashBoardController = Get.find(); + final createNewIdentityInteractor = Get.find(); + final createNewDefaultIdentityInteractor = Get.find(); + const uuid = Uuid(); + + final session = mailboxDashBoardController.sessionCurrent; + final accountId = mailboxDashBoardController.accountId.value; + final listIdentityRequest = provisioningIdentities + .map((provisioningIdentity) => CreateNewIdentityRequest( + Id(uuid.v1()), + provisioningIdentity.identity, + isDefaultIdentity: provisioningIdentity.isDefault, + )); + + for (var identityRequest in listIdentityRequest) { + if (identityRequest.isDefaultIdentity) { + await createNewDefaultIdentityInteractor + .execute(session!, accountId!, identityRequest) + .last; + } else { + await createNewIdentityInteractor + .execute(session!, accountId!, identityRequest) + .last; + } + } + + IdentityInteractorsBindings().dispose(); + } } \ No newline at end of file diff --git a/integration_test/models/provisioning_identity.dart b/integration_test/models/provisioning_identity.dart new file mode 100644 index 000000000..e50fc1092 --- /dev/null +++ b/integration_test/models/provisioning_identity.dart @@ -0,0 +1,11 @@ +import 'package:jmap_dart_client/jmap/identities/identity.dart'; + +class ProvisioningIdentity { + final Identity identity; + final bool isDefault; + + ProvisioningIdentity({ + required this.identity, + this.isDefault = false, + }); +} \ No newline at end of file diff --git a/integration_test/robots/composer_robot.dart b/integration_test/robots/composer_robot.dart index 64a0ddaf6..ddb82e8f2 100644 --- a/integration_test/robots/composer_robot.dart +++ b/integration_test/robots/composer_robot.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:model/email/prefix_email_address.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; @@ -7,6 +8,7 @@ import 'package:tmail_ui_user/features/composer/presentation/composer_controller import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/view/mobile/mobile_editor_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_mobile_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart'; @@ -97,4 +99,16 @@ class ComposerRobot extends CoreRobot { Future tapReadReceiptPopupItemOnMenu() async { await $(#read_receipt_popup_item).tap(); } + + Future tapSaveAsDraftPopupItemOnMenu() async { + await $(#save_as_draft_popup_item).tap(); + } + + Future tapRecipientExpandButton() async { + await $(#prefix_to_recipient_expand_button).tap(); + } + + Future tapFromFieldPopupMenu() async { + await $(FromComposerMobileWidget).$(InkWell).tap(); + } } \ No newline at end of file diff --git a/integration_test/robots/identities_list_menu_robot.dart b/integration_test/robots/identities_list_menu_robot.dart new file mode 100644 index 000000000..54a8666d5 --- /dev/null +++ b/integration_test/robots/identities_list_menu_robot.dart @@ -0,0 +1,12 @@ + +import 'package:flutter_test/flutter_test.dart'; + +import '../base/core_robot.dart'; + +class IdentitiesListMenuRobot extends CoreRobot { + IdentitiesListMenuRobot(super.$); + + Future selectIdentityByName(String name) async { + await $(find.text(name)).tap(); + } +} \ No newline at end of file diff --git a/integration_test/scenarios/composer/change_identity_in_draft_email_scenario.dart b/integration_test/scenarios/composer/change_identity_in_draft_email_scenario.dart new file mode 100644 index 000000000..453a35197 --- /dev/null +++ b/integration_test/scenarios/composer/change_identity_in_draft_email_scenario.dart @@ -0,0 +1,108 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_mobile_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +import '../../base/base_test_scenario.dart'; +import '../../models/provisioning_identity.dart'; +import '../../robots/composer_robot.dart'; +import '../../robots/identities_list_menu_robot.dart'; +import '../../robots/mailbox_menu_robot.dart'; +import '../../robots/thread_robot.dart'; + +class ChangeIdentityInDraftEmailScenario extends BaseTestScenario { + const ChangeIdentityInDraftEmailScenario(super.$); + + @override + Future runTestLogic() async { + const email = String.fromEnvironment('BASIC_AUTH_EMAIL'); + const subject = 'Change identity in draft email'; + + final threadRobot = ThreadRobot($); + final composerRobot = ComposerRobot($); + final mailboxMenuRobot = MailboxMenuRobot($); + final identitiesListMenuRobot = IdentitiesListMenuRobot($); + final imagePaths = ImagePaths(); + final appLocalizations = AppLocalizations(); + + final identity1 = Identity( + name: 'Identity 1', + email: email, + htmlSignature: Signature('Signature 1'), + sortOrder: UnsignedInt(0), + ); + final identity2 = Identity( + name: 'Identity 2', + email: email, + htmlSignature: Signature('Signature 2'), + sortOrder: UnsignedInt(100), + ); + await provisionIdentities([ + ProvisioningIdentity(identity: identity1, isDefault: true), + ProvisioningIdentity(identity: identity2), + ]); + await $.pumpAndSettle(); + + await threadRobot.openComposer(); + await $.pumpAndSettle(); + await _expectComposerViewVisible(); + + await composerRobot.grantContactPermission(); + + await composerRobot.addSubject(subject); + await composerRobot.addContent(subject); + + await composerRobot.tapRecipientExpandButton(); + await $.pumpAndSettle(); + await _expectIdentityVisible(identity1); + + await composerRobot.tapFromFieldPopupMenu(); + await identitiesListMenuRobot.selectIdentityByName(identity2.name!); + await $.pumpAndSettle(); + await _expectIdentityVisible(identity2); + + await composerRobot.tapMoreOptionOnAppBar(); + await _expectSaveAsDraftOptionPopupMenuVisible(); + + await composerRobot.tapSaveAsDraftPopupItemOnMenu(); + await _expectSaveAsDraftEmailSuccessToast(appLocalizations); + + await composerRobot.tapCloseComposer(imagePaths); + + await threadRobot.openMailbox(); + await mailboxMenuRobot + .openFolderByName(appLocalizations.draftsMailboxDisplayName); + + await threadRobot.openEmailWithSubject(subject); + await _expectComposerViewVisible(); + + await composerRobot.grantContactPermission(); + + await composerRobot.tapRecipientExpandButton(); + await $.pumpAndSettle(); + await _expectIdentityVisible(identity2); + } + + Future _expectComposerViewVisible() => + expectViewVisible($(ComposerView)); + + Future _expectSaveAsDraftOptionPopupMenuVisible() => + expectViewVisible($(#save_as_draft_popup_item)); + + Future _expectSaveAsDraftEmailSuccessToast( + AppLocalizations appLocalizations, + ) => + expectViewVisible($(appLocalizations.drafts_saved)); + + Future _expectIdentityVisible(Identity identity) async { + expect( + $(FromComposerMobileWidget).which( + (widget) => widget.selectedIdentity?.name == identity.name + ).visible, + isTrue, + ); + } +} diff --git a/integration_test/tests/compose/change_identity_in_draft_email_test.dart b/integration_test/tests/compose/change_identity_in_draft_email_test.dart new file mode 100644 index 000000000..d3c1ea962 --- /dev/null +++ b/integration_test/tests/compose/change_identity_in_draft_email_test.dart @@ -0,0 +1,9 @@ +import '../../base/test_base.dart'; +import '../../scenarios/composer/change_identity_in_draft_email_scenario.dart'; + +void main() { + TestBase().runPatrolTest( + description: 'Should see new identity in From field when select new identity and save as draft successfully', + scenarioBuilder: ($) => ChangeIdentityInDraftEmailScenario($), + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index ba6a8409e..c8f8637df 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -114,7 +114,7 @@ class ComposerView extends GetWidget { children: [ Obx(() { if (controller.fromRecipientState.value == PrefixRecipientState.enabled) { - return FromComposerMobileWidget( + return FromComposerMobileWidget( selectedIdentity: controller.identitySelected.value, imagePaths: controller.imagePaths, responsiveUtils: controller.responsiveUtils, @@ -551,6 +551,7 @@ class ComposerView extends GetWidget { PopupMenuItem( padding: EdgeInsets.zero, child: PopupItemWidget( + key: const Key('save_as_draft_popup_item'), iconAction: controller.imagePaths.icSaveToDraft, nameAction: AppLocalizations.of(context).saveAsDraft, colorIcon: ComposerStyle.popupItemIconColor, diff --git a/lib/features/composer/presentation/extensions/setup_email_content_extension.dart b/lib/features/composer/presentation/extensions/setup_email_content_extension.dart index e38842acd..43ebbea4f 100644 --- a/lib/features/composer/presentation/extensions/setup_email_content_extension.dart +++ b/lib/features/composer/presentation/extensions/setup_email_content_extension.dart @@ -1,5 +1,4 @@ -import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; @@ -96,7 +95,7 @@ extension SetupEmailContentExtension on ComposerController { emailContentsViewState.value = Left(uiState); consumeState(Stream.value(Left(uiState))); } else { - emailContentsViewState.value = Right(UIState.idle); + emailContentsViewState.value = Right(LoadEmailContentCompleted()); } break; case EmailActionType.editSendingEmail: @@ -161,7 +160,7 @@ extension SetupEmailContentExtension on ComposerController { emailContentsViewState.value = Left(GetEmailContentFailure(uiState.exception)); consumeState(Stream.value(Left(GetEmailContentFailure(uiState.exception)))); } else { - emailContentsViewState.value = Right(UIState.idle); + emailContentsViewState.value = Right(LoadEmailContentCompleted()); } } break; @@ -207,7 +206,7 @@ extension SetupEmailContentExtension on ComposerController { emailContentsViewState.value = Left(GetEmailContentFailure(uiState.exception)); consumeState(Stream.value(Left(GetEmailContentFailure(uiState.exception)))); } else { - emailContentsViewState.value = Right(UIState.idle); + emailContentsViewState.value = Right(LoadEmailContentCompleted()); } } break; @@ -219,7 +218,7 @@ extension SetupEmailContentExtension on ComposerController { emailContentsViewState.value = Right(successState); break; default: - emailContentsViewState.value = Right(UIState.idle); + emailContentsViewState.value = Right(LoadEmailContentCompleted()); break; } } diff --git a/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart b/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart index fd516a70e..9d9e00291 100644 --- a/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart +++ b/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart @@ -31,117 +31,122 @@ class MobileEditorView extends StatelessWidget with EditorViewMixin { @override Widget build(BuildContext context) { - if (arguments == null) { + if (arguments == null || contentViewState == null) { return const SizedBox.shrink(); } - switch (arguments!.emailActionType) { - case EmailActionType.compose: - case EmailActionType.composeFromEmailAddress: - case EmailActionType.composeFromFileShared: - return MobileEditorWidget( - content: HtmlExtension.editorStartTags, - direction: AppUtils.getCurrentDirection(context), - onCreatedEditorAction: onCreatedEditorAction, - onLoadCompletedEditorAction: onLoadCompletedEditorAction, - onEditorContentHeightChanged: onEditorContentHeightChanged, - ); - case EmailActionType.editDraft: - case EmailActionType.editSendingEmail: - case EmailActionType.composeFromContentShared: - case EmailActionType.reopenComposerBrowser: - case EmailActionType.composeFromMailtoUri: - case EmailActionType.composeFromUnsubscribeMailtoLink: - case EmailActionType.editAsNewEmail: - if (contentViewState == null) { - return const SizedBox.shrink(); - } - return contentViewState!.fold( - (failure) => MobileEditorWidget( - content: HtmlExtension.editorStartTags, - direction: AppUtils.getCurrentDirection(context), - onCreatedEditorAction: onCreatedEditorAction, - onLoadCompletedEditorAction: onLoadCompletedEditorAction, - onEditorContentHeightChanged: onEditorContentHeightChanged, - ), - (success) { - if (success is GetEmailContentLoading) { - return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); - } else { - var newContent = HtmlExtension.editorStartTags; - if (success is GetEmailContentSuccess) { - newContent = success.htmlEmailContent; - } else if (success is GetEmailContentFromCacheSuccess) { - newContent = success.htmlEmailContent; - } - if (newContent.isEmpty) { - newContent = HtmlExtension.editorStartTags; - } - return MobileEditorWidget( - content: newContent, - direction: AppUtils.getCurrentDirection(context), - onCreatedEditorAction: onCreatedEditorAction, - onLoadCompletedEditorAction: onLoadCompletedEditorAction, - onEditorContentHeightChanged: onEditorContentHeightChanged, - ); - } + final direction = AppUtils.getCurrentDirection(context); + final locale = Localizations.localeOf(context); + final appLocalizations = AppLocalizations.of(context); + final actionType = arguments!.emailActionType; + + if (_isEditOrComposeAction(actionType)) { + return contentViewState!.fold( + (_) => buildEditor(direction: direction), + (success) { + if (success is GetEmailContentLoading) { + return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); } - ); - case EmailActionType.reply: - case EmailActionType.replyToList: - case EmailActionType.replyAll: - case EmailActionType.forward: - if (contentViewState == null) { - return const SizedBox.shrink(); - } - return contentViewState!.fold( - (failure) { - final emailContentQuoted = getEmailContentQuotedAsHtml( - locale: Localizations.localeOf(context), - appLocalizations: AppLocalizations.of(context), - emailContent: '', - emailActionType: arguments!.emailActionType, - presentationEmail: arguments!.presentationEmail! - ); - return MobileEditorWidget( - content: emailContentQuoted, - direction: AppUtils.getCurrentDirection(context), - onCreatedEditorAction: onCreatedEditorAction, - onLoadCompletedEditorAction: onLoadCompletedEditorAction, - onEditorContentHeightChanged: onEditorContentHeightChanged, - ); - }, - (success) { - if (success is GetEmailContentLoading) { - return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); - } else { - final emailContentQuoted = getEmailContentQuotedAsHtml( - locale: Localizations.localeOf(context), - appLocalizations: AppLocalizations.of(context), - emailContent: success is GetEmailContentSuccess - ? success.htmlEmailContent - : '', - emailActionType: arguments!.emailActionType, - presentationEmail: arguments!.presentationEmail! - ); - return MobileEditorWidget( - content: emailContentQuoted, - direction: AppUtils.getCurrentDirection(context), - onCreatedEditorAction: onCreatedEditorAction, - onLoadCompletedEditorAction: onLoadCompletedEditorAction, - onEditorContentHeightChanged: onEditorContentHeightChanged, - ); - } + + String newContent = ''; + if (success is GetEmailContentSuccess) { + newContent = success.htmlEmailContent; + } else if (success is GetEmailContentFromCacheSuccess) { + newContent = success.htmlEmailContent; } - ); - default: - return MobileEditorWidget( - content: HtmlExtension.editorStartTags, - direction: AppUtils.getCurrentDirection(context), - onCreatedEditorAction: onCreatedEditorAction, - onLoadCompletedEditorAction: onLoadCompletedEditorAction, - onEditorContentHeightChanged: onEditorContentHeightChanged, - ); + + return buildEditor( + direction: direction, + content: newContent, + ); + }, + ); } + + if (_isReplyOrForwardAction(actionType)) { + return contentViewState!.fold( + (_) => buildEditor( + direction: direction, + content: _buildQuotedContent( + appLocalizations: appLocalizations, + locale: locale, + ), + ), + (success) { + if (success is GetEmailContentLoading) { + return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); + } + + return buildEditor( + direction: direction, + content: _buildQuotedContent( + appLocalizations: appLocalizations, + locale: locale, + emailContent: success is GetEmailContentSuccess + ? success.htmlEmailContent + : '', + ), + ); + }, + ); + } + + return contentViewState!.fold( + (_) => buildEditor(direction: direction), + (success) { + if (success is GetEmailContentLoading) { + return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); + } + return buildEditor(direction: direction); + }, + ); + } + + bool _isEditOrComposeAction(EmailActionType type) { + return { + EmailActionType.editDraft, + EmailActionType.editSendingEmail, + EmailActionType.composeFromContentShared, + EmailActionType.reopenComposerBrowser, + EmailActionType.composeFromMailtoUri, + EmailActionType.composeFromUnsubscribeMailtoLink, + EmailActionType.editAsNewEmail, + }.contains(type); + } + + bool _isReplyOrForwardAction(EmailActionType type) { + return { + EmailActionType.reply, + EmailActionType.replyToList, + EmailActionType.replyAll, + EmailActionType.forward, + }.contains(type); + } + + String _buildQuotedContent({ + required AppLocalizations appLocalizations, + required Locale locale, + String emailContent = '', + }) { + return getEmailContentQuotedAsHtml( + locale: locale, + appLocalizations: appLocalizations, + emailContent: emailContent, + emailActionType: arguments!.emailActionType, + presentationEmail: arguments!.presentationEmail!, + ); + } + + Widget buildEditor({ + required TextDirection direction, + String content = '', + }) { + return MobileEditorWidget( + content: content.isEmpty ? HtmlExtension.editorStartTags : content, + direction: direction, + onCreatedEditorAction: onCreatedEditorAction, + onLoadCompletedEditorAction: onLoadCompletedEditorAction, + onEditorContentHeightChanged: onEditorContentHeightChanged, + ); } } \ No newline at end of file diff --git a/lib/features/email/domain/state/get_email_content_state.dart b/lib/features/email/domain/state/get_email_content_state.dart index fb7d5e8a5..7ca833a30 100644 --- a/lib/features/email/domain/state/get_email_content_state.dart +++ b/lib/features/email/domain/state/get_email_content_state.dart @@ -49,6 +49,8 @@ class GetEmailContentFromCacheSuccess extends UIState { ]; } +class LoadEmailContentCompleted extends UIState {} + class GetEmailContentFailure extends FeatureFailure { GetEmailContentFailure(dynamic exception, {super.onRetry})