TF-3535 E2E composer attachment and inline

This commit is contained in:
DatDang
2025-03-07 15:45:03 +07:00
committed by Dat H. Pham
parent 86e98289be
commit 52f9da941b
5 changed files with 239 additions and 91 deletions
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io' hide HttpClient;
import 'package:collection/collection.dart';
@@ -33,6 +34,7 @@ import 'package:uuid/uuid.dart';
import '../models/provisioning_email.dart';
import '../models/provisioning_identity.dart';
import '../resources/test_images.dart';
mixin ScenarioUtilsMixin {
Future<void> provisionEmail(
@@ -44,7 +46,8 @@ mixin ScenarioUtilsMixin {
ComposerBindings().dependencies();
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final createNewAndSendEmailInteractor = Get.find<CreateNewAndSendEmailInteractor>();
final createNewAndSendEmailInteractor =
Get.find<CreateNewAndSendEmailInteractor>();
final threadController = Get.find<ThreadController>();
final identity = await getIdentity();
@@ -53,27 +56,33 @@ mixin ScenarioUtilsMixin {
// Provision emails
await Future.wait(provisioningEmails.map((provisioningEmail) async {
final attachments = await uploadAttachments(provisioningEmail.attachmentPaths);
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,
ownEmailAddress: mailboxDashBoardController.ownEmailAddress.value,
subject: provisioningEmail.subject,
emailContent: provisioningEmail.content,
toRecipients: {EmailAddress(null, provisioningEmail.toEmail)},
outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId,
sentMailboxId: folderLocationRole != null
? mailboxDashBoardController.mapDefaultMailboxIdByRole[folderLocationRole]
: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent],
identity: identity,
attachments: attachments,
hasRequestReadReceipt: requestReadReceipt,
),
).last;
return await createNewAndSendEmailInteractor
.execute(
createEmailRequest: CreateEmailRequest(
session: mailboxDashBoardController.sessionCurrent!,
accountId: mailboxDashBoardController.accountId.value!,
emailActionType: EmailActionType.compose,
ownEmailAddress: mailboxDashBoardController.ownEmailAddress.value,
subject: provisioningEmail.subject,
emailContent: provisioningEmail.content,
toRecipients: {EmailAddress(null, provisioningEmail.toEmail)},
outboxMailboxId:
mailboxDashBoardController.outboxMailbox?.mailboxId,
sentMailboxId: folderLocationRole != null
? mailboxDashBoardController
.mapDefaultMailboxIdByRole[folderLocationRole]
: mailboxDashBoardController
.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent],
identity: identity,
attachments: attachments,
hasRequestReadReceipt: requestReadReceipt,
),
)
.last;
}));
// Refresh view after provisioning emails
@@ -90,12 +99,10 @@ mixin ScenarioUtilsMixin {
bool? isStar,
}) async {
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final emails = mailboxDashBoardController
.emailsInCurrentMailbox
.where((presentationEmail) => subjects.contains(
presentationEmail.subject
))
.toList();
final emails = mailboxDashBoardController.emailsInCurrentMailbox
.where(
(presentationEmail) => subjects.contains(presentationEmail.subject))
.toList();
final session = mailboxDashBoardController.sessionCurrent;
final accountId = mailboxDashBoardController.accountId.value;
if (session == null || accountId == null) return;
@@ -110,26 +117,22 @@ mixin ScenarioUtilsMixin {
if (isRead != null) {
final markEmailAsReadMethod = SetEmailMethod(accountId)
..addUpdates(emails.listEmailIds.generateMapUpdateObjectMarkAsRead(
isRead ? ReadActions.markAsRead : ReadActions.markAsUnread
));
isRead ? ReadActions.markAsRead : ReadActions.markAsUnread));
requestBuilder.invocation(markEmailAsReadMethod);
capabilities.addAll(markEmailAsReadMethod
.requiredCapabilities
.toCapabilitiesSupportTeamMailboxes(session, accountId));
capabilities.addAll(markEmailAsReadMethod.requiredCapabilities
.toCapabilitiesSupportTeamMailboxes(session, accountId));
}
// Mark as star/unstar
if (isStar != null) {
final markEmailAsStarMethod = SetEmailMethod(accountId)
..addUpdates(emails.listEmailIds.generateMapUpdateObjectMarkAsStar(
isStar ? MarkStarAction.markStar : MarkStarAction.unMarkStar
));
isStar ? MarkStarAction.markStar : MarkStarAction.unMarkStar));
requestBuilder.invocation(markEmailAsStarMethod);
capabilities.addAll(markEmailAsStarMethod
.requiredCapabilities
.toCapabilitiesSupportTeamMailboxes(session, accountId));
capabilities.addAll(markEmailAsStarMethod.requiredCapabilities
.toCapabilitiesSupportTeamMailboxes(session, accountId));
}
await (requestBuilder..usings(capabilities)).build().execute();
}
@@ -139,9 +142,15 @@ mixin ScenarioUtilsMixin {
return file.writeAsString(content);
}
Future<File> preparingPngWithName(String pngName) async {
final bytes = base64Decode(TestImages.base64);
final directory = await getTemporaryDirectory();
final file = File('${directory.path}/$pngName.png');
return file.writeAsBytes(bytes);
}
Future<List<Attachment>> uploadAttachments(
List<String> attachmentPaths
) async {
List<String> attachmentPaths) async {
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final attachments = <Attachment>[];
@@ -156,20 +165,22 @@ mixin ScenarioUtilsMixin {
);
try {
final uploadUri = mailboxDashBoardController.sessionCurrent!.getUploadUri(
final uploadUri =
mailboxDashBoardController.sessionCurrent!.getUploadUri(
mailboxDashBoardController.accountId.value!,
jmapUrl: mailboxDashBoardController.dynamicUrlInterceptors.jmapUrl,
);
final uploadAttachmentInteractor = Get.find<UploadAttachmentInteractor>();
final uploadAttachmentState = await uploadAttachmentInteractor
.execute(fileInfo, uploadUri)
.last;
final uploadAttachmentInteractor =
Get.find<UploadAttachmentInteractor>();
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;
final uploadAttachment =
await success.uploadAttachment.progressState.last;
return uploadAttachment.fold(
(failure) => null,
(success) {
@@ -196,10 +207,12 @@ mixin ScenarioUtilsMixin {
Future<Identity?> getIdentity() async {
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final getAllIdentitiesInteractor = Get.find<GetAllIdentitiesInteractor>();
final getAllIdentities = await getAllIdentitiesInteractor.execute(
mailboxDashBoardController.sessionCurrent!,
mailboxDashBoardController.accountId.value!,
).last;
final getAllIdentities = await getAllIdentitiesInteractor
.execute(
mailboxDashBoardController.sessionCurrent!,
mailboxDashBoardController.accountId.value!,
)
.last;
return getAllIdentities.fold(
(failure) => null,
(success) {
@@ -211,45 +224,50 @@ mixin ScenarioUtilsMixin {
);
}
bool isMatchingEmailList(List<EmailAddress> emailList, Set<String> allowedEmails) {
bool isMatchingEmailList(
List<EmailAddress> emailList, Set<String> allowedEmails) {
if (emailList.length != allowedEmails.length) {
return false;
}
final Set<String> emails = emailList.map((e) => e.email).whereType<String>().toSet();
final Set<String> emails =
emailList.map((e) => e.email).whereType<String>().toSet();
return emails.length == allowedEmails.length && emails.containsAll(allowedEmails);
return emails.length == allowedEmails.length &&
emails.containsAll(allowedEmails);
}
Future<void> provisionIdentities(List<ProvisioningIdentity> provisioningIdentities) async {
Future<void> provisionIdentities(
List<ProvisioningIdentity> provisioningIdentities) async {
IdentityInteractorsBindings().dependencies();
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final createNewIdentityInteractor = Get.find<CreateNewIdentityInteractor>();
final createNewDefaultIdentityInteractor = Get.find<CreateNewDefaultIdentityInteractor>();
final createNewDefaultIdentityInteractor =
Get.find<CreateNewDefaultIdentityInteractor>();
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,
));
.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;
.execute(session!, accountId!, identityRequest)
.last;
} else {
await createNewIdentityInteractor
.execute(session!, accountId!, identityRequest)
.last;
.execute(session!, accountId!, identityRequest)
.last;
}
}
IdentityInteractorsBindings().dispose();
}
}
}
File diff suppressed because one or more lines are too long
+70 -26
View File
@@ -1,8 +1,13 @@
import 'dart:io';
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:get/get.dart';
import 'package:model/email/prefix_email_address.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:model/upload/file_info.dart';
import 'package:rich_text_composer/rich_text_composer.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart';
@@ -12,6 +17,7 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from
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';
import 'package:tmail_ui_user/features/upload/domain/state/local_image_picker_state.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import '../base/core_robot.dart';
@@ -24,26 +30,29 @@ class ComposerRobot extends CoreRobot {
required PrefixEmailAddress prefixEmailAddress,
required String email,
}) async {
final finder = $(RecipientComposerWidget)
.which<RecipientComposerWidget>((widget) => widget.prefix == prefixEmailAddress);
final finder = $(RecipientComposerWidget).which<RecipientComposerWidget>(
(widget) => widget.prefix == prefixEmailAddress);
final isTextFieldFocused = finder
.which<RecipientComposerWidget>((view) => view.focusNode?.hasFocus ?? false)
.exists;
.which<RecipientComposerWidget>(
(view) => view.focusNode?.hasFocus ?? false)
.exists;
if (!isTextFieldFocused) {
await finder.tap();
}
await finder.enterTextWithoutTapAction(email);
await $(RecipientSuggestionItemWidget)
.which<RecipientSuggestionItemWidget>((widget) => widget.emailAddress.email?.contains(email) ?? false)
.tap();
.which<RecipientSuggestionItemWidget>(
(widget) => widget.emailAddress.email?.contains(email) ?? false)
.tap();
}
Future<void> addSubject(String subject) async {
final finder = $(SubjectComposerWidget);
final isTextFieldFocused = finder
.which<SubjectComposerWidget>((view) => view.focusNode?.hasFocus ?? false)
.exists;
.which<SubjectComposerWidget>(
(view) => view.focusNode?.hasFocus ?? false)
.exists;
if (!isTextFieldFocused) {
await finder.tap();
}
@@ -53,38 +62,45 @@ class ComposerRobot extends CoreRobot {
Future<void> addContent(String content) async {
ComposerController? composerController;
await $(ComposerView)
.which<ComposerView>((widget) {
composerController = widget.controller;
return true;
})
.$(MobileEditorView).$(HtmlEditor).$(InAppWebView).tap();
.which<ComposerView>((widget) {
composerController = widget.controller;
return true;
})
.$(MobileEditorView)
.$(HtmlEditor)
.$(InAppWebView)
.tap();
await composerController?.htmlEditorApi?.requestFocusLastChild();
await composerController!.htmlEditorApi!.insertHtml('$content <br><br>');
await composerController!.htmlEditorApi!.insertHtml('$content <br><br>');
}
Future<void> sendEmail(ImagePaths imagePaths) async {
await $(AppBarComposerWidget)
.$(TMailButtonWidget)
.which<TMailButtonWidget>((widget) => widget.icon == imagePaths.icSendMobile)
.tap();
.$(TMailButtonWidget)
.which<TMailButtonWidget>(
(widget) => widget.icon == imagePaths.icSendMobile)
.tap();
}
Future<void> grantContactPermission() async {
if (await $.native.isPermissionDialogVisible(timeout: const Duration(seconds: 5))) {
if (await $.native
.isPermissionDialogVisible(timeout: const Duration(seconds: 5))) {
await $.native.grantPermissionWhenInUse();
}
}
Future<void> tapCloseComposer(ImagePaths imagePaths) async {
await $(AppBarComposerWidget)
.$(TMailButtonWidget)
.which<TMailButtonWidget>((widget) => widget.icon == imagePaths.icCancel)
.tap();
.$(TMailButtonWidget)
.which<TMailButtonWidget>(
(widget) => widget.icon == imagePaths.icCancel)
.tap();
}
Future<void> tapSaveButtonOnSaveDraftConfirmDialog(AppLocalizations appLocalizations) async {
Future<void> tapSaveButtonOnSaveDraftConfirmDialog(
AppLocalizations appLocalizations) async {
await $(find.text(appLocalizations.save)).tap();
}
@@ -114,9 +130,37 @@ class ComposerRobot extends CoreRobot {
Future<void> saveAsTemplate() async {
await $(AppBarComposerWidget)
.$(TMailButtonWidget)
.which<TMailButtonWidget>((widget) => widget.icon == ImagePaths().icMore)
.tap();
.$(TMailButtonWidget)
.which<TMailButtonWidget>(
(widget) => widget.icon == ImagePaths().icMore)
.tap();
await $(AppLocalizations().saveAsTemplate).tap();
}
}
Future<void> addAttachment(File file) async {
final controller = Get.find<ComposerController>();
controller.uploadController.justUploadAttachmentsAction(
uploadFiles: [
FileInfo(
fileName: file.path.split('/').last,
fileSize: await file.length(),
filePath: file.path,
)
],
uploadUri:
controller.mailboxDashBoardController.sessionCurrent!.getUploadUri(
controller.mailboxDashBoardController.accountId.value!,
jmapUrl: controller.dynamicUrlInterceptors.jmapUrl,
),
);
}
Future<void> addInline(File file) async {
final controller = Get.find<ComposerController>();
controller.handleSuccessViewState(LocalImagePickerSuccess(FileInfo(
filePath: file.path,
fileSize: await file.length(),
fileName: file.path.split('/').last,
)));
}
}
@@ -0,0 +1,71 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_item_composer_widget.dart';
import '../../base/base_test_scenario.dart';
import '../../resources/test_images.dart';
import '../../robots/composer_robot.dart';
import '../../robots/thread_robot.dart';
class ComposerUploadAttachmentAndInlineImageScenario extends BaseTestScenario {
const ComposerUploadAttachmentAndInlineImageScenario(super.$);
@override
Future<void> runTestLogic() async {
const pngName = 'composer-png';
final threadRobot = ThreadRobot($);
final composerRobot = ComposerRobot($);
await threadRobot.openComposer();
await _expectComposerViewVisible();
await composerRobot.grantContactPermission();
final png = await preparingPngWithName(pngName);
await composerRobot.addAttachment(png);
await composerRobot.addInline(png);
await $.pumpAndSettle();
_expectAttachment(pngName);
await _expectInline();
await composerRobot.addAttachment(png);
await composerRobot.addAttachment(png);
await $.pumpAndSettle();
_expectMultipleAttachments(3);
}
Future<void> _expectInline() async {
final currentHtmlContent = await Get
.find<ComposerController>()
.getContentInEditor();
expect(
currentHtmlContent.contains(TestImages.base64),
true,
);
}
void _expectAttachment(String pngName) {
return expect(
$(AttachmentItemComposerWidget)
.which<AttachmentItemComposerWidget>(
(widget) => widget.fileName.contains(pngName)
),
findsOneWidget,
);
}
void _expectMultipleAttachments(int numberOfAttachments) {
return expect(
$(AttachmentItemComposerWidget),
findsNWidgets(numberOfAttachments),
);
}
Future<void> _expectComposerViewVisible() => expectViewVisible($(ComposerView));
}
@@ -0,0 +1,10 @@
import '../../base/test_base.dart';
import '../../scenarios/composer/upload_attachment_and_inline_image_scenario.dart';
void main() {
TestBase().runPatrolTest(
description: 'Should see attachment and inline image '
'when upload success in composer',
scenarioBuilder: ($) => ComposerUploadAttachmentAndInlineImageScenario($),
);
}