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
This commit is contained in:
2026-10-29 18:09:08 +03:00
parent 6a31880a85
commit f14f426381
11 changed files with 190 additions and 31 deletions

View File

@@ -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<String?>();
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);
});
});
});
}