diff --git a/.github/workflows/patrol-integration-test.yaml b/.github/workflows/patrol-integration-test.yaml index b7d59dbcc..e9a89f744 100644 --- a/.github/workflows/patrol-integration-test.yaml +++ b/.github/workflows/patrol-integration-test.yaml @@ -7,7 +7,7 @@ on: env: JAVA_VERSION: 17 - FLUTTER_VERSION: 3.27.4 + FLUTTER_VERSION: 3.32.8 jobs: mobile_integration_test: diff --git a/core/lib/presentation/views/image/image_loader_mixin.dart b/core/lib/presentation/views/image/image_loader_mixin.dart index 7c8656b61..3d8e760b3 100644 --- a/core/lib/presentation/views/image/image_loader_mixin.dart +++ b/core/lib/presentation/views/image/image_loader_mixin.dart @@ -1,5 +1,6 @@ import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -10,6 +11,10 @@ mixin ImageLoaderMixin { required String imagePath, double? imageSize }) { + log('$runtimeType::buildImage: imagePath = $imagePath'); + if (PlatformInfo.isIntegrationTesting) { + return buildNoImage(imageSize ?? 150); + } if (isImageNetworkLink(imagePath) && isImageSVG(imagePath)) { return SvgPicture.network( imagePath, @@ -19,6 +24,9 @@ mixin ImageLoaderMixin { placeholderBuilder: (_) { return const CupertinoActivityIndicator(); }, + errorBuilder: (_, __, ___) { + return buildNoImage(imageSize ?? 150); + }, ); } else if (isImageNetworkLink(imagePath)) { return Image.network( @@ -27,16 +35,10 @@ mixin ImageLoaderMixin { width: imageSize ?? 150, height: imageSize ?? 150, loadingBuilder: (_, child, loadingProgress) { - if (loadingProgress != null && - loadingProgress.cumulativeBytesLoaded != loadingProgress.expectedTotalBytes) { - return const Center( - child: CupertinoActivityIndicator(), - ); - } - return child; + if (loadingProgress == null) return child; + return const Center(child: CupertinoActivityIndicator()); }, - errorBuilder: (context, error, stackTrace) { - logError('ImageLoaderMixin::buildImage:Exception = $error'); + errorBuilder: (_, __, ___) { return buildNoImage(imageSize ?? 150); }, ); diff --git a/integration_test/mixin/scenario_utils_mixin.dart b/integration_test/mixin/scenario_utils_mixin.dart index 06c2b69b6..bc1c4ea2b 100644 --- a/integration_test/mixin/scenario_utils_mixin.dart +++ b/integration_test/mixin/scenario_utils_mixin.dart @@ -4,7 +4,7 @@ import 'dart:io' hide HttpClient; import 'package:collection/collection.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/http/http_client.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; @@ -282,7 +282,7 @@ mixin ScenarioUtilsMixin { IdentityInteractorsBindings().dispose(); } - Future hideKeyboard() async { - await SystemChannels.textInput.invokeMethod('TextInput.hide'); + void hideKeyboard() { + FocusManager.instance.primaryFocus?.unfocus(); } } diff --git a/integration_test/robots/composer_robot.dart b/integration_test/robots/composer_robot.dart index db02d3a22..7e72a3c60 100644 --- a/integration_test/robots/composer_robot.dart +++ b/integration_test/robots/composer_robot.dart @@ -14,6 +14,7 @@ import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_b 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/view/mobile/mobile_editor_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/list_recipients_collapsed_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_mobile_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; @@ -29,6 +30,10 @@ import '../extensions/patrol_finder_extension.dart'; class ComposerRobot extends CoreRobot { ComposerRobot(super.$); + Future expandRecipientsFields() async { + await $(RecipientsCollapsedComposerWidget).$(GestureDetector).tap(); + } + Future addRecipientIntoField({ required PrefixEmailAddress prefixEmailAddress, required String email, diff --git a/integration_test/robots/email_robot.dart b/integration_test/robots/email_robot.dart index 92f870235..63811fdfb 100644 --- a/integration_test/robots/email_robot.dart +++ b/integration_test/robots/email_robot.dart @@ -16,7 +16,7 @@ class EmailRobot extends CoreRobot { } Future tapDownloadAllButton() async { - await $(AppLocalizations().archiveAndDownload).tap(); + await $(#download_all_attachments_button).tap(); await $.pumpAndSettle(); } diff --git a/integration_test/scenarios/app_grid_scenario.dart b/integration_test/scenarios/app_grid_scenario.dart index 9f975649e..cfa72886f 100644 --- a/integration_test/scenarios/app_grid_scenario.dart +++ b/integration_test/scenarios/app_grid_scenario.dart @@ -16,6 +16,7 @@ class AppGridScenario extends BaseTestScenario { @override Future runTestLogic() async { + PlatformInfo.isIntegrationTesting = true; final threadRobot = ThreadRobot($); final mailboxMenuRobot = MailboxMenuRobot($); final appGridRobot = AppGridRobot($); @@ -52,6 +53,7 @@ class AppGridScenario extends BaseTestScenario { } else if (PlatformInfo.isIOS) { await _expectMailboxViewInVisible(); } + PlatformInfo.isIntegrationTesting = false; } Future _expectMailboxViewVisible() => expectViewVisible($(MailboxView)); diff --git a/integration_test/scenarios/composer/upload_attachment_and_inline_image_scenario.dart b/integration_test/scenarios/composer/upload_attachment_and_inline_image_scenario.dart index a3be37349..fd10387ad 100644 --- a/integration_test/scenarios/composer/upload_attachment_and_inline_image_scenario.dart +++ b/integration_test/scenarios/composer/upload_attachment_and_inline_image_scenario.dart @@ -31,7 +31,7 @@ class ComposerUploadAttachmentAndInlineImageScenario extends BaseTestScenario { await _expectAttachment(pngName); await composerRobot.addInline(png); - await $.pumpAndSettle(); + await $.pumpAndSettle(duration: const Duration(seconds: 1)); await _expectInline(); await composerRobot.addAttachment(png); diff --git a/integration_test/scenarios/email/reply_to_own_sent_email_scenario.dart b/integration_test/scenarios/email/reply_to_own_sent_email_scenario.dart index fcae1e0a5..dbb87917f 100644 --- a/integration_test/scenarios/email/reply_to_own_sent_email_scenario.dart +++ b/integration_test/scenarios/email/reply_to_own_sent_email_scenario.dart @@ -1,9 +1,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:model/email/prefix_email_address.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import '../../base/base_test_scenario.dart'; +import '../../robots/composer_robot.dart'; import '../../robots/email_robot.dart'; import '../../robots/mailbox_menu_robot.dart'; import '../../robots/thread_robot.dart'; @@ -19,6 +21,7 @@ class ReplyToOwnSentEmailScenario extends BaseTestScenario { final threadRobot = ThreadRobot($); final mailboxMenuRobot = MailboxMenuRobot($); final emailRobot = EmailRobot($); + final composerRobot = ComposerRobot($); final sendEmailScenario = SendEmailScenario($, customSubject: subject); final appLocalizations = AppLocalizations(); @@ -29,9 +32,20 @@ class ReplyToOwnSentEmailScenario extends BaseTestScenario { ); await threadRobot.openEmailWithSubject(subject); await emailRobot.onTapReplyEmail(); + await _expectComposerViewVisible(); + + await composerRobot.grantContactPermission(); + + await composerRobot.expandRecipientsFields(); + await $.pumpAndSettle(); + _expectToFieldContainListEmailAddressCorrectly(); } + Future _expectComposerViewVisible() async { + await expectViewVisible($(ComposerView)); + } + void _expectToFieldContainListEmailAddressCorrectly() { expect( $(RecipientComposerWidget).which((widget) => diff --git a/integration_test/scenarios/email_detailed/compose_email_from_email_address_scenario.dart b/integration_test/scenarios/email_detailed/compose_email_from_email_address_scenario.dart index 055950732..76fd93bfb 100644 --- a/integration_test/scenarios/email_detailed/compose_email_from_email_address_scenario.dart +++ b/integration_test/scenarios/email_detailed/compose_email_from_email_address_scenario.dart @@ -49,6 +49,9 @@ class ComposeEmailFromEmailAddressScenario extends BaseTestScenario { await composerRobot.grantContactPermission(); + await composerRobot.expandRecipientsFields(); + await $.pumpAndSettle(); + await _expectToFieldContainListEmailAddressCorrectly(emailUser); } diff --git a/integration_test/scenarios/email_detailed/deformed_inlined_image_scenario.dart b/integration_test/scenarios/email_detailed/deformed_inlined_image_scenario.dart index 2e25e7400..d5e3761ec 100644 --- a/integration_test/scenarios/email_detailed/deformed_inlined_image_scenario.dart +++ b/integration_test/scenarios/email_detailed/deformed_inlined_image_scenario.dart @@ -1,9 +1,8 @@ import 'package:core/presentation/views/html_viewer/html_content_viewer_widget.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; -import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart'; import '../../base/base_test_scenario.dart'; @@ -121,10 +120,12 @@ class DeformedInlinedImageScenario extends BaseTestScenario { } Future _expectEmailViewDisplaysNormalizedInlineImages() async { + GlobalKey? htmlContentViewKey; HtmlContentViewer? htmlContentViewer; await $(HtmlContentViewer) .which((view) { htmlContentViewer = view; + htmlContentViewKey = view.key as GlobalKey; return true; }) .first @@ -133,17 +134,6 @@ class DeformedInlinedImageScenario extends BaseTestScenario { log('DeformedInlinedImageScenario::_expectEmailViewDisplaysNormalizedInlineImages:initialWidth = ${htmlContentViewer?.initialWidth}'); expect(htmlContentViewer?.contentHtml.isNotEmpty, isTrue); - SingleEmailController? emailController; - await $(EmailView) - .which((view) { - emailController = view.controller; - return true; - }) - .first - .tap(); - expect(emailController, isNotNull); - - final htmlContentViewKey = emailController?.htmlContentViewKey; expect(htmlContentViewKey, isNotNull); final result = await htmlContentViewKey!.currentState!.webViewController.evaluateJavascript( diff --git a/integration_test/scenarios/email_detailed/forwarding_email_lost_attachments_scenario.dart b/integration_test/scenarios/email_detailed/forwarding_email_lost_attachments_scenario.dart index 2b107f58a..01b500398 100644 --- a/integration_test/scenarios/email_detailed/forwarding_email_lost_attachments_scenario.dart +++ b/integration_test/scenarios/email_detailed/forwarding_email_lost_attachments_scenario.dart @@ -55,6 +55,10 @@ class ForwardingEmailLostAttachmentsScenario extends BaseTestScenario { await _expectComposerViewVisible(); await composerRobot.grantContactPermission(); + + await composerRobot.expandRecipientsFields(); + await $.pumpAndSettle(); + await composerRobot.addRecipientIntoField( prefixEmailAddress: PrefixEmailAddress.to, email: emailUser, diff --git a/integration_test/scenarios/email_detailed/reply_all_email_scenario.dart b/integration_test/scenarios/email_detailed/reply_all_email_scenario.dart index 109481ac9..e2372a203 100644 --- a/integration_test/scenarios/email_detailed/reply_all_email_scenario.dart +++ b/integration_test/scenarios/email_detailed/reply_all_email_scenario.dart @@ -44,6 +44,9 @@ class ReplyAllEmailScenario extends BaseTestScenario { await composerRobot.grantContactPermission(); + await composerRobot.expandRecipientsFields(); + await $.pumpAndTrySettle(); + await _expectComposerSubjectDisplayedCorrectly(appLocalizations); await _expectToFieldContainListEmailAddressCorrectly(); await _expectCcFieldContainListEmailAddressCorrectly(); diff --git a/integration_test/scenarios/email_detailed/reply_email_with_reply_to_scenario.dart b/integration_test/scenarios/email_detailed/reply_email_with_reply_to_scenario.dart index a434b093f..feaf60b8c 100644 --- a/integration_test/scenarios/email_detailed/reply_email_with_reply_to_scenario.dart +++ b/integration_test/scenarios/email_detailed/reply_email_with_reply_to_scenario.dart @@ -44,6 +44,9 @@ class ReplyEmailWithReplyToScenario extends BaseTestScenario { await composerRobot.grantContactPermission(); + await composerRobot.expandRecipientsFields(); + await $.pumpAndTrySettle(); + await _expectComposerSubjectDisplayedCorrectly(appLocalizations); await _expectToFieldContainReplyToEmailAddress(); } diff --git a/integration_test/scenarios/email_detailed/reply_email_without_reply_to_scenario.dart b/integration_test/scenarios/email_detailed/reply_email_without_reply_to_scenario.dart index e2cf59174..ec504c191 100644 --- a/integration_test/scenarios/email_detailed/reply_email_without_reply_to_scenario.dart +++ b/integration_test/scenarios/email_detailed/reply_email_without_reply_to_scenario.dart @@ -44,6 +44,9 @@ class ReplyEmailWithoutReplyToScenario extends BaseTestScenario { await composerRobot.grantContactPermission(); + await composerRobot.expandRecipientsFields(); + await $.pumpAndTrySettle(); + await _expectComposerSubjectDisplayedCorrectly(appLocalizations); await _expectToFieldContainFromEmailAddress(); } diff --git a/integration_test/scenarios/email_detailed/reply_to_list_email_scenario.dart b/integration_test/scenarios/email_detailed/reply_to_list_email_scenario.dart index 98ea2c8e1..6a705a27c 100644 --- a/integration_test/scenarios/email_detailed/reply_to_list_email_scenario.dart +++ b/integration_test/scenarios/email_detailed/reply_to_list_email_scenario.dart @@ -44,6 +44,9 @@ class ReplyToListEmailScenario extends BaseTestScenario { await composerRobot.grantContactPermission(); + await composerRobot.expandRecipientsFields(); + await $.pumpAndTrySettle(); + await _expectComposerSubjectDisplayedCorrectly(appLocalizations); await _expectToFieldContainListPostEmailAddress(); } diff --git a/integration_test/scenarios/mailbox/long_press_empty_and_recover_spam_scenario.dart b/integration_test/scenarios/mailbox/long_press_empty_and_recover_spam_scenario.dart index 24c5e731f..c807adc3a 100644 --- a/integration_test/scenarios/mailbox/long_press_empty_and_recover_spam_scenario.dart +++ b/integration_test/scenarios/mailbox/long_press_empty_and_recover_spam_scenario.dart @@ -70,6 +70,7 @@ class LongPressEmptyAndRecoverSpamScenario extends BaseTestScenario { } Future _expectEmailWithSubjectVisible(String subject) async { + await $.scrollUntilVisible(finder: $(subject)); await expectViewVisible($(subject)); } diff --git a/integration_test/scenarios/mailbox/long_press_empty_and_recover_trash_scenario.dart b/integration_test/scenarios/mailbox/long_press_empty_and_recover_trash_scenario.dart index 86f786c0e..41d2b82db 100644 --- a/integration_test/scenarios/mailbox/long_press_empty_and_recover_trash_scenario.dart +++ b/integration_test/scenarios/mailbox/long_press_empty_and_recover_trash_scenario.dart @@ -68,6 +68,7 @@ class LongPressEmptyAndRecoverTrashScenario extends BaseTestScenario { } Future _expectEmailWithSubjectVisible(String subject) async { + await $.scrollUntilVisible(finder: $(subject)); await expectViewVisible($(subject)); } diff --git a/integration_test/scenarios/mailbox/pull_to_refresh_scenario.dart b/integration_test/scenarios/mailbox/pull_to_refresh_scenario.dart index 0a9fd8a55..ed7c8d519 100644 --- a/integration_test/scenarios/mailbox/pull_to_refresh_scenario.dart +++ b/integration_test/scenarios/mailbox/pull_to_refresh_scenario.dart @@ -37,11 +37,11 @@ class PullToRefreshScenario extends BaseTestScenario { _expectEmailWithSubjectInvisible(visibleAfterPullToRefresh); await threadRobot.pullToRefreshByEmailSubject(visibleBeforePullToRefresh); - _expectEmailWithSubjectVisible(visibleAfterPullToRefresh); + await _expectEmailWithSubjectVisible(visibleAfterPullToRefresh); } - _expectEmailWithSubjectVisible(String subject) { - expect($(subject), findsOneWidget); + Future _expectEmailWithSubjectVisible(String subject) async { + await expectViewVisible($(subject)); } _expectEmailWithSubjectInvisible(String subject) { diff --git a/integration_test/scenarios/reply_email_with_content_contain_image_base64_data_scenario.dart b/integration_test/scenarios/reply_email_with_content_contain_image_base64_data_scenario.dart index b0fc701a2..74a488e47 100644 --- a/integration_test/scenarios/reply_email_with_content_contain_image_base64_data_scenario.dart +++ b/integration_test/scenarios/reply_email_with_content_contain_image_base64_data_scenario.dart @@ -46,6 +46,9 @@ class ReplyEmailWithContentContainImageBase64DataScenario extends BaseTestScenar await composerRobot.grantContactPermission(); + await composerRobot.expandRecipientsFields(); + await $.pumpAndSettle(); + await composerRobot.addRecipientIntoField( prefixEmailAddress: PrefixEmailAddress.to, email: emailUser, diff --git a/integration_test/scenarios/save_as_template_scenario.dart b/integration_test/scenarios/save_as_template_scenario.dart index 95586bd81..368573f8b 100644 --- a/integration_test/scenarios/save_as_template_scenario.dart +++ b/integration_test/scenarios/save_as_template_scenario.dart @@ -17,11 +17,12 @@ class SaveAsTemplateScenario extends BaseTestScenario { await composerRobot.grantContactPermission(); await composerRobot.saveAsTemplate(); - + await $.pumpAndTrySettle(); await _expectSaveToastSuccessVisible(); await composerRobot.addSubject('test subject'); - await hideKeyboard(); + hideKeyboard(); + await $.pumpAndTrySettle(duration: const Duration(seconds: 1)); await composerRobot.saveAsTemplate(); await _expectUpdateToastSuccessVisible(); diff --git a/integration_test/scenarios/search/persist_filter_when_change_search_input_text_scenario.dart b/integration_test/scenarios/search/persist_filter_when_change_search_input_text_scenario.dart index 59baed91f..6282ed54f 100644 --- a/integration_test/scenarios/search/persist_filter_when_change_search_input_text_scenario.dart +++ b/integration_test/scenarios/search/persist_filter_when_change_search_input_text_scenario.dart @@ -14,7 +14,7 @@ class PersistFilterWhenChangeSearchInputTextScenario Future runTestLogic() async { const email = String.fromEnvironment('BASIC_AUTH_EMAIL'); const subject = 'Persist search filter'; - const queryStringFirst = 'Persist'; + const queryStringFirst = 'Persist search filter'; const queryStringSecond = 'Persist search'; final threadRobot = ThreadRobot($); diff --git a/lib/features/email/presentation/widgets/attachments_info.dart b/lib/features/email/presentation/widgets/attachments_info.dart index b200a0334..dcfbf92f5 100644 --- a/lib/features/email/presentation/widgets/attachments_info.dart +++ b/lib/features/email/presentation/widgets/attachments_info.dart @@ -68,6 +68,7 @@ class AttachmentsInfo extends StatelessWidget { ), if (onTapDownloadAllButton != null) TMailButtonWidget( + key: const Key('download_all_attachments_button'), text: AppLocalizations.of(context).downloadAll, icon: !responsiveUtils.isMobile(context) ? imagePaths.icDownloadAttachment diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index e2d105bbf..495fc749b 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2025-10-07T12:02:25.245876", + "@@last_modified": "2025-10-14T15:18:48.964794", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -4592,12 +4592,6 @@ "placeholders_order": [], "placeholders": {} }, - "archiveAndDownload": "Archive and download", - "@archiveAndDownload": { - "type": "text", - "placeholders_order": [], - "placeholders": {} - }, "creatingAnArchiveForDownloading": "Creating an archive for downloading", "@creatingAnArchiveForDownloading": { "type": "text", diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 865c512ce..ae9b189b0 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4838,13 +4838,6 @@ class AppLocalizations { ); } - String get archiveAndDownload { - return Intl.message( - 'Archive and download', - name: 'archiveAndDownload', - ); - } - String get creatingAnArchiveForDownloading { return Intl.message( 'Creating an archive for downloading',