diff --git a/core/lib/presentation/state/failure.dart b/core/lib/presentation/state/failure.dart index 9ea7d6a707..e1f1306191 100644 --- a/core/lib/presentation/state/failure.dart +++ b/core/lib/presentation/state/failure.dart @@ -1,3 +1,5 @@ +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; abstract class Failure with EquatableMixin { @@ -8,9 +10,10 @@ abstract class Failure with EquatableMixin { abstract class FeatureFailure extends Failure { final dynamic exception; + final Stream>? onRetry; - FeatureFailure({this.exception}); + FeatureFailure({this.exception, this.onRetry}); @override - List get props => [exception]; + List get props => [exception, onRetry]; } diff --git a/core/lib/presentation/utils/app_toast.dart b/core/lib/presentation/utils/app_toast.dart index b1bbf0aa5b..76e85aaabc 100644 --- a/core/lib/presentation/utils/app_toast.dart +++ b/core/lib/presentation/utils/app_toast.dart @@ -184,4 +184,122 @@ class AppToast { : (duration ?? const Duration(seconds: 3)), ); } + + void showToastMessageWithMultipleActions( + BuildContext context, + String message, + { + List<({ + String? actionName, + Function? onActionClick, + Widget? actionIcon + })> actions = const [], + Widget? leadingIcon, + String? leadingSVGIcon, + Color? leadingSVGIconColor, + double? maxWidth, + bool infinityToast = false, + Color? backgroundColor, + Color? textColor, + Color? textActionColor, + TextStyle? textStyle, + EdgeInsets? padding, + TextAlign? textAlign, + Duration? duration, + } + ) { + final responsiveUtils = Get.find(); + List trailingWidgets = []; + for (var action in actions) { + if (action.actionName == null) continue; + + if (action.actionIcon == null) { + trailingWidgets.add(PointerInterceptor( + child: TextButton( + onPressed: () { + ToastView.dismiss(); + action.onActionClick?.call(); + }, + child: Text( + action.actionName!, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: textActionColor ?? Colors.white + ), + ), + ), + )); + } else { + trailingWidgets.add(PointerInterceptor( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + ToastView.dismiss(); + action.onActionClick?.call(); + }, + customBorder: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + action.actionIcon!, + Text( + action.actionName!, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: textActionColor ?? Colors.white + ), + ) + ], + ), + ), + ), + ), + )); + } + } + + Widget? leadingWidget; + if (leadingIcon != null) { + leadingWidget = PointerInterceptor(child: leadingIcon); + } else { + if (leadingSVGIcon != null) { + leadingWidget = PointerInterceptor( + child: SvgPicture.asset( + leadingSVGIcon, + width: 24, + height: 24, + fit: BoxFit.fill, + colorFilter: leadingSVGIconColor?.asFilter(), + ) + ); + } + } + + TMailToast.showToast( + message, + context, + maxWidth: maxWidth ?? responsiveUtils.getMaxWidthToast(context), + toastPosition: ToastPosition.BOTTOM, + textStyle: textStyle ?? TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: textColor ?? AppColor.primaryColor + ), + backgroundColor: backgroundColor ?? Colors.white, + trailing: Row(children: trailingWidgets), + leading: leadingWidget, + padding: padding, + textAlign: textAlign, + toastDuration: infinityToast + ? null + : (duration ?? const Duration(seconds: 3)), + ); + } } diff --git a/core/test/presentation/utils/app_toast_test.dart b/core/test/presentation/utils/app_toast_test.dart new file mode 100644 index 0000000000..1115205a87 --- /dev/null +++ b/core/test/presentation/utils/app_toast_test.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:mockito/mockito.dart'; +import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; + +class MockResponsiveUtils extends Mock implements ResponsiveUtils { + @override + double getMaxWidthToast(BuildContext context) => 300; +} + +void main() { + group('AppToast - showToastMessageWithMultipleActions', () { + late AppToast appToast; + late MockResponsiveUtils mockResponsiveUtils; + + setUp(() { + appToast = AppToast(); + mockResponsiveUtils = MockResponsiveUtils(); + Get.put(mockResponsiveUtils); + }); + + tearDown(() { + Get.reset(); + }); + + testWidgets( + 'should show action button ' + 'when actionName is provided and trigger callback when tapped', + (WidgetTester tester) async { + await tester.runAsync(() async { + var callbackTriggered = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return TextButton( + onPressed: () { + appToast.showToastMessageWithMultipleActions( + context, + 'Test message', + actions: [ + ( + actionName: 'Retry', + onActionClick: () { + callbackTriggered = true; + }, + actionIcon: null, + ), + ], + ); + }, + child: const Text('Show toast'), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('Show toast')); + await tester.pump(); + + expect(find.text('Retry'), findsOneWidget); + + await tester.tap(find.text('Retry')); + await tester.pump(); + + expect(callbackTriggered, true); + }); + }); + }); +} \ No newline at end of file diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 78e93c64bf..cc6866edfa 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -617,4 +617,37 @@ abstract class BaseController extends GetxController final minInputLength = session.getMinInputLengthAutocomplete(accountId); return minInputLength?.value.toInt() ?? AppConfig.defaultMinInputLengthAutocomplete; } + + void showRetryToast(FeatureFailure failure) { + if (currentOverlayContext == null || currentContext == null) return; + + final exception = failure.exception; + final errorMessage = exception is MethodLevelErrors && exception.message != null + ? AppLocalizations.of(currentContext!).unexpectedError('${exception.message!}') + : AppLocalizations.of(currentContext!).unknownError; + + appToast.showToastMessageWithMultipleActions( + currentOverlayContext!, + errorMessage, + actions: [ + if (failure.onRetry != null) + ( + actionName: AppLocalizations.of(currentContext!).retry, + onActionClick: () => consumeState(failure.onRetry!), + actionIcon: SvgPicture.asset(imagePaths.icUndo), + ), + ( + actionName: AppLocalizations.of(currentContext!).close, + onActionClick: () => ToastView.dismiss(), + actionIcon: SvgPicture.asset( + imagePaths.icClose, + colorFilter: Colors.white.asFilter(), + ), + ) + ], + backgroundColor: AppColor.toastErrorBackgroundColor, + textColor: Colors.white, + infinityToast: true, + ); + } } diff --git a/lib/features/email/domain/state/get_email_content_state.dart b/lib/features/email/domain/state/get_email_content_state.dart index af729942df..fb7d5e8a5e 100644 --- a/lib/features/email/domain/state/get_email_content_state.dart +++ b/lib/features/email/domain/state/get_email_content_state.dart @@ -51,5 +51,6 @@ class GetEmailContentFromCacheSuccess extends UIState { class GetEmailContentFailure extends FeatureFailure { - GetEmailContentFailure(dynamic exception) : super(exception: exception); + GetEmailContentFailure(dynamic exception, {super.onRetry}) + : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/email/domain/usecases/get_email_content_interactor.dart b/lib/features/email/domain/usecases/get_email_content_interactor.dart index 6e7baeb364..48fc891d31 100644 --- a/lib/features/email/domain/usecases/get_email_content_interactor.dart +++ b/lib/features/email/domain/usecases/get_email_content_interactor.dart @@ -49,7 +49,17 @@ class GetEmailContentInteractor { } } catch (e) { log('GetEmailContentInteractor::execute(): exception = $e'); - yield Left(GetEmailContentFailure(e)); + yield Left(GetEmailContentFailure( + e, + onRetry: execute( + session, + accountId, + emailId, + baseDownloadUrl, + transformConfiguration, + additionalProperties: additionalProperties + ), + )); } } @@ -97,7 +107,17 @@ class GetEmailContentInteractor { } } catch (e) { logError('GetEmailContentInteractor::_getContentEmailFromServer():EXCEPTION: $e'); - yield Left(GetEmailContentFailure(e)); + yield Left(GetEmailContentFailure( + e, + onRetry: execute( + session, + accountId, + emailId, + baseDownloadUrl, + transformConfiguration, + additionalProperties: additionalProperties + ), + )); } } diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index fd0593e9c1..e244c7f20e 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -35,6 +35,7 @@ import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mdn/disposition.dart'; import 'package:jmap_dart_client/jmap/mdn/mdn.dart'; import 'package:model/email/eml_attachment.dart'; +import 'package:model/error_type_handler/unknown_uri_exception.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; @@ -332,7 +333,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } else if (failure is ParseCalendarEventFailure) { _handleParseCalendarEventFailure(failure); } else if (failure is GetEmailContentFailure) { - emailLoadedViewState.value = Left(failure); + _handleGetEmailContentFailure(failure); } else if (failure is PrintEmailFailure) { _showMessageWhenEmailPrintingFailed(failure); } else if (failure is CalendarEventReplyFailure) { @@ -348,6 +349,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } + void _handleGetEmailContentFailure(GetEmailContentFailure failure) { + emailLoadedViewState.value = Left(failure); + showRetryToast(failure); + } + void _registerObxStreamListener() { ever(mailboxDashBoardController.accountId, (accountId) { if (accountId is AccountId) { @@ -609,7 +615,20 @@ class SingleEmailController extends BaseController with AppLoaderMixin { )); } catch (e) { logError('SingleEmailController::_getEmailContentAction(): $e'); - consumeState(Stream.value(Left(GetEmailContentFailure(e)))); + consumeState(Stream.value(Left(GetEmailContentFailure( + e, + onRetry: e is UnknownUriException + ? null + : _getEmailContentInteractor.execute( + session!, + accountId!, + emailId, + session!.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl), + PlatformInfo.isWeb + ? TransformConfiguration.forPreviewEmailOnWeb() + : TransformConfiguration.forPreviewEmail(), + ), + )))); } } } diff --git a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart index f00f13a5b9..b4e8e32731 100644 --- a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart +++ b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart @@ -87,17 +87,12 @@ class MailboxRepositoryImpl extends MailboxRepository { Session session, {Properties? properties} ) async { - try { - final jmapMailboxResponse = await mapDataSource[DataSourceType.network]!.getAllMailbox( - session, - accountId, - properties: properties); - log('MailboxRepositoryImpl::_getAllMailboxFromJMAP: MAILBOX_NETWORK = ${jmapMailboxResponse.mailboxes.length} | STATE_NETWORK = ${jmapMailboxResponse.state}'); - return jmapMailboxResponse; - } catch (e) { - logError('MailboxRepositoryImpl::_getAllMailboxFromJMAP: Exception: $e'); - return null; - } + final jmapMailboxResponse = await mapDataSource[DataSourceType.network]!.getAllMailbox( + session, + accountId, + properties: properties); + log('MailboxRepositoryImpl::_getAllMailboxFromJMAP: MAILBOX_NETWORK = ${jmapMailboxResponse.mailboxes.length} | STATE_NETWORK = ${jmapMailboxResponse.state}'); + return jmapMailboxResponse; } Future _syncNewInCache( diff --git a/lib/features/mailbox/domain/state/get_all_mailboxes_state.dart b/lib/features/mailbox/domain/state/get_all_mailboxes_state.dart index 3cb2486a6a..6490e923a8 100644 --- a/lib/features/mailbox/domain/state/get_all_mailboxes_state.dart +++ b/lib/features/mailbox/domain/state/get_all_mailboxes_state.dart @@ -19,5 +19,5 @@ class GetAllMailboxSuccess extends UIState { class GetAllMailboxFailure extends FeatureFailure { - GetAllMailboxFailure(dynamic exception) : super(exception: exception); + GetAllMailboxFailure(dynamic exception, {super.onRetry}) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart index 367848876f..07d6be5c6d 100644 --- a/lib/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart @@ -25,7 +25,10 @@ class GetAllMailboxInteractor { properties: properties) .map(_toGetMailboxState); } catch (e) { - yield Left(GetAllMailboxFailure(e)); + yield Left(GetAllMailboxFailure( + e, + onRetry: execute(session, accountId, properties: properties), + )); } } diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 4373582e96..b0fb7ff0c0 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -232,6 +232,7 @@ class MailboxController extends BaseMailboxController (failure) { if (failure is GetAllMailboxFailure) { mailboxDashBoardController.updateRefreshAllMailboxState(Left(RefreshAllMailboxFailure())); + showRetryToast(failure); } }, (success) { diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 546baa81d5..8a646de116 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -2674,7 +2674,7 @@ class MailboxDashBoardController extends ReloadableController (failure) => false, (success) => success is RefreshingAllEmail); log('MailboxDashBoardController::isRefreshingAllMailboxAndEmail:isRefreshingMailbox = $isRefreshingMailbox | isRefreshingEmail = $isRefreshingEmail'); - return isRefreshingMailbox && isRefreshingEmail; + return isRefreshingMailbox || isRefreshingEmail; } void selectAllEmailAction() { diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index d85380e289..3481428fea 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -483,6 +483,7 @@ class SearchEmailController extends BaseController void _searchEmailsFailure(SearchEmailFailure failure) { listResultSearch.clear(); resultSearchViewState.value = Left(failure); + showRetryToast(failure); } void searchMoreEmailsAction() { diff --git a/lib/features/thread/domain/state/search_email_state.dart b/lib/features/thread/domain/state/search_email_state.dart index 05599e9deb..b78bc20df4 100644 --- a/lib/features/thread/domain/state/search_email_state.dart +++ b/lib/features/thread/domain/state/search_email_state.dart @@ -17,5 +17,5 @@ class SearchEmailSuccess extends UIState { class SearchEmailFailure extends FeatureFailure { - SearchEmailFailure(dynamic exception) : super(exception: exception); + SearchEmailFailure(dynamic exception, {super.onRetry}) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/search_email_interactor.dart b/lib/features/thread/domain/usecases/search_email_interactor.dart index 6d75a963b7..1e6ccceead 100644 --- a/lib/features/thread/domain/usecases/search_email_interactor.dart +++ b/lib/features/thread/domain/usecases/search_email_interactor.dart @@ -55,7 +55,19 @@ class SearchEmailInteractor { yield Right(SearchEmailSuccess(presentationEmailList)); } catch (e) { - yield Left(SearchEmailFailure(e)); + yield Left(SearchEmailFailure( + e, + onRetry: execute( + session, + accountId, + filter: filter, + limit: limit, + position: position, + sort: sort, + properties: properties, + needRefreshSearchState: true, + ), + )); } } } \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 1f2d70951f..c9bb7a9d83 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -201,6 +201,7 @@ class ThreadController extends BaseController with EmailActionController { mailboxDashBoardController.updateRefreshAllEmailState(Left(RefreshAllEmailFailure())); canSearchMore = false; mailboxDashBoardController.emailsInCurrentMailbox.clear(); + showRetryToast(failure); } else if (failure is SearchMoreEmailFailure) { loadingMoreStatus.value = LoadingMoreStatus.completed; canSearchMore = true; diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index dfc59c1443..f091d42c02 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -4393,5 +4393,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "retry": "Retry", + "@retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 12b91dfdf7..9f7ab9626a 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4618,4 +4618,11 @@ class AppLocalizations { name: 'viewEntireMessage', ); } + + String get retry { + return Intl.message( + 'Retry', + name: 'retry', + ); + } } diff --git a/test/features/email/presentation/controller/single_email_controller_test.dart b/test/features/email/presentation/controller/single_email_controller_test.dart index 22d5c9efb9..7d0e9622d4 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -1,9 +1,10 @@ import 'dart:convert'; -import 'dart:ui'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart' hide State; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' hide State; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -24,6 +25,7 @@ import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasourc import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/features/email/data/repository/calendar_event_repository_impl.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_accept_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_all_attachments_for_web_interactor.dart'; @@ -57,6 +59,9 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oi import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart'; +import 'package:tmail_ui_user/main/localizations/localization_service.dart'; import 'package:tmail_ui_user/main/utils/toast_manager.dart'; import 'package:tmail_ui_user/main/utils/twake_app_manager.dart'; import 'package:uuid/uuid.dart'; @@ -449,4 +454,75 @@ void main() { ); }); }); + + Widget makeTestableWidget({required Widget child}) { + return GetMaterialApp( + localizationsDelegates: const [ + AppLocalizationsDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: LocalizationService.supportedLocales, + home: Scaffold(body: child), + ); + } + + testWidgets( + 'should show retry toast ' + 'when handleFailureViewState is called with GetEmailContentFailure', + (tester) async { + // arrange + when(mailboxDashboardController.selectedEmail).thenReturn(Rxn(PresentationEmail())); + when(mailboxDashboardController.emailUIAction).thenReturn(Rxn(EmailUIAction())); + when(mailboxDashboardController.viewState).thenReturn(Rx(Right(UIState.idle))); + when(appToast.showToastMessageWithMultipleActions( + any, + any, + actions: anyNamed('actions'), + textColor: anyNamed('textColor'), + backgroundColor: anyNamed('backgroundColor'), + infinityToast: anyNamed('infinityToast'), + )).thenAnswer((realInvocation) { + AppToast().showToastMessageWithMultipleActions( + realInvocation.positionalArguments[0], + realInvocation.positionalArguments[1], + actions: realInvocation.namedArguments[const Symbol('actions')], + textColor: realInvocation.namedArguments[const Symbol('textColor')], + backgroundColor: realInvocation.namedArguments[const Symbol('backgroundColor')], + infinityToast: realInvocation.namedArguments[const Symbol('infinityToast')], + ); + }); + when(imagePaths.icUndo).thenReturn(ImagePaths().icUndo); + when(imagePaths.icClose).thenReturn(ImagePaths().icClose); + Get.put(singleEmailController); + final widget = makeTestableWidget(child: const _TestView()); + await tester.pumpWidget(widget); + await tester.pump(); + + // act + singleEmailController.handleFailureViewState(GetEmailContentFailure( + null, + onRetry: const Stream.empty(), + )); + await tester.pump(); + + // assert + expect(find.text(AppLocalizations().unknownError), findsOneWidget); + expect(find.text(AppLocalizations().retry), findsOneWidget); + expect(find.text(AppLocalizations().close), findsOneWidget); + + // cleanup + Get.delete(); + }, + ); } + +class _TestView extends GetWidget { + const _TestView(); + + @override + Widget build(BuildContext context) { + return const SizedBox(); + } +} \ No newline at end of file diff --git a/test/features/mailbox/repository/mailbox_respository_test.dart b/test/features/mailbox/repository/mailbox_respository_test.dart index d204fd6149..38528da04a 100644 --- a/test/features/mailbox/repository/mailbox_respository_test.dart +++ b/test/features/mailbox/repository/mailbox_respository_test.dart @@ -67,12 +67,9 @@ void main() { final streamMailboxResponses = mailboxRepository.getAllMailbox(sessionFixture, accountIdFixture); - final listMailboxResponse = await streamMailboxResponses.toList(); - - expect(listMailboxResponse.length, 1); - expect( - listMailboxResponse, - containsAllInOrder([ + await expectLater( + streamMailboxResponses, + emitsInOrder([ CacheMailboxResponse( mailboxes: [ MailboxFixtures.mailboxA, @@ -81,7 +78,8 @@ void main() { MailboxFixtures.mailboxD, ], state: StateFixtures.currentMailboxState - ) + ), + emitsError(isA()) ]) ); }); diff --git a/test/features/thread/domain/usecases/search_email_interactor_test.dart b/test/features/thread/domain/usecases/search_email_interactor_test.dart index 507f2b8d3c..954d5c11f4 100644 --- a/test/features/thread/domain/usecases/search_email_interactor_test.dart +++ b/test/features/thread/domain/usecases/search_email_interactor_test.dart @@ -1,3 +1,5 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; @@ -100,7 +102,7 @@ void main() { test( 'should return Failure when threadRepository.searchEmails returns Failure', - () { + () async { // arrange final exception = Exception(); when( @@ -119,15 +121,19 @@ void main() { SessionFixtures.aliceSession, AccountFixtures.aliceAccountId, filter: EmailFilterCondition(text: 'test'), - ); + ).asBroadcastStream(); // assert + final firstState = await result.first; + final lastState = await result.last; + expect(firstState, Right(SearchingState())); expect( - result, - emitsInOrder([ - Right(SearchingState()), - Left(SearchEmailFailure(exception)), - ]), + lastState.fold((failure) { + return failure is SearchEmailFailure + && failure.exception == exception + && failure.onRetry is Stream>; + }, (success) => false), + true, ); }, );