diff --git a/integration_test/models/provisioning_email.dart b/integration_test/models/provisioning_email.dart new file mode 100644 index 000000000..dd84bdf22 --- /dev/null +++ b/integration_test/models/provisioning_email.dart @@ -0,0 +1,13 @@ +class ProvisioningEmail { + final String toEmail; + final String subject; + final String content; + final List attachmentPaths; + + ProvisioningEmail({ + required this.toEmail, + required this.subject, + required this.content, + this.attachmentPaths = const [], + }); +} \ No newline at end of file diff --git a/integration_test/robots/search_robot.dart b/integration_test/robots/search_robot.dart index 8923b5091..549ad0f60 100644 --- a/integration_test/robots/search_robot.dart +++ b/integration_test/robots/search_robot.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:core/presentation/views/text/text_field_builder.dart'; +import 'package:tmail_ui_user/features/search/email/presentation/search_email_view.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import '../base/core_robot.dart'; @@ -27,4 +30,13 @@ class SearchRobot extends CoreRobot { await $(find.text(sortOrderName)).tap(); await $.pump(const Duration(seconds: 2)); } + + Future enterKeyword(String keyword) async { + await $(SearchEmailView).$(TextFieldBuilder).enterText(keyword); + } + + Future tapOnShowAllResultsText() async { + await $.waitUntilVisible($(AppLocalizations().showingResultsFor)); + await $(AppLocalizations().showingResultsFor).tap(); + } } \ No newline at end of file diff --git a/integration_test/robots/thread_robot.dart b/integration_test/robots/thread_robot.dart index 6ff0767b1..8789a7af7 100644 --- a/integration_test/robots/thread_robot.dart +++ b/integration_test/robots/thread_robot.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/views/search/search_bar_view.dart'; import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/base/widget/compose_floating_button.dart'; +import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart'; import '../base/core_robot.dart'; @@ -14,4 +15,8 @@ class ThreadRobot extends CoreRobot { Future openSearchView() async { await $(SearchBarView).$(InkWell).tap(); } + + Future tapOnSearchField() async { + await $(ThreadView).$(SearchBarView).tap(); + } } \ No newline at end of file diff --git a/integration_test/scenarios/search_result_highlights_scenario.dart b/integration_test/scenarios/search_result_highlights_scenario.dart new file mode 100644 index 000000000..38dbf4c71 --- /dev/null +++ b/integration_test/scenarios/search_result_highlights_scenario.dart @@ -0,0 +1,57 @@ +import 'package:core/core.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../base/base_scenario.dart'; +import '../models/provisioning_email.dart'; +import '../robots/search_robot.dart'; +import '../robots/thread_robot.dart'; +import '../utils/scenario_utils_mixin.dart'; +import 'login_with_basic_auth_scenario.dart'; + +class SearchResultHighlightsScenario extends BaseScenario with ScenarioUtilsMixin { + SearchResultHighlightsScenario( + super.$, { + required this.loginWithBasicAuthScenario, + required this.keyword, + required this.longEmailContents, + }); + + final LoginWithBasicAuthScenario loginWithBasicAuthScenario; + final String keyword; + final List longEmailContents; + + @override + Future execute() async { + // Robots + final threadRobot = ThreadRobot($); + final searchRobot = SearchRobot($); + + // Login + await loginWithBasicAuthScenario.execute(); + + // Prepare attachment file + final file = await preparingTxtFile(keyword); + + // Provisioning some emails + await provisionEmail(longEmailContents + .map( + (emailContent) => ProvisioningEmail( + toEmail: loginWithBasicAuthScenario.email, + subject: keyword, + content: emailContent, + attachmentPaths: emailContent == longEmailContents.first + ? [file.path] + : [], + ), + ) + .toList()); + await $.pumpAndSettle(); + + // Search + await threadRobot.tapOnSearchField(); + await searchRobot.enterKeyword(keyword); + await searchRobot.tapOnShowAllResultsText(); + await $.pump(const Duration(seconds: 5)); + expect($(RichTextBuilder).$(keyword.split(' ').first).hitTestable().evaluate().length, 12); + } +} \ No newline at end of file diff --git a/integration_test/scenarios/search_suggestion_highlights_scenario.dart b/integration_test/scenarios/search_suggestion_highlights_scenario.dart new file mode 100644 index 000000000..13459b98f --- /dev/null +++ b/integration_test/scenarios/search_suggestion_highlights_scenario.dart @@ -0,0 +1,56 @@ +import 'package:core/core.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../base/base_scenario.dart'; +import '../models/provisioning_email.dart'; +import '../robots/search_robot.dart'; +import '../robots/thread_robot.dart'; +import '../utils/scenario_utils_mixin.dart'; +import 'login_with_basic_auth_scenario.dart'; + +class SearchSuggestionHighlightsScenario extends BaseScenario with ScenarioUtilsMixin { + SearchSuggestionHighlightsScenario( + super.$, { + required this.loginWithBasicAuthScenario, + required this.keyword, + required this.longEmailContents, + }); + + final LoginWithBasicAuthScenario loginWithBasicAuthScenario; + final String keyword; + final List longEmailContents; + + @override + Future execute() async { + // Robots + final threadRobot = ThreadRobot($); + final searchRobot = SearchRobot($); + + // Login + await loginWithBasicAuthScenario.execute(); + + // Prepare attachment file + final file = await preparingTxtFile(keyword); + + // Provisioning some emails + await provisionEmail(longEmailContents + .map( + (emailContent) => ProvisioningEmail( + toEmail: loginWithBasicAuthScenario.email, + subject: keyword, + content: emailContent, + attachmentPaths: emailContent == longEmailContents.first + ? [file.path] + : [], + ), + ) + .toList()); + await $.pumpAndSettle(); + + // Search + await threadRobot.tapOnSearchField(); + await searchRobot.enterKeyword(keyword); + await $.pump(const Duration(seconds: 5)); + expect($(RichTextBuilder).$(keyword.split(' ').first).hitTestable().evaluate().length, 10); + } +} \ No newline at end of file diff --git a/integration_test/tests/search/search_result_highlights_test.dart b/integration_test/tests/search/search_result_highlights_test.dart new file mode 100644 index 000000000..19ca947a2 --- /dev/null +++ b/integration_test/tests/search/search_result_highlights_test.dart @@ -0,0 +1,36 @@ +import '../../base/test_base.dart'; +import '../../scenarios/login_with_basic_auth_scenario.dart'; +import '../../scenarios/search_result_highlights_scenario.dart'; + +void main() { + TestBase().runPatrolTest( + description: 'Should see highlighted keyword in search result', + test: ($) async { + const keyword = 'Search snippet results'; + + const longContentWithSearchKeywordAtTheStart = "$keyword Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s"; + const longContentWithSearchKeywordAtTheMiddle = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. $keyword Lorem Ipsum has been the industry's standard dummy text ever since the 1500s"; + const longContentWithSearchKeywordAtTheEnd = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s $keyword"; + + final loginWithBasicAuthScenario = LoginWithBasicAuthScenario($, + username: const String.fromEnvironment('USERNAME'), + hostUrl: const String.fromEnvironment('BASIC_AUTH_URL'), + email: const String.fromEnvironment('BASIC_AUTH_EMAIL'), + password: const String.fromEnvironment('PASSWORD'), + ); + + final searchResultHighlightsScenario = SearchResultHighlightsScenario( + $, + loginWithBasicAuthScenario: loginWithBasicAuthScenario, + keyword: keyword, + longEmailContents: [ + longContentWithSearchKeywordAtTheStart, + longContentWithSearchKeywordAtTheMiddle, + longContentWithSearchKeywordAtTheEnd + ], + ); + + await searchResultHighlightsScenario.execute(); + } + ); +} \ No newline at end of file diff --git a/integration_test/tests/search/search_suggestion_highlights_test.dart b/integration_test/tests/search/search_suggestion_highlights_test.dart new file mode 100644 index 000000000..22f4c8d43 --- /dev/null +++ b/integration_test/tests/search/search_suggestion_highlights_test.dart @@ -0,0 +1,36 @@ +import '../../base/test_base.dart'; +import '../../scenarios/login_with_basic_auth_scenario.dart'; +import '../../scenarios/search_suggestion_highlights_scenario.dart'; + +void main() { + TestBase().runPatrolTest( + description: 'Should see highlighted keyword in search suggestion', + test: ($) async { + const keyword = 'Search snippet suggestions'; + + const longContentWithSearchKeywordAtTheStart = "$keyword Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s"; + const longContentWithSearchKeywordAtTheMiddle = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. $keyword Lorem Ipsum has been the industry's standard dummy text ever since the 1500s"; + const longContentWithSearchKeywordAtTheEnd = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s $keyword"; + + final loginWithBasicAuthScenario = LoginWithBasicAuthScenario($, + username: const String.fromEnvironment('USERNAME'), + hostUrl: const String.fromEnvironment('BASIC_AUTH_URL'), + email: const String.fromEnvironment('BASIC_AUTH_EMAIL'), + password: const String.fromEnvironment('PASSWORD'), + ); + + final searchResultHighlightsScenario = SearchSuggestionHighlightsScenario( + $, + loginWithBasicAuthScenario: loginWithBasicAuthScenario, + keyword: keyword, + longEmailContents: [ + longContentWithSearchKeywordAtTheStart, + longContentWithSearchKeywordAtTheMiddle, + longContentWithSearchKeywordAtTheEnd + ], + ); + + await searchResultHighlightsScenario.execute(); + } + ); +} \ No newline at end of file diff --git a/integration_test/utils/scenario_utils_mixin.dart b/integration_test/utils/scenario_utils_mixin.dart new file mode 100644 index 000000000..76e5b35d9 --- /dev/null +++ b/integration_test/utils/scenario_utils_mixin.dart @@ -0,0 +1,141 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:get/get.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:model/email/email_action_type.dart'; +import 'package:model/extensions/session_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:model/upload/file_info.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.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/upload_attachment_interactor.dart'; +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/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/thread/presentation/thread_controller.dart'; +import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; + +import '../models/provisioning_email.dart'; + +mixin ScenarioUtilsMixin { + Future provisionEmail(List provisioningEmails) async { + ComposerBindings().dependencies(); + + final mailboxDashBoardController = Get.find(); + final createNewAndSendEmailInteractor = Get.find(); + final threadController = Get.find(); + + final identity = await getIdentity(); + + if (identity == null) return; + + // Provision emails + await Future.wait(provisioningEmails.map((provisioningEmail) async { + final attachments = await uploadAttachments(provisioningEmail.attachmentPaths); + + // Create and send email + return await createNewAndSendEmailInteractor.execute( + createEmailRequest: CreateEmailRequest( + session: mailboxDashBoardController.sessionCurrent!, + accountId: mailboxDashBoardController.accountId.value!, + emailActionType: EmailActionType.compose, + subject: provisioningEmail.subject, + emailContent: provisioningEmail.content, + fromSender: {}, + toRecipients: {EmailAddress(null, provisioningEmail.toEmail)}, + ccRecipients: {}, + bccRecipients: {}, + outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId, + sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], + identity: identity, + attachments: attachments, + ), + ).last; + })); + + // Refresh view after provisioning emails + threadController.refreshAllEmail(); + + ComposerBindings().dispose(); + } + + Future preparingTxtFile(String content) async { + final directory = await getTemporaryDirectory(); + final file = File('${directory.path}/test.txt'); + return file.writeAsString(content); + } + + Future> uploadAttachments( + List attachmentPaths + ) async { + final mailboxDashBoardController = Get.find(); + + final attachments = []; + for (final path in attachmentPaths) { + final file = File(path); + final fileName = path.split('/').last; + + final fileInfo = FileInfo( + fileName: fileName, + filePath: path, + fileSize: await file.length(), + ); + final uploadUri = mailboxDashBoardController.sessionCurrent!.getUploadUri( + mailboxDashBoardController.accountId.value!, + jmapUrl: mailboxDashBoardController.dynamicUrlInterceptors.jmapUrl, + ); + + final uploadAttachmentInteractor = Get.find(); + final uploadAttachmentState = await uploadAttachmentInteractor + .execute(fileInfo, uploadUri) + .last; + final attachment = await uploadAttachmentState.fold( + (failure) => null, + (success) async { + if (success is UploadAttachmentSuccess) { + final uploadAttachment = await success.uploadAttachment.progressState.last; + return uploadAttachment.fold( + (failure) => null, + (success) { + if (success is! SuccessAttachmentUploadState) return null; + + return success.attachment; + }, + ); + } + }, + ); + if (attachment != null) { + attachments.add(attachment); + } + } + + return attachments; + } + + Future getIdentity() async { + final mailboxDashBoardController = Get.find(); + final getAllIdentitiesInteractor = Get.find(); + final getAllIdentities = await getAllIdentitiesInteractor.execute( + mailboxDashBoardController.sessionCurrent!, + mailboxDashBoardController.accountId.value!, + ).last; + return getAllIdentities.fold( + (failure) => null, + (success) { + if (success is GetAllIdentitiesSuccess) { + return success.identities?.firstOrNull; + } + return null; + }, + ); + } +} \ No newline at end of file