From e423d0d49de8690054d308951ba266a9ac7529b1 Mon Sep 17 00:00:00 2001 From: DatDang Date: Tue, 1 Oct 2024 11:44:40 +0700 Subject: [PATCH 1/2] TF-3034 Prevent duplicate draft warning --- core/lib/utils/platform_info.dart | 12 +- .../presentation/composer_controller.dart | 126 +- .../presentation/model/saved_email_draft.dart | 39 + pubspec.lock | 2 +- pubspec.yaml | 2 + .../composer_controller_test.dart | 1459 +++++++++++++++++ .../model/saved_email_draft_test.dart | 148 ++ test/mocks/mock_web_view_platform.dart | 58 + 8 files changed, 1785 insertions(+), 61 deletions(-) create mode 100644 lib/features/composer/presentation/model/saved_email_draft.dart create mode 100644 test/features/composer/presentation/composer_controller_test.dart create mode 100644 test/features/composer/presentation/model/saved_email_draft_test.dart create mode 100644 test/mocks/mock_web_view_platform.dart diff --git a/core/lib/utils/platform_info.dart b/core/lib/utils/platform_info.dart index 72c002b8f1..f7f0edc190 100644 --- a/core/lib/utils/platform_info.dart +++ b/core/lib/utils/platform_info.dart @@ -7,12 +7,12 @@ abstract class PlatformInfo { static bool isTestingForWeb = false; static bool get isWeb => kIsWeb || isTestingForWeb; - static bool get isLinux => !kIsWeb && defaultTargetPlatform == TargetPlatform.linux; - static bool get isWindows => !kIsWeb && defaultTargetPlatform == TargetPlatform.windows; - static bool get isMacOS => !kIsWeb && defaultTargetPlatform == TargetPlatform.macOS; - static bool get isFuchsia => !kIsWeb && defaultTargetPlatform == TargetPlatform.fuchsia; - static bool get isIOS => !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; - static bool get isAndroid => !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + static bool get isLinux => !isWeb && defaultTargetPlatform == TargetPlatform.linux; + static bool get isWindows => !isWeb && defaultTargetPlatform == TargetPlatform.windows; + static bool get isMacOS => !isWeb && defaultTargetPlatform == TargetPlatform.macOS; + static bool get isFuchsia => !isWeb && defaultTargetPlatform == TargetPlatform.fuchsia; + static bool get isIOS => !isWeb && defaultTargetPlatform == TargetPlatform.iOS; + static bool get isAndroid => !isWeb && defaultTargetPlatform == TargetPlatform.android; static bool get isMobile => isAndroid || isIOS; static bool get isDesktop => isLinux || isWindows || isMacOS; static bool get isCanvasKit => isRendererCanvasKit; diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 96a271e0e2..17b632328f 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -62,6 +62,7 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/list_sha import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/signature_status.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; @@ -109,7 +110,6 @@ class ComposerController extends BaseController with DragDropFileMixin implement final mailboxDashBoardController = Get.find(); final networkConnectionController = Get.find(); - final _dynamicUrlInterceptors = Get.find(); final _beforeReconnectManager = Get.find(); final composerArguments = Rxn(); @@ -199,6 +199,14 @@ class ComposerController extends BaseController with DragDropFileMixin implement ButtonState _saveToDraftButtonState = ButtonState.enabled; ButtonState _sendButtonState = ButtonState.enabled; SignatureStatus _identityContentOnOpenPolicy = SignatureStatus.editedAvailable; + int? _savedEmailDraftHash; + bool _restoringSignatureButton = false; + + @visibleForTesting + bool get restoringSignatureButton => _restoringSignatureButton; + + @visibleForTesting + int? get savedEmailDraftHash => _savedEmailDraftHash; late Worker uploadInlineImageWorker; late Worker dashboardViewStateWorker; @@ -330,6 +338,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement maxWithEditor = null; } else if (success is GetAlwaysReadReceiptSettingSuccess) { hasRequestReadReceipt.value = success.alwaysReadReceiptEnabled; + _initEmailDraftHash(); } else if (success is RestoreEmailInlineImagesSuccess) { _updateEditorContent(success); } @@ -358,6 +367,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement } } else if (failure is GetAlwaysReadReceiptSettingFailure) { hasRequestReadReceipt.value = false; + _initEmailDraftHash(); } } @@ -732,16 +742,18 @@ class ComposerController extends BaseController with DragDropFileMixin implement (identity) => identity.id == identityId); } - void _initIdentities(ComposerArguments composerArguments) { + Future _initIdentities(ComposerArguments composerArguments) async { listFromIdentities.value = composerArguments.identities ?? []; final selectedIdentityFromId = _selectIdentityFromId( composerArguments.selectedIdentityId); if (listFromIdentities.isEmpty) { _getAllIdentities(); } else if (selectedIdentityFromId != null) { - _selectIdentity(selectedIdentityFromId); + await _selectIdentity(selectedIdentityFromId); + _initEmailDraftHash(); } else if (composerArguments.identities?.isNotEmpty == true) { - _selectIdentity(composerArguments.identities!.first); + await _selectIdentity(composerArguments.identities!.first); + _initEmailDraftHash(); } } @@ -764,8 +776,10 @@ class ComposerController extends BaseController with DragDropFileMixin implement composerArguments.value?.selectedIdentityId); if (selectedIdentityFromId != null) { await _selectIdentity(selectedIdentityFromId); + _initEmailDraftHash(); } else { await _selectIdentity(listIdentitiesMayDeleted.firstOrNull); + _initEmailDraftHash(); } } } @@ -1216,7 +1230,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { - final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl); + final uploadUri = session.getUploadUri(accountId, jmapUrl: dynamicUrlInterceptors.jmapUrl); uploadController.justUploadAttachmentsAction( uploadFiles: pickedFiles, uploadUri: uploadUri, @@ -1230,50 +1244,41 @@ class ComposerController extends BaseController with DragDropFileMixin implement uploadController.deleteFileUploaded(uploadId); } - Future _validateEmailChange({ - required BuildContext context, - required EmailActionType emailActionType, - PresentationEmail? presentationEmail, - Role? mailboxRole, - }) async { - final newEmailBody = await _getContentInEditor(); - final oldEmailBody = _initTextEditor ?? ''; - log('ComposerController::_validateEmailChange: newEmailBody = $newEmailBody | oldEmailBody = $oldEmailBody'); - final isEmailBodyChanged = !oldEmailBody.trim().isSame(newEmailBody.trim()); - - final newEmailSubject = subjectEmail.value ?? ''; - final oldEmailSubject = emailActionType == EmailActionType.editDraft - ? presentationEmail?.getEmailTitle().trim() ?? '' - : ''; - final isEmailSubjectChanged = !oldEmailSubject.trim().isSame(newEmailSubject.trim()); - - final recipients = presentationEmail - ?.generateRecipientsEmailAddressForComposer( - emailActionType: emailActionType, - mailboxRole: mailboxRole - ) ?? const Tuple3([], [], []); - - final newToEmailAddress = listToEmailAddress; - final oldToEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value1 : []; - final isToEmailAddressChanged = !oldToEmailAddress.isSame(newToEmailAddress); - - final newCcEmailAddress = listCcEmailAddress; - final oldCcEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value2 : []; - final isCcEmailAddressChanged = !oldCcEmailAddress.isSame(newCcEmailAddress); - - final newBccEmailAddress = listBccEmailAddress; - final oldBccEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value3 : []; - final isBccEmailAddressChanged = !oldBccEmailAddress.isSame(newBccEmailAddress); - - final isAttachmentsChanged = !initialAttachments.isSame(uploadController.attachmentsUploaded.toList()); - log('ComposerController::_validateChangeEmail: isEmailBodyChanged = $isEmailBodyChanged | isEmailSubjectChanged = $isEmailSubjectChanged | isToEmailAddressChanged = $isToEmailAddressChanged | isCcEmailAddressChanged = $isCcEmailAddressChanged | isBccEmailAddressChanged = $isBccEmailAddressChanged | isAttachmentsChanged = $isAttachmentsChanged'); - if (isEmailBodyChanged || isEmailSubjectChanged - || isToEmailAddressChanged || isCcEmailAddressChanged - || isBccEmailAddressChanged || isAttachmentsChanged) { - return true; + Future _validateEmailChange() async { + final newDraftHash = await _hashDraftEmail(); + + return _savedEmailDraftHash != newDraftHash; + } + + Future _hashDraftEmail() async { + final emailContent = await _getContentInEditor(); + + final savedEmailDraft = SavedEmailDraft( + subject: subjectEmail.value ?? '', + content: emailContent, + toRecipients: listToEmailAddress.toSet(), + ccRecipients: listCcEmailAddress.toSet(), + bccRecipients: listBccEmailAddress.toSet(), + identity: identitySelected.value, + attachments: uploadController.attachmentsUploaded, + hasReadReceipt: hasRequestReadReceipt.value, + ); + + return savedEmailDraft.hashCode; + } + + Future _updateSavedEmailDraftHash() async { + _savedEmailDraftHash = await _hashDraftEmail(); + } + + Future _initEmailDraftHash() async { + if (composerArguments.value?.emailActionType != EmailActionType.compose + && composerArguments.value?.emailActionType != EmailActionType.editDraft + ) { + return; } - return false; + _savedEmailDraftHash = await _hashDraftEmail(); } void handleClickSaveAsDraftsButton(BuildContext context) async { @@ -1306,10 +1311,12 @@ class ComposerController extends BaseController with DragDropFileMixin implement _saveToDraftButtonState = ButtonState.enabled; _emailIdEditing = resultState.emailId; mailboxDashBoardController.consumeState(Stream.value(Right(resultState))); + _updateSavedEmailDraftHash(); } else if (resultState is UpdateEmailDraftsSuccess) { _saveToDraftButtonState = ButtonState.enabled; _emailIdEditing = resultState.emailId; mailboxDashBoardController.consumeState(Stream.value(Right(resultState))); + _updateSavedEmailDraftHash(); } else if ((resultState is SaveEmailAsDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException) || (resultState is UpdateEmailDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException)) { _saveToDraftButtonState = ButtonState.enabled; @@ -1437,6 +1444,8 @@ class ComposerController extends BaseController with DragDropFileMixin implement final selectedIdentityFromHeader = _selectIdentityFromId(identityIdFromHeader); if (selectedIdentityFromHeader == null) return; identitySelected.value = selectedIdentityFromHeader; + + _initEmailDraftHash(); } Future restoreCollapsibleButton(String? emailContent) async { @@ -1445,6 +1454,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement final emailDocument = parse(emailContent); final signature = emailDocument.querySelector('div.tmail-signature'); if (signature == null) return; + _restoringSignatureButton = true; await _applySignature(signature.innerHtml); } catch (e) { logError('ComposerController::_restoreCollapsibleButton: $e'); @@ -1793,7 +1803,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement void _handleUploadInlineSuccess(SuccessAttachmentUploadState uploadState) { uploadController.clearUploadInlineViewState(); - final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl); + final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); final accountId = mailboxDashBoardController.accountId.value; if (baseDownloadUrl != null && accountId != null) { @@ -1988,6 +1998,18 @@ class ComposerController extends BaseController with DragDropFileMixin implement initTextEditor(text); } _textEditorWeb = text; + + _initEmailDraftHashAfterSignatureButtonRestored(text); + } + + void _initEmailDraftHashAfterSignatureButtonRestored(String? emailContent) { + if (!_restoringSignatureButton) return; + final emailDocument = parse(emailContent); + final signatureButton = emailDocument.querySelector('button.tmail-signature-button'); + if (signatureButton == null) return; + + _restoringSignatureButton = false; + _initEmailDraftHash(); } void initTextEditor(String? text) { @@ -2087,12 +2109,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement return; } - final isChanged = await _validateEmailChange( - context: context, - emailActionType: composerArguments.value!.emailActionType, - presentationEmail: composerArguments.value!.presentationEmail, - mailboxRole: composerArguments.value!.mailboxRole - ); + final isChanged = await _validateEmailChange(); if (isChanged && context.mounted) { clearFocus(context); @@ -2368,6 +2385,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement void _setUpRequestReadReceiptForDraftEmail(Email? email) { if (email?.hasRequestReadReceipt == true) { hasRequestReadReceipt.value = true; + _initEmailDraftHash(); } else { _getAlwaysReadReceiptSetting(); } diff --git a/lib/features/composer/presentation/model/saved_email_draft.dart b/lib/features/composer/presentation/model/saved_email_draft.dart new file mode 100644 index 0000000000..a39809032d --- /dev/null +++ b/lib/features/composer/presentation/model/saved_email_draft.dart @@ -0,0 +1,39 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/email/attachment.dart'; + +class SavedEmailDraft with EquatableMixin { + final String content; + final String subject; + final Set toRecipients; + final Set ccRecipients; + final Set bccRecipients; + final List attachments; + final Identity? identity; + final bool hasReadReceipt; + + SavedEmailDraft({ + required this.content, + required this.subject, + required this.toRecipients, + required this.ccRecipients, + required this.bccRecipients, + required this.attachments, + required this.identity, + required this.hasReadReceipt, + }); + + @override + List get props => [ + content, + subject, + // Prevent identical Set + {0: toRecipients}, + {1: ccRecipients}, + {2: bccRecipients}, + attachments, + identity, + hasReadReceipt + ]; +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 801f9dc384..69a0d59200 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1578,7 +1578,7 @@ packages: source: hosted version: "3.1.3" plugin_platform_interface: - dependency: transitive + dependency: "direct dev" description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" diff --git a/pubspec.yaml b/pubspec.yaml index 41d36b3d71..0e3f360dcc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -270,6 +270,8 @@ dev_dependencies: http_mock_adapter: 0.4.2 + plugin_platform_interface: 2.1.8 + dependency_overrides: firebase_core_platform_interface: 4.6.0 diff --git a/test/features/composer/presentation/composer_controller_test.dart b/test/features/composer/presentation/composer_controller_test.dart new file mode 100644 index 0000000000..17a66d7fea --- /dev/null +++ b/test/features/composer/presentation/composer_controller_test.dart @@ -0,0 +1,1459 @@ +import 'package:core/data/network/config/dynamic_url_interceptors.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/utils/application_manager.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:html_editor_enhanced/html_editor.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.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/email/email_header.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/email/email_property.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:rich_text_composer/rich_text_composer.dart'; +import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart'; +import 'package:tmail_ui_user/features/caching/caching_manager.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; +import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/formatting_options_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart'; +import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.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/get_all_identities_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart'; +import 'package:tmail_ui_user/features/server_settings/domain/state/get_always_read_receipt_setting_state.dart'; +import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_always_read_receipt_setting_interactor.dart'; +import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; +import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart'; +import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart'; +import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; +import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; +import 'package:tmail_ui_user/main/utils/toast_manager.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../fixtures/account_fixtures.dart'; +import '../../../fixtures/session_fixtures.dart'; +import '../../../fixtures/widget_fixtures.dart'; +import '../../../mocks/mock_web_view_platform.dart'; +import 'composer_controller_test.mocks.dart'; + +mockControllerCallback() => InternalFinalCallback(callback: () {}); +const fallbackGenerators = { + #onStart: mockControllerCallback, + #onDelete: mockControllerCallback, +}; + +class MockRichTextWebController extends Mock implements RichTextWebController { + @override + Rx get formattingOptionsState => + FormattingOptionsState.disabled.obs; + + @override + bool get isFormattingOptionsEnabled => formattingOptionsState.value == FormattingOptionsState.enabled; + + @override + HtmlEditorController get editorController => MockHtmlEditorController(); + + @override + bool get codeViewEnabled => false; +} + +class MockMailboxDashBoardController extends Mock implements MailboxDashBoardController { + @override + InternalFinalCallback get onStart => mockControllerCallback(); + @override + InternalFinalCallback get onDelete => mockControllerCallback(); + + @override + Rxn get accountId => Rxn(AccountFixtures.aliceAccountId); + @override + Session? get sessionCurrent => SessionFixtures.aliceSession; + + @override + Rxn get attachmentDraggableAppState => Rxn(DraggableAppState.inActive); + @override + bool get isAttachmentDraggableAppActive => attachmentDraggableAppState.value == DraggableAppState.active; + + @override + Rxn get localFileDraggableAppState => Rxn(DraggableAppState.inActive); + @override + bool get isLocalFileDraggableAppActive => localFileDraggableAppState.value == DraggableAppState.active; + + @override + Map get mapDefaultMailboxIdByRole => {PresentationMailbox.roleDrafts: MailboxId(Id('value'))}; + + @override + String get baseDownloadUrl => ''; +} + +@GenerateNiceMocks([ + // Base controller mock specs + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + + // Composer controller mock specs + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(fallbackGenerators: fallbackGenerators), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + + // Additional Getx dependencies mock specs + MockSpec(fallbackGenerators: fallbackGenerators), + MockSpec(), + MockSpec(fallbackGenerators: fallbackGenerators), + + // Additional misc dependencies mock specs + MockSpec(), + MockSpec(), +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Declaration base controller + late MockCachingManager mockCachingManager; + late MockLanguageCacheManager mockLanguageCacheManager; + late MockAuthorizationInterceptors mockAuthorizationInterceptors; + late MockDynamicUrlInterceptors mockDynamicUrlInterceptors; + late MockDeleteCredentialInteractor mockDeleteCredentialInteractor; + late MockLogoutOidcInteractor mockLogoutOidcInteractor; + late MockDeleteAuthorityOidcInteractor mockDeleteAuthorityOidcInteractor; + late MockAppToast mockAppToast; + late MockUuid mockUuid; + late MockApplicationManager mockApplicationManager; + late MockToastManager mockToastManager; + + // Declaration composer controller + late ComposerController? composerController; + late MockLocalFilePickerInteractor mockLocalFilePickerInteractor; + late MockLocalImagePickerInteractor mockLocalImagePickerInteractor; + late MockGetEmailContentInteractor mockGetEmailContentInteractor; + late MockGetAllIdentitiesInteractor mockGetAllIdentitiesInteractor; + late MockUploadController mockUploadController; + late MockRemoveComposerCacheOnWebInteractor mockRemoveComposerCacheOnWebInteractor; + late MockSaveComposerCacheOnWebInteractor mockSaveComposerCacheOnWebInteractor; + late MockDownloadImageAsBase64Interactor mockDownloadImageAsBase64Interactor; + late MockTransformHtmlEmailContentInteractor mockTransformHtmlEmailContentInteractor; + late MockGetAlwaysReadReceiptSettingInteractor mockGetAlwaysReadReceiptSettingInteractor; + late MockCreateNewAndSendEmailInteractor mockCreateNewAndSendEmailInteractor; + late MockCreateNewAndSaveEmailToDraftsInteractor mockCreateNewAndSaveEmailToDraftsInteractor; + + // Declaration Getx dependencies + final mockMailboxDashBoardController = MockMailboxDashBoardController(); + final mockNetworkConnectionController = MockNetworkConnectionController(); + final mockBeforeReconnectManager = MockBeforeReconnectManager(); + final mockRichTextMobileTabletController = MockRichTextMobileTabletController(); + final mockRichTextWebController = MockRichTextWebController(); + + // Declaration misc dependencies + late MockHtmlEditorApi mockHtmlEditorApi; + + setUp(() { + Get.testMode = true; + // Mock base controller + mockCachingManager = MockCachingManager(); + mockLanguageCacheManager = MockLanguageCacheManager(); + mockAuthorizationInterceptors = MockAuthorizationInterceptors(); + mockDynamicUrlInterceptors = MockDynamicUrlInterceptors(); + mockDeleteCredentialInteractor = MockDeleteCredentialInteractor(); + mockLogoutOidcInteractor = MockLogoutOidcInteractor(); + mockDeleteAuthorityOidcInteractor = MockDeleteAuthorityOidcInteractor(); + mockAppToast = MockAppToast(); + mockUuid = MockUuid(); + mockApplicationManager = MockApplicationManager(); + mockToastManager = MockToastManager(); + + Get.put(mockCachingManager); + Get.put(mockLanguageCacheManager); + Get.put(mockAuthorizationInterceptors); + Get.put( + mockAuthorizationInterceptors, + tag: BindingTag.isolateTag, + ); + Get.put(mockDynamicUrlInterceptors); + Get.put(mockDeleteCredentialInteractor); + Get.put(mockLogoutOidcInteractor); + Get.put(mockDeleteAuthorityOidcInteractor); + Get.put(mockAppToast); + Get.put(ImagePaths()); + Get.put(ResponsiveUtils()); + Get.put(mockUuid); + Get.put(mockApplicationManager); + Get.put(mockToastManager); + + // Mock Getx controllers + Get.put(mockMailboxDashBoardController); + Get.put(mockNetworkConnectionController); + Get.put(mockBeforeReconnectManager); + Get.put(mockRichTextMobileTabletController); + + // Mock composer controller + mockLocalFilePickerInteractor = MockLocalFilePickerInteractor(); + mockLocalImagePickerInteractor = MockLocalImagePickerInteractor(); + mockGetEmailContentInteractor = MockGetEmailContentInteractor(); + mockGetAllIdentitiesInteractor = MockGetAllIdentitiesInteractor(); + mockUploadController = MockUploadController(); + mockRemoveComposerCacheOnWebInteractor = MockRemoveComposerCacheOnWebInteractor(); + mockSaveComposerCacheOnWebInteractor = MockSaveComposerCacheOnWebInteractor(); + mockDownloadImageAsBase64Interactor = MockDownloadImageAsBase64Interactor(); + mockTransformHtmlEmailContentInteractor = MockTransformHtmlEmailContentInteractor(); + mockGetAlwaysReadReceiptSettingInteractor = MockGetAlwaysReadReceiptSettingInteractor(); + mockCreateNewAndSendEmailInteractor = MockCreateNewAndSendEmailInteractor(); + mockCreateNewAndSaveEmailToDraftsInteractor = MockCreateNewAndSaveEmailToDraftsInteractor(); + + composerController = ComposerController( + mockLocalFilePickerInteractor, + mockLocalImagePickerInteractor, + mockGetEmailContentInteractor, + mockGetAllIdentitiesInteractor, + mockUploadController, + mockRemoveComposerCacheOnWebInteractor, + mockSaveComposerCacheOnWebInteractor, + mockDownloadImageAsBase64Interactor, + mockTransformHtmlEmailContentInteractor, + mockGetAlwaysReadReceiptSettingInteractor, + mockCreateNewAndSendEmailInteractor, + mockCreateNewAndSaveEmailToDraftsInteractor + ); + + mockHtmlEditorApi = MockHtmlEditorApi(); + }); + + tearDown(() { + Get.reset(); + composerController = null; + }); + + group('ComposerController test:', () { + group('hash draft email test:', () { + const emailContent = 'some email content'; + const emailSubject = 'some email subject'; + final toRecipient = EmailAddress('to', 'to@linagora.com'); + final ccRecipient = EmailAddress('cc', 'cc@linagora.com'); + final bccRecipient = EmailAddress('bcc', 'bcc@linagora.com'); + final identity = Identity(); + final attachment = Attachment(); + const alwaysReadReceiptEnabled = true; + + group('email action type is EmailActionType.compose:', () { + setUp(() { + composerController?.composerArguments.value = ComposerArguments( + emailActionType: EmailActionType.compose); + }); + + test( + 'should update _savedEmailDraftHash ' + 'when there is a new view state ' + 'and the state is GetAlwaysReadReceiptSettingSuccess', + () async { + // arrange + composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; + when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( + mockHtmlEditorApi); + + when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.identitySelected.value = identity; + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + final state = GetAlwaysReadReceiptSettingSuccess( + alwaysReadReceiptEnabled: alwaysReadReceiptEnabled); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: identity, + attachments: [attachment], + hasReadReceipt: alwaysReadReceiptEnabled + ); + + // act + composerController?.handleSuccessViewState(state); + await untilCalled(mockHtmlEditorApi.getText()); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + }); + + test( + 'should update _savedEmailDraftHash ' + 'when there is a new view state ' + 'and the state is GetAlwaysReadReceiptSettingFailure', + () async { + // arrange + composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; + when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( + mockHtmlEditorApi); + + when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.identitySelected.value = identity; + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + final state = GetAlwaysReadReceiptSettingFailure(Exception()); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: identity, + attachments: [attachment], + hasReadReceipt: false + ); + + // act + composerController?.handleFailureViewState(state); + await untilCalled(mockHtmlEditorApi.getText()); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + }); + + test( + 'should update _savedEmailDraftHash ' + 'when _initIdentities is called ' + 'and listFromIdentities is not empty ' + 'and selectedIdentity is available', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + composerController?.onChangeTextEditorWeb(emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + final selectedIdentity = Identity(id: IdentityId(Id('alice'))); + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + when(mockMailboxDashBoardController.composerArguments).thenReturn( + ComposerArguments( + selectedIdentityId: selectedIdentity.id, + identities: [selectedIdentity])); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: selectedIdentity, + attachments: [attachment], + hasReadReceipt: false + ); + + // act + composerController?.onReady(); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + + test( + 'should update _savedEmailDraftHash ' + 'when _initIdentities is called ' + 'and listFromIdentities is not empty ' + 'and selectedIdentity is not available', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + composerController?.onChangeTextEditorWeb(emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + when(mockMailboxDashBoardController.composerArguments).thenReturn( + ComposerArguments(identities: [identity])); + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: identity, + attachments: [attachment], + hasReadReceipt: false + ); + + // act + composerController?.onReady(); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + + test( + 'should update _savedEmailDraftHash ' + 'when _initIdentities is called ' + 'and listFromIdentities is empty ' + 'and selectedIdentity is available', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + composerController?.onChangeTextEditorWeb(emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + final selectedIdentity = Identity( + id: IdentityId(Id('alice')), + mayDelete: true); + when(mockMailboxDashBoardController.composerArguments).thenReturn( + ComposerArguments(selectedIdentityId: selectedIdentity.id)); + when(mockGetAllIdentitiesInteractor.execute(any, any)).thenAnswer( + (_) => Stream.value( + Right(GetAllIdentitiesSuccess([selectedIdentity], null)))); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: selectedIdentity, + attachments: [attachment], + hasReadReceipt: false + ); + + // act + composerController?.onReady(); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + + test( + 'should update _savedEmailDraftHash ' + 'when _initIdentities is called ' + 'and listFromIdentities is empty ' + 'and selectedIdentity is not available', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + composerController?.onChangeTextEditorWeb(emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + final identity = Identity( + id: IdentityId(Id('alice')), + mayDelete: true); + when(mockMailboxDashBoardController.composerArguments).thenReturn( + ComposerArguments()); + when(mockGetAllIdentitiesInteractor.execute(any, any)).thenAnswer( + (_) => Stream.value( + Right(GetAllIdentitiesSuccess([identity], null)))); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: identity, + attachments: [attachment], + hasReadReceipt: false + ); + + // act + composerController?.onReady(); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + + testWidgets( + 'should update _savedEmailDraftHash ' + 'when user click save draft button ' + 'and SaveEmailAsDraftsSuccess is returned', + (tester) async { + await tester.runAsync(() async { + // arrange + PlatformInfo.isTestingForWeb = true; + InAppWebViewPlatform.instance = MockWebViewPlatform(); + + when(mockUploadController.uploadInlineViewState).thenReturn( + Rx(Right(UIState.idle))); + when(mockUploadController.listUploadAttachments).thenReturn( + RxList()); + + Get.put(composerController!); + composerController?.richTextWebController = mockRichTextWebController; + + composerController?.onChangeTextEditorWeb(emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + final selectedIdentity = Identity(id: IdentityId(Id('alice'))); + composerController?.identitySelected.value = selectedIdentity; + composerController?.composerArguments.value = ComposerArguments(); + when( + mockCreateNewAndSaveEmailToDraftsInteractor.execute( + createEmailRequest: anyNamed('createEmailRequest'), + cancelToken: anyNamed('cancelToken'))) + .thenAnswer((_) => Stream.value( + Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')))))); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: selectedIdentity, + attachments: [attachment], + hasReadReceipt: false + ); + + await tester.pumpWidget(WidgetFixtures.makeTestableWidget( + child: const Stack(children: [ComposerView()]))); + await tester.pump(); + + // act + final saveAsDraftButton = find.ancestor( + of: find.byType(InkWell), + matching: find.byWidgetPredicate( + (widget) => widget is TMailButtonWidget + && widget.icon == ImagePaths().icSaveToDraft)); + await tester.tap(saveAsDraftButton); + await tester.pump(); + await untilCalled( + mockCreateNewAndSaveEmailToDraftsInteractor.execute( + createEmailRequest: anyNamed('createEmailRequest'), + cancelToken: anyNamed('cancelToken'))); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + }); + + testWidgets( + 'should update _savedEmailDraftHash ' + 'when user click save draft button ' + 'and UpdateEmailDraftsSuccess is returned', + (tester) async { + await tester.runAsync(() async { + // arrange + PlatformInfo.isTestingForWeb = true; + InAppWebViewPlatform.instance = MockWebViewPlatform(); + + when(mockUploadController.uploadInlineViewState).thenReturn( + Rx(Right(UIState.idle))); + when(mockUploadController.listUploadAttachments).thenReturn( + RxList()); + + Get.put(composerController!); + composerController?.richTextWebController = mockRichTextWebController; + + composerController?.onChangeTextEditorWeb(emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + final selectedIdentity = Identity(id: IdentityId(Id('alice'))); + composerController?.identitySelected.value = selectedIdentity; + composerController?.composerArguments.value = ComposerArguments(); + when( + mockCreateNewAndSaveEmailToDraftsInteractor.execute( + createEmailRequest: anyNamed('createEmailRequest'), + cancelToken: anyNamed('cancelToken'))) + .thenAnswer((_) => Stream.value( + Right(UpdateEmailDraftsSuccess(EmailId(Id('123')))))); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: selectedIdentity, + attachments: [attachment], + hasReadReceipt: false + ); + + await tester.pumpWidget(WidgetFixtures.makeTestableWidget( + child: const Stack(children: [ComposerView()]))); + await tester.pump(); + + // act + final saveAsDraftButton = find.ancestor( + of: find.byType(InkWell), + matching: find.byWidgetPredicate( + (widget) => widget is TMailButtonWidget + && widget.icon == ImagePaths().icSaveToDraft)); + await tester.tap(saveAsDraftButton); + await tester.pump(); + await untilCalled( + mockCreateNewAndSaveEmailToDraftsInteractor.execute( + createEmailRequest: anyNamed('createEmailRequest'), + cancelToken: anyNamed('cancelToken'))); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + }); + }); + + group('email action type is EmailActionType.editDraft:', () { + setUp(() { + composerController?.composerArguments.value = ComposerArguments( + emailActionType: EmailActionType.editDraft); + }); + + test( + 'should update _savedEmailDraftHash ' + 'when there is a new view state ' + 'and the state is GetAlwaysReadReceiptSettingSuccess', + () async { + // arrange + composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; + when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( + mockHtmlEditorApi); + + when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.identitySelected.value = identity; + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + const alwaysReadReceiptEnabled = true; + final state = GetAlwaysReadReceiptSettingSuccess( + alwaysReadReceiptEnabled: alwaysReadReceiptEnabled); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: identity, + attachments: [attachment], + hasReadReceipt: alwaysReadReceiptEnabled + ); + + // act + composerController?.handleSuccessViewState(state); + await untilCalled(mockHtmlEditorApi.getText()); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + }); + + test( + 'should update _savedEmailDraftHash ' + 'when there is a new view state ' + 'and the state is GetAlwaysReadReceiptSettingFailure', + () async { + // arrange + composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; + when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( + mockHtmlEditorApi); + + when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.identitySelected.value = identity; + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + final state = GetAlwaysReadReceiptSettingFailure(Exception()); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: identity, + attachments: [attachment], + hasReadReceipt: false + ); + + // act + composerController?.handleFailureViewState(state); + await untilCalled(mockHtmlEditorApi.getText()); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + }); + + test( + 'should update _savedEmailDraftHash ' + 'when _initIdentities is called ' + 'and listFromIdentities is not empty ' + 'and selectedIdentity is available', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + composerController?.onChangeTextEditorWeb(emailContent); + + final selectedIdentity = Identity(id: IdentityId(Id('alice'))); + + when(mockMailboxDashBoardController.composerArguments).thenReturn( + ComposerArguments( + emailActionType: EmailActionType.editDraft, + emailContents: emailContent, + presentationEmail: PresentationEmail( + id: EmailId(Id('some-email-id')), + subject: emailSubject, + to: {toRecipient}, + cc: {ccRecipient}, + bcc: {bccRecipient}, + mailboxContain: PresentationMailbox( + MailboxId(Id('some-mailbox-id')), + role: PresentationMailbox.roleJunk)), + selectedIdentityId: selectedIdentity.id, + identities: [selectedIdentity])); + + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: selectedIdentity, + attachments: [attachment], + hasReadReceipt: false + ); + + // act + composerController?.onReady(); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + + test( + 'should update _savedEmailDraftHash ' + 'when _initIdentities is called ' + 'and listFromIdentities is not empty ' + 'and selectedIdentity is not available', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + composerController?.onChangeTextEditorWeb(emailContent); + + final identity = Identity(id: IdentityId(Id('alice'))); + + when(mockMailboxDashBoardController.composerArguments).thenReturn( + ComposerArguments( + emailActionType: EmailActionType.editDraft, + emailContents: emailContent, + presentationEmail: PresentationEmail( + id: EmailId(Id('some-email-id')), + subject: emailSubject, + to: {toRecipient}, + cc: {ccRecipient}, + bcc: {bccRecipient}, + mailboxContain: PresentationMailbox( + MailboxId(Id('some-mailbox-id')), + role: PresentationMailbox.roleJunk)), + identities: [identity])); + + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: identity, + attachments: [attachment], + hasReadReceipt: false + ); + + // act + composerController?.onReady(); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + + test( + 'should update _savedEmailDraftHash ' + 'when _initIdentities is called ' + 'and listFromIdentities is empty ' + 'and selectedIdentity is available', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + composerController?.onChangeTextEditorWeb(emailContent); + + final selectedIdentity = Identity( + id: IdentityId(Id('alice')), + mayDelete: true); + + when(mockMailboxDashBoardController.composerArguments).thenReturn( + ComposerArguments( + emailActionType: EmailActionType.editDraft, + emailContents: emailContent, + presentationEmail: PresentationEmail( + id: EmailId(Id('some-email-id')), + subject: emailSubject, + to: {toRecipient}, + cc: {ccRecipient}, + bcc: {bccRecipient}, + mailboxContain: PresentationMailbox( + MailboxId(Id('some-mailbox-id')), + role: PresentationMailbox.roleJunk)), + selectedIdentityId: selectedIdentity.id)); + when(mockGetAllIdentitiesInteractor.execute(any, any)).thenAnswer( + (_) => Stream.value( + Right(GetAllIdentitiesSuccess([selectedIdentity], null)))); + + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: selectedIdentity, + attachments: [attachment], + hasReadReceipt: false + ); + + // act + composerController?.onReady(); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + + test( + 'should update _savedEmailDraftHash ' + 'when _initIdentities is called ' + 'and listFromIdentities is empty ' + 'and selectedIdentity is not available', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + composerController?.onChangeTextEditorWeb(emailContent); + + final identity = Identity( + id: IdentityId(Id('alice')), + mayDelete: true); + + when(mockMailboxDashBoardController.composerArguments).thenReturn( + ComposerArguments( + emailActionType: EmailActionType.editDraft, + emailContents: emailContent, + presentationEmail: PresentationEmail( + id: EmailId(Id('some-email-id')), + subject: emailSubject, + to: {toRecipient}, + cc: {ccRecipient}, + bcc: {bccRecipient}, + mailboxContain: PresentationMailbox( + MailboxId(Id('some-mailbox-id')), + role: PresentationMailbox.roleJunk)),)); + when(mockGetAllIdentitiesInteractor.execute(any, any)).thenAnswer( + (_) => Stream.value( + Right(GetAllIdentitiesSuccess([identity], null)))); + + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: identity, + attachments: [attachment], + hasReadReceipt: false + ); + + // act + composerController?.onReady(); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + + testWidgets( + 'should update _savedEmailDraftHash ' + 'when user click save draft button ' + 'and SaveEmailAsDraftsSuccess is returned', + (tester) async { + await tester.runAsync(() async { + // arrange + PlatformInfo.isTestingForWeb = true; + InAppWebViewPlatform.instance = MockWebViewPlatform(); + + when(mockUploadController.uploadInlineViewState).thenReturn( + Rx(Right(UIState.idle))); + when(mockUploadController.listUploadAttachments).thenReturn( + RxList()); + + Get.put(composerController!); + composerController?.richTextWebController = mockRichTextWebController; + + composerController?.onChangeTextEditorWeb(emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + final selectedIdentity = Identity(id: IdentityId(Id('alice'))); + composerController?.identitySelected.value = selectedIdentity; + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + composerController?.composerArguments.value = ComposerArguments( + emailActionType: EmailActionType.editDraft); + when( + mockCreateNewAndSaveEmailToDraftsInteractor.execute( + createEmailRequest: anyNamed('createEmailRequest'), + cancelToken: anyNamed('cancelToken'))) + .thenAnswer((_) => Stream.value( + Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')))))); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: selectedIdentity, + attachments: [attachment], + hasReadReceipt: false + ); + + await tester.pumpWidget(WidgetFixtures.makeTestableWidget( + child: const Stack(children: [ComposerView()]))); + await tester.pump(); + + // act + final saveAsDraftButton = find.ancestor( + of: find.byType(InkWell), + matching: find.byWidgetPredicate( + (widget) => widget is TMailButtonWidget + && widget.icon == ImagePaths().icSaveToDraft)); + await tester.tap(saveAsDraftButton); + await tester.pump(); + await untilCalled( + mockCreateNewAndSaveEmailToDraftsInteractor.execute( + createEmailRequest: anyNamed('createEmailRequest'), + cancelToken: anyNamed('cancelToken'))); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + }); + + testWidgets( + 'should update _savedEmailDraftHash ' + 'when user click save draft button ' + 'and UpdateEmailDraftsSuccess is returned', + (tester) async { + await tester.runAsync(() async { + // arrange + PlatformInfo.isTestingForWeb = true; + InAppWebViewPlatform.instance = MockWebViewPlatform(); + + when(mockUploadController.uploadInlineViewState).thenReturn( + Rx(Right(UIState.idle))); + when(mockUploadController.listUploadAttachments).thenReturn( + RxList()); + + Get.put(composerController!); + composerController?.richTextWebController = mockRichTextWebController; + + composerController?.onChangeTextEditorWeb(emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + final selectedIdentity = Identity(id: IdentityId(Id('alice'))); + composerController?.identitySelected.value = selectedIdentity; + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + composerController?.composerArguments.value = ComposerArguments( + emailActionType: EmailActionType.editDraft); + when( + mockCreateNewAndSaveEmailToDraftsInteractor.execute( + createEmailRequest: anyNamed('createEmailRequest'), + cancelToken: anyNamed('cancelToken'))) + .thenAnswer((_) => Stream.value( + Right(UpdateEmailDraftsSuccess(EmailId(Id('123')))))); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: selectedIdentity, + attachments: [attachment], + hasReadReceipt: false + ); + + await tester.pumpWidget(WidgetFixtures.makeTestableWidget( + child: const Stack(children: [ComposerView()]))); + await tester.pump(); + + // act + final saveAsDraftButton = find.ancestor( + of: find.byType(InkWell), + matching: find.byWidgetPredicate( + (widget) => widget is TMailButtonWidget + && widget.icon == ImagePaths().icSaveToDraft)); + await tester.tap(saveAsDraftButton); + await tester.pump(); + await untilCalled( + mockCreateNewAndSaveEmailToDraftsInteractor.execute( + createEmailRequest: anyNamed('createEmailRequest'), + cancelToken: anyNamed('cancelToken'))); + + // assert + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + }); + + test( + 'should update _savedEmailDraftHash with the same value' + 'and call _updateSavedEmailDraftHash twice ' + 'when there is a new view state ' + 'and the state is GetEmailContentSuccess', + () async { + // arrange + composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; + when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( + mockHtmlEditorApi); + + when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContent); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.hasRequestReadReceipt.value = alwaysReadReceiptEnabled; + + const idenityId = 'some-identity-id'; + final identity = Identity(id: IdentityId(Id(idenityId))); + composerController?.identitySelected.value = identity; + composerController?.listFromIdentities.add(identity); + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + final state = GetEmailContentSuccess( + htmlEmailContent: '', + emailCurrent: Email( + identityHeader: {IndividualHeaderIdentifier.identityHeader: idenityId}, + headers: {EmailHeader(EmailProperty.headerMdnKey, 'value')})); + + final savedEmailDraft = SavedEmailDraft( + content: emailContent, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: identity, + attachments: [attachment], + hasReadReceipt: alwaysReadReceiptEnabled + ); + + // act + composerController?.handleSuccessViewState(state); + await untilCalled(mockHtmlEditorApi.getText()); + + // assert + verify(mockHtmlEditorApi.getText()).called(2); + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + }); + + test( + 'should update _savedEmailDraftHash ' + 'when restoring signature button finished', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + const emailContentWithSignature = '
'; + const emailContentWithSignatureButton = '
'; + + when(mockHtmlEditorApi.getText()).thenAnswer((_) async => emailContentWithSignatureButton); + composerController?.subjectEmail.value = emailSubject; + composerController?.listToEmailAddress = [toRecipient]; + composerController?.listCcEmailAddress = [ccRecipient]; + composerController?.listBccEmailAddress = [bccRecipient]; + composerController?.identitySelected.value = identity; + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + composerController?.hasRequestReadReceipt.value = alwaysReadReceiptEnabled; + + final savedEmailDraft = SavedEmailDraft( + content: emailContentWithSignatureButton, + subject: emailSubject, + toRecipients: {toRecipient}, + ccRecipients: {ccRecipient}, + bccRecipients: {bccRecipient}, + identity: identity, + attachments: [attachment], + hasReadReceipt: alwaysReadReceiptEnabled + ); + + // act + await composerController?.restoreCollapsibleButton(emailContentWithSignature); + expect(composerController?.restoringSignatureButton, true); + composerController?.onChangeTextEditorWeb(emailContentWithSignatureButton); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.restoringSignatureButton, false); + expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + }); + + group('email action type is neither EmailActionType.compose nor EmailActionType.editDraft:', () { + setUp(() { + composerController?.composerArguments.value = ComposerArguments( + emailActionType: EmailActionType.reply); + }); + + test( + 'should not update _savedEmailDraftHash ' + 'when there is a new view state ' + 'and the state is GetAlwaysReadReceiptSettingSuccess', + () async { + // arrange + composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; + when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( + mockHtmlEditorApi); + + final state = GetAlwaysReadReceiptSettingSuccess( + alwaysReadReceiptEnabled: true); + + // act + composerController?.handleSuccessViewState(state); + + // assert + verifyNever(mockHtmlEditorApi.getText()); + expect(composerController?.savedEmailDraftHash, isNull); + }); + + test( + 'should not update _savedEmailDraftHash ' + 'when there is a new view state ' + 'and the state is GetAlwaysReadReceiptSettingFailure', + () async { + // arrange + composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; + when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( + mockHtmlEditorApi); + + final state = GetAlwaysReadReceiptSettingFailure(Exception()); + + // act + composerController?.handleFailureViewState(state); + + // assert + verifyNever(mockHtmlEditorApi.getText()); + expect(composerController?.savedEmailDraftHash, isNull); + }); + + test( + 'should not update _savedEmailDraftHash ' + 'when _initIdentities is called ' + 'and listFromIdentities is not empty ' + 'and selectedIdentity is available', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + final selectedIdentity = Identity(id: IdentityId(Id('alice'))); + when(mockMailboxDashBoardController.composerArguments).thenReturn( + ComposerArguments( + emailActionType: EmailActionType.reply, + emailContents: emailContent, + presentationEmail: PresentationEmail( + mailboxContain: PresentationMailbox( + MailboxId(Id('some-mailbox-id')), + role: PresentationMailbox.roleJunk)), + selectedIdentityId: selectedIdentity.id, + identities: [selectedIdentity])); + + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + // act + composerController?.onReady(); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.savedEmailDraftHash, isNull); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + + test( + 'should not update _savedEmailDraftHash ' + 'when _initIdentities is called ' + 'and listFromIdentities is not empty ' + 'and selectedIdentity is not available', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + final identity = Identity(id: IdentityId(Id('alice'))); + when(mockMailboxDashBoardController.composerArguments).thenReturn( + ComposerArguments( + emailActionType: EmailActionType.reply, + emailContents: emailContent, + presentationEmail: PresentationEmail( + mailboxContain: PresentationMailbox( + MailboxId(Id('some-mailbox-id')), + role: PresentationMailbox.roleJunk)), + identities: [identity])); + + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + // act + composerController?.onReady(); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.savedEmailDraftHash, isNull); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + + test( + 'should not update _savedEmailDraftHash ' + 'when _initIdentities is called ' + 'and listFromIdentities is empty ' + 'and selectedIdentity is available', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + final selectedIdentity = Identity( + id: IdentityId(Id('alice')), + mayDelete: true); + when(mockMailboxDashBoardController.composerArguments).thenReturn( + ComposerArguments( + emailActionType: EmailActionType.reply, + emailContents: emailContent, + presentationEmail: PresentationEmail( + mailboxContain: PresentationMailbox( + MailboxId(Id('some-mailbox-id')), + role: PresentationMailbox.roleJunk)), + selectedIdentityId: selectedIdentity.id)); + when(mockGetAllIdentitiesInteractor.execute(any, any)).thenAnswer( + (_) => Stream.value( + Right(GetAllIdentitiesSuccess([selectedIdentity], null)))); + + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + // act + composerController?.onReady(); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.savedEmailDraftHash, isNull); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + + test( + 'should not update _savedEmailDraftHash ' + 'when _initIdentities is called ' + 'and listFromIdentities is empty ' + 'and selectedIdentity is not available', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + + final identity = Identity( + id: IdentityId(Id('alice')), + mayDelete: true); + + when(mockMailboxDashBoardController.composerArguments).thenReturn( + ComposerArguments( + emailActionType: EmailActionType.reply, + emailContents: emailContent, + presentationEmail: PresentationEmail( + mailboxContain: PresentationMailbox( + MailboxId(Id('some-mailbox-id')), + role: PresentationMailbox.roleJunk)),)); + when(mockGetAllIdentitiesInteractor.execute(any, any)).thenAnswer( + (_) => Stream.value( + Right(GetAllIdentitiesSuccess([identity], null)))); + + when(mockUploadController.attachmentsUploaded).thenReturn([attachment]); + + // act + composerController?.onReady(); + await Future.delayed(Duration.zero); + + // assert + expect(composerController?.savedEmailDraftHash, isNull); + + // tear down + PlatformInfo.isTestingForWeb = false; + }); + + test( + 'should not update _savedEmailDraftHash ' + 'when there is a new view state ' + 'and the state is GetEmailContentSuccess', + () async { + // arrange + composerController?.richTextMobileTabletController = mockRichTextMobileTabletController; + when(mockRichTextMobileTabletController.htmlEditorApi).thenReturn( + mockHtmlEditorApi); + + const idenityId = 'some-identity-id'; + final identity = Identity(id: IdentityId(Id(idenityId))); + composerController?.identitySelected.value = identity; + composerController?.listFromIdentities.add(identity); + + final state = GetEmailContentSuccess( + htmlEmailContent: emailContent, + emailCurrent: Email( + identityHeader: {IndividualHeaderIdentifier.identityHeader: idenityId})); + + // act + composerController?.handleSuccessViewState(state); + + // assert + verifyNever(mockHtmlEditorApi.getText()); + expect(composerController?.savedEmailDraftHash, isNull); + }); + }); + }); + }); +} \ No newline at end of file diff --git a/test/features/composer/presentation/model/saved_email_draft_test.dart b/test/features/composer/presentation/model/saved_email_draft_test.dart new file mode 100644 index 0000000000..1544ded790 --- /dev/null +++ b/test/features/composer/presentation/model/saved_email_draft_test.dart @@ -0,0 +1,148 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.dart'; + +void main() { + group('saved email draft test', () { + test( + 'should tag toRecipients, ccRecipients and bccRecipients in props', + () { + // arrange + final savedEmailDraft = SavedEmailDraft( + subject: 'subject', + content: 'content', + toRecipients: {EmailAddress('to name', 'to email')}, + ccRecipients: {EmailAddress('cc name', 'cc email')}, + bccRecipients: {EmailAddress('bcc name', 'bcc email')}, + identity: null, + attachments: [], + hasReadReceipt: false + ); + + // act + final props = savedEmailDraft.props; + + // assert + expect(props[2], equals({0: savedEmailDraft.toRecipients})); + expect(props[3], equals({1: savedEmailDraft.ccRecipients})); + expect(props[4], equals({2: savedEmailDraft.bccRecipients})); + }); + + test( + 'should generate different hashcode ' + 'when toRecipients, ccRecipients and bccRecipients are different', + () { + // arrange + const subject = 'subject'; + const content = 'content'; + final recipent = EmailAddress('recipent name', 'recipent email'); + final identity = Identity(); + final attachments = []; + const hasReadReceipt = false; + + final toSavedEmailDraft = SavedEmailDraft( + subject: subject, + content: content, + toRecipients: {recipent}, + ccRecipients: {}, + bccRecipients: {}, + identity: identity, + attachments: attachments, + hasReadReceipt: hasReadReceipt + ); + + final ccSavedEmailDraft = SavedEmailDraft( + subject: subject, + content: content, + toRecipients: {}, + ccRecipients: {recipent}, + bccRecipients: {}, + identity: identity, + attachments: attachments, + hasReadReceipt: hasReadReceipt + ); + + final bccSavedEmailDraft = SavedEmailDraft( + subject: subject, + content: content, + toRecipients: {}, + ccRecipients: {}, + bccRecipients: {recipent}, + identity: identity, + attachments: attachments, + hasReadReceipt: hasReadReceipt + ); + + // act + final toProps = toSavedEmailDraft.props; + final ccProps = ccSavedEmailDraft.props; + final bccProps = bccSavedEmailDraft.props; + + // assert + expect(toProps.hashCode, isNot(ccProps.hashCode)); + expect(ccProps.hashCode, isNot(bccProps.hashCode)); + expect(bccProps.hashCode, isNot(toProps.hashCode)); + }); + + test( + 'should generate different hashcode ' + 'when toRecipients is updated', + () { + // arrange + final listToRecipients = { + EmailAddress('to name', 'to email') + }; + final savedEmailDraft = SavedEmailDraft( + subject: 'subject', + content: 'content', + toRecipients: listToRecipients, + ccRecipients: {EmailAddress('cc name', 'cc email')}, + bccRecipients: {EmailAddress('bcc name', 'bcc email')}, + identity: null, + attachments: [], + hasReadReceipt: false + ); + final hashCodeBeforeChange = savedEmailDraft.hashCode; + + // act + listToRecipients.add(EmailAddress('to name 2', 'to email 2')); + final hashCodeAfterChange = savedEmailDraft.hashCode; + + // assert + expect(hashCodeBeforeChange, isNot(hashCodeAfterChange)); + }); + + test( + 'should generate same hashcode ' + 'when all properties are the same', + () { + // arrange + final savedEmailDraft = SavedEmailDraft( + subject: 'subject', + content: 'content', + toRecipients: {EmailAddress('to name', 'to email')}, + ccRecipients: {EmailAddress('cc name', 'cc email')}, + bccRecipients: {EmailAddress('bcc name', 'bcc email')}, + identity: null, + attachments: [], + hasReadReceipt: false + ); + + final savedEmailDraft2 = SavedEmailDraft( + subject: 'subject', + content: 'content', + toRecipients: {EmailAddress('to name', 'to email')}, + ccRecipients: {EmailAddress('cc name', 'cc email')}, + bccRecipients: {EmailAddress('bcc name', 'bcc email')}, + identity: null, + attachments: [], + hasReadReceipt: false + ); + + // assert + expect(savedEmailDraft.hashCode, equals(savedEmailDraft2.hashCode)); + }); + }); +} \ No newline at end of file diff --git a/test/mocks/mock_web_view_platform.dart b/test/mocks/mock_web_view_platform.dart new file mode 100644 index 0000000000..a3f1fdc85b --- /dev/null +++ b/test/mocks/mock_web_view_platform.dart @@ -0,0 +1,58 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:rich_text_composer/rich_text_composer.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockWebViewPlatform extends InAppWebViewPlatform with MockPlatformInterfaceMixin { + @override + PlatformInAppWebViewWidget createPlatformInAppWebViewWidget( + PlatformInAppWebViewWidgetCreationParams params, + ) => MockWebViewWidget.implementation(params); + + @override + PlatformCookieManager createPlatformCookieManager( + PlatformCookieManagerCreationParams params, + ) => MockPlatformCookieManager(); +} + +class MockPlatformCookieManager extends Fake implements PlatformCookieManager { + @override + Future deleteAllCookies() async { + return true; + } + + @override + Future setCookie({ + required WebUri url, + required String name, + required String value, + String path = '/', + String? domain, + int? expiresDate, + int? maxAge, + bool? isSecure, + bool? isHttpOnly, + HTTPCookieSameSitePolicy? sameSite, + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController, + }) async { + return true; + } +} + +class MockWebViewWidget extends PlatformInAppWebViewWidget { + MockWebViewWidget.implementation(super.params) : super.implementation(); + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } + + @override + T controllerFromPlatform(PlatformInAppWebViewController controller) { + throw UnimplementedError(); + } + + @override + void dispose() {} +} From 7f23fc0c387aad5b36aa2240a43e72d33a9bed21 Mon Sep 17 00:00:00 2001 From: DatDang Date: Wed, 2 Oct 2024 16:23:11 +0700 Subject: [PATCH 2/2] TF-3171 Fix duplicate signature button on Composer view changed --- lib/features/composer/presentation/composer_controller.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 17b632328f..62a30e80c3 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1452,6 +1452,11 @@ class ComposerController extends BaseController with DragDropFileMixin implement try { if (emailContent == null) return; final emailDocument = parse(emailContent); + + final existedSignatureButton = emailDocument.querySelector( + 'button.tmail-signature-button'); + if (existedSignatureButton != null) return; + final signature = emailDocument.querySelector('div.tmail-signature'); if (signature == null) return; _restoringSignatureButton = true; @@ -1935,6 +1940,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement void handleInitHtmlEditorWeb(String initContent) async { + if (_isEmailBodyLoaded) return; log('ComposerController::handleInitHtmlEditorWeb:'); _isEmailBodyLoaded = true; richTextWebController?.editorController.setFullScreen();