From 6a31880a85f0c841059cb4ae7f439b08fde10ff8 Mon Sep 17 00:00:00 2001 From: alex-chevski Date: Thu, 29 Oct 2026 17:27:12 +0300 Subject: [PATCH 1/2] fix(consultations): open chat from consultation card The chat icon on the consultant consultation card had an empty onTap handler. Resolve the conversation with the client via the chat API and navigate to it. related to #1106 --- lib/core/di/cubit_registrations.dart | 2 +- .../cubit/consultations_cubit.dart | 15 +++++- .../widgets/consultation_card_widget.dart | 18 ++++++- .../cubit/consultations_cubit_test.dart | 38 ++++++++++++++- .../consultation_card_widget_test.dart | 48 ++++++++++++++++++- 5 files changed, 115 insertions(+), 6 deletions(-) diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index 05bfd7d1..19a1962b 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -263,7 +263,7 @@ class CubitRegistrations { ), ); sl.registerFactory( - () => ConsultationsCubit(consultationsRepository: sl()), + () => ConsultationsCubit(consultationsRepository: sl(), chatRepository: sl()), ); sl.registerFactory( () => ConsultantCommentsCubit(diaryRepository: sl()), diff --git a/lib/features/consultations/presentation/cubit/consultations_cubit.dart b/lib/features/consultations/presentation/cubit/consultations_cubit.dart index 80f8b206..ea830c88 100644 --- a/lib/features/consultations/presentation/cubit/consultations_cubit.dart +++ b/lib/features/consultations/presentation/cubit/consultations_cubit.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:eatvio_app/core/enums/base_state.dart'; +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart'; import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -8,7 +9,8 @@ import 'package:eatvio_app/core/error_reporting/report_from.dart'; class ConsultationsCubit extends Cubit { final ConsultationsRepository consultationsRepository; - ConsultationsCubit({required this.consultationsRepository}) : super(ConsultationsState()); + final ChatRepository chatRepository; + ConsultationsCubit({required this.consultationsRepository, required this.chatRepository}) : super(ConsultationsState()); Future loadConsultations() async { try { @@ -20,4 +22,15 @@ class ConsultationsCubit extends Cubit { emit(state.copyWith(state: BaseState.error)); } } + + Future getChatIdByUserSlug(String slug) async { + try { + final chatIdOrError = await chatRepository.getChatByUserSlug(slug); + return chatIdOrError == '404' ? null : chatIdOrError; + } catch (e, st) { + await reportFrom(e, st, this); + log(e.toString(), stackTrace: st); + return null; + } + } } diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index 7efa25f7..e579c9b3 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -3,14 +3,17 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class ConsultationCardWidget extends StatefulWidget { @@ -34,6 +37,7 @@ class _ConsultationCardWidgetState extends State { final List legendColors = const [Colors.white, Color(0xFFF0C497), Color(0xFFFF9500), Color(0xFF2F80ED), Color(0xFF4CD964)]; late final List<(int, DiaryModel)> givenDaysWithDayNumber; + bool _isOpeningChat = false; final List> legendStates = [ [DiaryStateEnum.init], [DiaryStateEnum.started, DiaryStateEnum.lastDayStarted], @@ -279,7 +283,7 @@ class _ConsultationCardWidgetState extends State { const SizedBox(width: 8), InkWell( borderRadius: BorderRadius.circular(20), - onTap: () {}, + onTap: _isOpeningChat ? null : _openChat, child: Ink( height: 52, width: 52, @@ -308,6 +312,18 @@ class _ConsultationCardWidgetState extends State { ); } + Future _openChat() async { + setState(() => _isOpeningChat = true); + final chatId = await context.read().getChatIdByUserSlug(widget.consultationsModel.userSlug); + if (!mounted) return; + setState(() => _isOpeningChat = false); + if (chatId == null) { + showErrorToast(S.of(context).msg_something_went_wrong); + return; + } + GoRouter.of(context).pushNamed('chatScreen', extra: {'uuid': chatId}); + } + Row _legendTitle(String text, Color color) { return Row( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/test/features/consultations/presentation/cubit/consultations_cubit_test.dart b/test/features/consultations/presentation/cubit/consultations_cubit_test.dart index 26ab5974..c9fe20b5 100644 --- a/test/features/consultations/presentation/cubit/consultations_cubit_test.dart +++ b/test/features/consultations/presentation/cubit/consultations_cubit_test.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/core/enums/base_state.dart'; +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart'; @@ -8,14 +9,21 @@ import 'package:mocktail/mocktail.dart'; class MockConsultationsRepository extends Mock implements ConsultationsRepository {} +class MockChatRepository extends Mock implements ChatRepository {} + void main() { group('ConsultationsCubit', () { late ConsultationsCubit cubit; late MockConsultationsRepository mockRepository; + late MockChatRepository mockChatRepository; setUp(() { mockRepository = MockConsultationsRepository(); - cubit = ConsultationsCubit(consultationsRepository: mockRepository); + mockChatRepository = MockChatRepository(); + cubit = ConsultationsCubit( + consultationsRepository: mockRepository, + chatRepository: mockChatRepository, + ); }); tearDown(() { @@ -47,5 +55,33 @@ void main() { expect(cubit.state.state, BaseState.error); }); + + test('getChatIdByUserSlug - возвращает id чата', () async { + when(() => mockChatRepository.getChatByUserSlug('john-doe')) + .thenAnswer((_) async => 'chat-uuid'); + + final chatId = await cubit.getChatIdByUserSlug('john-doe'); + + expect(chatId, 'chat-uuid'); + verify(() => mockChatRepository.getChatByUserSlug('john-doe')).called(1); + }); + + test('getChatIdByUserSlug - возвращает null при ошибке', () async { + when(() => mockChatRepository.getChatByUserSlug(any())) + .thenThrow(Exception('Error')); + + final chatId = await cubit.getChatIdByUserSlug('john-doe'); + + expect(chatId, isNull); + }); + + test('getChatIdByUserSlug - возвращает null когда чат не найден', () async { + when(() => mockChatRepository.getChatByUserSlug(any())) + .thenAnswer((_) async => '404'); + + final chatId = await cubit.getChatIdByUserSlug('john-doe'); + + expect(chatId, isNull); + }); }); } diff --git a/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart b/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart index c897d8e2..c170f224 100644 --- a/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart +++ b/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart @@ -3,16 +3,21 @@ import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_card_widget.dart'; import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:network_image_mock/network_image_mock.dart'; +class MockConsultationsCubit extends Mock implements ConsultationsCubit {} + void main() { group('ConsultationCardWidget smoke', () { setUpAll(() async { @@ -64,7 +69,8 @@ void main() { personalDietId: null, ); - Widget wrap(Widget child) { + Widget wrap(Widget child, {ConsultationsCubit? cubit}) { + final consultationsCubit = cubit ?? MockConsultationsCubit(); return MaterialApp.router( theme: ThemeHelper().themeData(), localizationsDelegates: const [ @@ -77,7 +83,20 @@ void main() { routerConfig: GoRouter( initialLocation: '/', routes: [ - GoRoute(path: '/', builder: (context, state) => child), + GoRoute( + path: '/', + builder: (context, state) => BlocProvider.value( + value: consultationsCubit, + child: child, + ), + ), + GoRoute( + path: '/chat-screen', + name: 'chatScreen', + builder: (context, state) => Scaffold( + body: Text('chat:${(state.extra as Map)['uuid']}'), + ), + ), GoRoute( path: '/profile-screen', name: 'profileScreen', @@ -163,5 +182,30 @@ void main() { }); }, ); + + testWidgets('opens chat with consultation user when chat icon tapped', ( + tester, + ) async { + final cubit = MockConsultationsCubit(); + when(() => cubit.stream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.getChatIdByUserSlug('john-doe')) + .thenAnswer((_) async => 'chat-uuid'); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + wrap( + ConsultationCardWidget(consultationsModel: mockConsultation), + cubit: cubit, + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.chat_outlined)); + await tester.pumpAndSettle(); + + expect(find.text('chat:chat-uuid'), findsOneWidget); + verify(() => cubit.getChatIdByUserSlug('john-doe')).called(1); + }); + }); }); } From f14f42638133ab588ca90cd05f900edea2d03acf Mon Sep 17 00:00:00 2001 From: alex-chevski Date: Thu, 29 Oct 2026 18:09:08 +0300 Subject: [PATCH 2/2] fix(consultations): open chat via consultation chat_id Use chat_id from the consultations payload for direct navigation and keep the chat lookup as a fallback for legacy rows without chat_id. - validate chat id in ChatRepository and make it nullable - guard repeated taps while opening chat - handle nullable chat id in ProfileCubit and EmptyChatScreen related to #1106 --- .../presentation/pages/empty_chat_screen.dart | 24 ++-- .../chat/repositories/chat_repository.dart | 9 +- .../models/consultation_model.dart | 1 + .../cubit/consultations_cubit.dart | 3 +- .../widgets/consultation_card_widget.dart | 8 +- .../bloc/profile_cubit/profile_cubit.dart | 10 +- .../repositories/chat_repository_test.dart | 6 +- .../models/consultation_model_test.dart | 49 ++++++++ .../cubit/consultations_cubit_test.dart | 4 +- .../consultation_card_widget_test.dart | 105 ++++++++++++++++++ test/fixtures/doubles/profile_doubles.dart | 2 +- 11 files changed, 190 insertions(+), 31 deletions(-) diff --git a/lib/features/chat/presentation/pages/empty_chat_screen.dart b/lib/features/chat/presentation/pages/empty_chat_screen.dart index a0ddf60a..43f20390 100644 --- a/lib/features/chat/presentation/pages/empty_chat_screen.dart +++ b/lib/features/chat/presentation/pages/empty_chat_screen.dart @@ -135,9 +135,9 @@ class _EmptyChatScreenState extends State { textEditingController.clear(); }); } - final chatIdOrError = await sl().getChatByUserSlug(widget.user.slug); - if (context.mounted) { - AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatIdOrError}); + final chatId = await sl().getChatByUserSlug(widget.user.slug); + if (chatId != null && chatId.isNotEmpty && context.mounted) { + AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatId}); } } }, @@ -189,9 +189,9 @@ class _EmptyChatScreenState extends State { if (images.isNotEmpty) { List files = images.map((image) => File(image.path)).toList(); await sl().sendMessage(files: files, slug: widget.user.slug); - final chatIdOrError = await sl().getChatByUserSlug(widget.user.slug); - if (mounted) { - AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatIdOrError}); + final chatId = await sl().getChatByUserSlug(widget.user.slug); + if (chatId != null && chatId.isNotEmpty && mounted) { + AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatId}); } } }, @@ -205,9 +205,9 @@ class _EmptyChatScreenState extends State { if (images.isNotEmpty) { List files = images.map((image) => File(image.path)).toList(); await sl().sendMessage(files: files, slug: widget.user.slug); - final chatIdOrError = await sl().getChatByUserSlug(widget.user.slug); - if (mounted) { - AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatIdOrError}); + final chatId = await sl().getChatByUserSlug(widget.user.slug); + if (chatId != null && chatId.isNotEmpty && mounted) { + AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatId}); } } }, @@ -243,9 +243,9 @@ class _EmptyChatScreenState extends State { if (result != null) { List files = result.paths.map((path) => File(path!)).toList(); await sl().sendMessage(files: files, slug: widget.user.slug); - final chatIdOrError = await sl().getChatByUserSlug(widget.user.slug); - if (mounted) { - AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatIdOrError}); + final chatId = await sl().getChatByUserSlug(widget.user.slug); + if (chatId != null && chatId.isNotEmpty && mounted) { + AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatId}); } } }, diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index ade2ed26..9230e4d8 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -137,11 +137,12 @@ class ChatRepository { } - Future getChatByUserSlug(String slug) async { + Future getChatByUserSlug(String slug) async { final response = await chatService.getChatByUserSlug(slug: slug); - if (response.response.statusCode == 404) { - return '404'; + final data = response.data; + if (data is Map && data['id'] is String && (data['id'] as String).isNotEmpty) { + return data['id'] as String; } - return response.data['id']; + return null; } } diff --git a/lib/features/consultations/models/consultation_model.dart b/lib/features/consultations/models/consultation_model.dart index c73a8d37..50280cfe 100644 --- a/lib/features/consultations/models/consultation_model.dart +++ b/lib/features/consultations/models/consultation_model.dart @@ -9,6 +9,7 @@ part 'consultation_model.g.dart'; abstract class ConsultationModel with _$ConsultationModel { factory ConsultationModel({ required int id, + @JsonKey(name: 'chat_id') String? chatId, @JsonKey(name: 'last_state') required ChosenPlanStateEnum chosenPlanState, @JsonKey(name: 'plan_title') required String planTitle, @JsonKey(name: 'user_name') required String userName, diff --git a/lib/features/consultations/presentation/cubit/consultations_cubit.dart b/lib/features/consultations/presentation/cubit/consultations_cubit.dart index ea830c88..3b94fa8c 100644 --- a/lib/features/consultations/presentation/cubit/consultations_cubit.dart +++ b/lib/features/consultations/presentation/cubit/consultations_cubit.dart @@ -25,8 +25,7 @@ class ConsultationsCubit extends Cubit { Future getChatIdByUserSlug(String slug) async { try { - final chatIdOrError = await chatRepository.getChatByUserSlug(slug); - return chatIdOrError == '404' ? null : chatIdOrError; + return await chatRepository.getChatByUserSlug(slug); } catch (e, st) { await reportFrom(e, st, this); log(e.toString(), stackTrace: st); diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index e579c9b3..380ad7a9 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -313,11 +313,15 @@ class _ConsultationCardWidgetState extends State { } Future _openChat() async { + if (_isOpeningChat) return; setState(() => _isOpeningChat = true); - final chatId = await context.read().getChatIdByUserSlug(widget.consultationsModel.userSlug); + var chatId = widget.consultationsModel.chatId; + if (chatId == null || chatId.isEmpty) { + chatId = await context.read().getChatIdByUserSlug(widget.consultationsModel.userSlug); + } if (!mounted) return; setState(() => _isOpeningChat = false); - if (chatId == null) { + if (chatId == null || chatId.isEmpty) { showErrorToast(S.of(context).msg_something_went_wrong); return; } diff --git a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart index 2801c2b4..304aeefe 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -141,13 +141,13 @@ class ProfileCubit extends Cubit { Future getChatRoomIdBySlug({required String slug}) async { try { - final chatIdOrError = await chatRepository.getChatByUserSlug(slug); - final userProfile = await profileRepository.getProfileBySlug(slug); - if (chatIdOrError == '404') { + final chatId = await chatRepository.getChatByUserSlug(slug); + if (chatId == null || chatId.isEmpty) { + final userProfile = await profileRepository.getProfileBySlug(slug); AppRouter.navigatorKey.currentContext?.pushNamed('emptyChatScreen', extra: userProfile); } else { - AppRouter.navigatorKey.currentContext?.pushNamed('chatScreen', extra: {'uuid': chatIdOrError}); - return chatIdOrError; + AppRouter.navigatorKey.currentContext?.pushNamed('chatScreen', extra: {'uuid': chatId}); + return chatId; } } catch (e, st) { await reportFrom(e, st, this); diff --git a/test/features/chat/repositories/chat_repository_test.dart b/test/features/chat/repositories/chat_repository_test.dart index 247d1311..e0c271ab 100644 --- a/test/features/chat/repositories/chat_repository_test.dart +++ b/test/features/chat/repositories/chat_repository_test.dart @@ -526,11 +526,11 @@ void main() { }, ); - test('getChatByUserSlug - возвращает 404 при отсутствии чата', () async { + test('getChatByUserSlug - возвращает null, если в ответе нет id', () async { const slug = 'user-slug'; final response = HttpResponse( {}, - Response(requestOptions: RequestOptions(path: ''), statusCode: 404), + Response(requestOptions: RequestOptions(path: ''), statusCode: 200), ); when( @@ -539,7 +539,7 @@ void main() { final result = await chatRepository.getChatByUserSlug(slug); - expect(result, '404'); + expect(result, isNull); verify(mockChatService.getChatByUserSlug(slug: slug)).called(1); }); diff --git a/test/features/consultations/models/consultation_model_test.dart b/test/features/consultations/models/consultation_model_test.dart index 6d927618..64ac25f9 100644 --- a/test/features/consultations/models/consultation_model_test.dart +++ b/test/features/consultations/models/consultation_model_test.dart @@ -156,6 +156,55 @@ void main() { }); group('ConsultationModel', () { + test('fromJson parses chat_id', () { + final json = { + 'id': 1, + 'chat_id': 'direct-chat-uuid', + 'last_state': 'started', + 'plan_title': 'Test Plan', + 'user_name': 'User 1', + 'user_slug': 'user-1', + 'consultant_payment': 1000.0, + 'duration': 30, + 'current_diary_day': 5, + 'user_profile_photo_url': 'photo.jpg', + 'consultation_option_price': 2000, + 'start_date': '2024-01-01T00:00:00Z', + 'end_date': '2024-01-31T00:00:00Z', + 'consultant_id': 1, + 'diaries': [], + 'personal_diet_id': null, + }; + + final model = ConsultationModel.fromJson(json); + + expect(model.chatId, 'direct-chat-uuid'); + }); + + test('fromJson allows missing chat_id', () { + final json = { + 'id': 1, + 'last_state': 'started', + 'plan_title': 'Test Plan', + 'user_name': 'User 1', + 'user_slug': 'user-1', + 'consultant_payment': 1000.0, + 'duration': 30, + 'current_diary_day': 5, + 'user_profile_photo_url': 'photo.jpg', + 'consultation_option_price': 2000, + 'start_date': '2024-01-01T00:00:00Z', + 'end_date': '2024-01-31T00:00:00Z', + 'consultant_id': 1, + 'diaries': [], + 'personal_diet_id': null, + }; + + final model = ConsultationModel.fromJson(json); + + expect(model.chatId, isNull); + }); + test('toJson serializes ConsultationModel correctly', () { final model = ConsultationModel( id: 1, diff --git a/test/features/consultations/presentation/cubit/consultations_cubit_test.dart b/test/features/consultations/presentation/cubit/consultations_cubit_test.dart index c9fe20b5..0e3fe5ef 100644 --- a/test/features/consultations/presentation/cubit/consultations_cubit_test.dart +++ b/test/features/consultations/presentation/cubit/consultations_cubit_test.dart @@ -75,9 +75,9 @@ void main() { expect(chatId, isNull); }); - test('getChatIdByUserSlug - возвращает null когда чат не найден', () async { + test('getChatIdByUserSlug - возвращает null когда чата нет', () async { when(() => mockChatRepository.getChatByUserSlug(any())) - .thenAnswer((_) async => '404'); + .thenAnswer((_) async => null); final chatId = await cubit.getChatIdByUserSlug('john-doe'); diff --git a/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart b/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart index c170f224..46c5e042 100644 --- a/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart +++ b/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/locator_service.dart'; @@ -8,6 +10,7 @@ import 'package:eatvio_app/features/consultations/presentation/widgets/consultat import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -33,6 +36,20 @@ void main() { AppConfig(buildType: AppBuildType.tests) ..setBaseUrl('http://localhost'), ); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('PonnamKarthik/fluttertoast'), + (MethodCall methodCall) async => null, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('PonnamKarthik/fluttertoast'), + null, + ); }); final mockConsultation = ConsultationModel( @@ -207,5 +224,93 @@ void main() { verify(() => cubit.getChatIdByUserSlug('john-doe')).called(1); }); }); + + testWidgets( + 'opens chat directly with chat id without requesting it again', + (tester) async { + final cubit = MockConsultationsCubit(); + when(() => cubit.stream).thenAnswer((_) => const Stream.empty()); + final consultationWithChatId = mockConsultation.copyWith( + chatId: 'direct-chat-uuid', + ); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + wrap( + ConsultationCardWidget( + consultationsModel: consultationWithChatId, + ), + cubit: cubit, + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.chat_outlined)); + await tester.pumpAndSettle(); + + expect(find.text('chat:direct-chat-uuid'), findsOneWidget); + verifyNever(() => cubit.getChatIdByUserSlug(any())); + }); + }, + ); + + testWidgets('shows error and stays on card when chat id is null', ( + tester, + ) async { + final cubit = MockConsultationsCubit(); + when(() => cubit.stream).thenAnswer((_) => const Stream.empty()); + when(() => cubit.getChatIdByUserSlug('john-doe')) + .thenAnswer((_) async => null); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + wrap( + ConsultationCardWidget(consultationsModel: mockConsultation), + cubit: cubit, + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.chat_outlined)); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 3)); + + expect(find.textContaining('chat:'), findsNothing); + expect( + find.byType(ConsultationCardWidget), + findsOneWidget, + ); + verify(() => cubit.getChatIdByUserSlug('john-doe')).called(1); + }); + }); + + testWidgets('does not request chat id twice while first is pending', ( + tester, + ) async { + final cubit = MockConsultationsCubit(); + when(() => cubit.stream).thenAnswer((_) => const Stream.empty()); + final completer = Completer(); + when(() => cubit.getChatIdByUserSlug('john-doe')) + .thenAnswer((_) => completer.future); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + wrap( + ConsultationCardWidget(consultationsModel: mockConsultation), + cubit: cubit, + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.chat_outlined)); + await tester.tap(find.byIcon(Icons.chat_outlined)); + + completer.complete('chat-uuid'); + await tester.pumpAndSettle(); + + expect(find.text('chat:chat-uuid'), findsOneWidget); + verify(() => cubit.getChatIdByUserSlug('john-doe')).called(1); + }); + }); }); } diff --git a/test/fixtures/doubles/profile_doubles.dart b/test/fixtures/doubles/profile_doubles.dart index dfc8e935..0522fc1d 100644 --- a/test/fixtures/doubles/profile_doubles.dart +++ b/test/fixtures/doubles/profile_doubles.dart @@ -69,7 +69,7 @@ class FixtureChatRepository extends ChatRepository { : super(chatService: _NoopChatService()); @override - Future getChatByUserSlug(String slug) async { + Future getChatByUserSlug(String slug) async { return 'chat-uuid-123'; } }