forked from eatvio/eatvio_app
feat(chat): add consultation progress and personal diet status
related to #1115
This commit is contained in:
@@ -78,6 +78,66 @@ void main() {
|
||||
meta: const ChatMetaModel(lastPage: 1),
|
||||
);
|
||||
|
||||
const planForChosenPlanUpdates = PlanModel(
|
||||
id: 1,
|
||||
slug: 'test-plan',
|
||||
name: 'Test Plan',
|
||||
image: 'https://example.com/image.jpg',
|
||||
duration: 7,
|
||||
state: 'active',
|
||||
type: PlanType(
|
||||
fit: true,
|
||||
slim: false,
|
||||
behavior: false,
|
||||
wellness: false,
|
||||
),
|
||||
details: 'Test details',
|
||||
packetsCount: 3,
|
||||
author: Author(
|
||||
authorSlug: 'author',
|
||||
authorName: 'Author Name',
|
||||
authorProfileIcon: 'https://example.com/icon.jpg',
|
||||
authorIsMe: false,
|
||||
),
|
||||
totalLikes: 10,
|
||||
totalViews: 100,
|
||||
totalComments: 5,
|
||||
isFavorite: false,
|
||||
minPriceOfOptions: 1000,
|
||||
maxPriceOfOptions: 2000,
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
|
||||
ChosenPlanModel buildChosenPlan(
|
||||
int id,
|
||||
ChosenPlanStateEnum chosenPlanState,
|
||||
) {
|
||||
return ChosenPlanModel(
|
||||
id: id,
|
||||
optionType: 'options1',
|
||||
startDate: DateTime.now(),
|
||||
chosenPlanState: chosenPlanState,
|
||||
chatId: 'chat-id',
|
||||
consultantId: 1,
|
||||
plan: planForChosenPlanUpdates,
|
||||
);
|
||||
}
|
||||
|
||||
void stubChatChannel({
|
||||
required Stream<ChannelReadEvent> chosenPlanStream,
|
||||
}) {
|
||||
when(mockWsManager.client).thenReturn(MockPusherChannelsClient());
|
||||
when(
|
||||
mockWsManager.directChatStream,
|
||||
).thenAnswer((_) => const Stream<ChannelReadEvent>.empty());
|
||||
when(
|
||||
mockWsManager.clientTypingStream,
|
||||
).thenAnswer((_) => const Stream<ChannelReadEvent>.empty());
|
||||
when(
|
||||
mockWsManager.chosenPlanStateChangedStream,
|
||||
).thenAnswer((_) => chosenPlanStream);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
mockChatRepository = MockChatRepository();
|
||||
mockCardsRepository = MockCardsRepository();
|
||||
@@ -721,6 +781,160 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'chosenPlanStateChangedStream - перезапрашивает план и обновляет state',
|
||||
() async {
|
||||
const uuid = 'room-uuid';
|
||||
const planId = 321;
|
||||
final chosenPlan = buildChosenPlan(
|
||||
planId,
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
);
|
||||
final updatedPlan = chosenPlan.copyWith(
|
||||
chosenPlanState: ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
final controller = StreamController<ChannelReadEvent>.broadcast();
|
||||
|
||||
when(
|
||||
mockChatRepository.getChatRoom(uuid),
|
||||
).thenAnswer((_) async => testChatRoom.copyWith(chosenPlanId: planId));
|
||||
when(
|
||||
mockChatRepository.getChatMessages(uuid, 1),
|
||||
).thenAnswer((_) async => testChatMessages);
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: planId),
|
||||
).thenAnswer((_) async => chosenPlan);
|
||||
stubChatChannel(chosenPlanStream: controller.stream);
|
||||
|
||||
await chatRoomCubit.initChat(uuid);
|
||||
expect(chatRoomCubit.state, isA<ChatRoomStateLoaded>());
|
||||
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: planId),
|
||||
).thenAnswer((_) async => updatedPlan);
|
||||
clearInteractions(mockChatRepository);
|
||||
|
||||
controller.add(_FakeChannelReadEvent());
|
||||
await Future<void>.delayed(const Duration(milliseconds: 350));
|
||||
|
||||
verify(mockChatRepository.getChosenPlan(id: planId)).called(1);
|
||||
final state = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||
expect(
|
||||
state.chatRoom.chosenPlan?.chosenPlanState,
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
|
||||
await controller.close();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'chosenPlanStateChangedStream - использует chosenPlan.id если chosenPlanId null',
|
||||
() async {
|
||||
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
|
||||
const conversationUuid = 'fbd81429-8019-499a-80e8-e3933cc4d872';
|
||||
const planId = 654;
|
||||
final chosenPlan = buildChosenPlan(
|
||||
planId,
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
);
|
||||
final updatedPlan = chosenPlan.copyWith(
|
||||
chosenPlanState: ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
final controller = StreamController<ChannelReadEvent>.broadcast();
|
||||
|
||||
when(
|
||||
mockChatRepository.resolveOrderIdToConversation(orderId: orderId),
|
||||
).thenAnswer(
|
||||
(_) async => OrderIdToConversationModel(
|
||||
chosenPlanId: planId,
|
||||
conversationUuid: conversationUuid,
|
||||
),
|
||||
);
|
||||
when(
|
||||
mockChatRepository.getChatRoom(conversationUuid),
|
||||
).thenAnswer((_) async => testChatRoom);
|
||||
when(
|
||||
mockChatRepository.getChatMessages(conversationUuid, 1),
|
||||
).thenAnswer((_) async => testChatMessages);
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: planId),
|
||||
).thenAnswer((_) async => chosenPlan);
|
||||
stubChatChannel(chosenPlanStream: controller.stream);
|
||||
|
||||
await chatRoomCubit.initChat(null, orderId: orderId);
|
||||
final loaded = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||
expect(loaded.chatRoom.chosenPlanId, isNull);
|
||||
expect(loaded.chatRoom.chosenPlan?.id, planId);
|
||||
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: planId),
|
||||
).thenAnswer((_) async => updatedPlan);
|
||||
clearInteractions(mockChatRepository);
|
||||
|
||||
controller.add(_FakeChannelReadEvent());
|
||||
await Future<void>.delayed(const Duration(milliseconds: 350));
|
||||
|
||||
verify(mockChatRepository.getChosenPlan(id: planId)).called(1);
|
||||
final state = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||
expect(
|
||||
state.chatRoom.chosenPlan?.chosenPlanState,
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
|
||||
await controller.close();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'chosenPlanStateChangedStream - дебаунсит повторные события',
|
||||
() async {
|
||||
const uuid = 'room-uuid';
|
||||
const planId = 987;
|
||||
final chosenPlan = buildChosenPlan(
|
||||
planId,
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
);
|
||||
final updatedPlan = chosenPlan.copyWith(
|
||||
chosenPlanState: ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
final controller = StreamController<ChannelReadEvent>.broadcast();
|
||||
|
||||
when(
|
||||
mockChatRepository.getChatRoom(uuid),
|
||||
).thenAnswer((_) async => testChatRoom.copyWith(chosenPlanId: planId));
|
||||
when(
|
||||
mockChatRepository.getChatMessages(uuid, 1),
|
||||
).thenAnswer((_) async => testChatMessages);
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: planId),
|
||||
).thenAnswer((_) async => chosenPlan);
|
||||
stubChatChannel(chosenPlanStream: controller.stream);
|
||||
|
||||
await chatRoomCubit.initChat(uuid);
|
||||
expect(chatRoomCubit.state, isA<ChatRoomStateLoaded>());
|
||||
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: planId),
|
||||
).thenAnswer((_) async => updatedPlan);
|
||||
clearInteractions(mockChatRepository);
|
||||
|
||||
controller
|
||||
..add(_FakeChannelReadEvent())
|
||||
..add(_FakeChannelReadEvent());
|
||||
await Future<void>.delayed(const Duration(milliseconds: 350));
|
||||
|
||||
verify(mockChatRepository.getChosenPlan(id: planId)).called(1);
|
||||
final state = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||
expect(
|
||||
state.chatRoom.chosenPlan?.chosenPlanState,
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
|
||||
await controller.close();
|
||||
},
|
||||
);
|
||||
|
||||
test('choseStartPlanDate - устанавливает дату начала плана', () async {
|
||||
const uuid = 'room-uuid';
|
||||
const planId = 123;
|
||||
@@ -2578,3 +2792,8 @@ void main() {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeChannelReadEvent extends Fake implements ChannelReadEvent {
|
||||
@override
|
||||
dynamic get data => null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
import 'package:eatvio_app/features/chat/presentation/utils/chat_banner_resolver.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_progress.dart';
|
||||
import 'package:eatvio_app/features/consultations/utils/consultation_progress_mapper.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
ConsultationProgress _progress(
|
||||
ChosenPlanStateEnum state, {
|
||||
bool hasPersonalDiet = false,
|
||||
}) {
|
||||
return mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: hasPersonalDiet,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('resolveChatBanner — карта консультанта', () {
|
||||
test('in_coordination без карты просит добавить карту', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.stateInCoordination),
|
||||
amIConsultant: true,
|
||||
hasCard: false,
|
||||
state: ChosenPlanStateEnum.stateInCoordination,
|
||||
);
|
||||
|
||||
expect(banner, BannerType.card);
|
||||
});
|
||||
|
||||
test('in_coordination с картой просит выбрать дату', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.stateInCoordination),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.stateInCoordination,
|
||||
);
|
||||
|
||||
expect(banner, BannerType.date);
|
||||
});
|
||||
|
||||
test('paid + notProposed + диета просит предложить диету', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
hasPersonalDiet: true,
|
||||
),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
|
||||
expect(banner, BannerType.setPersonalDiet);
|
||||
});
|
||||
|
||||
test(
|
||||
'personal_diet_setted/started/in_progress + proposed просят сменить диету',
|
||||
() {
|
||||
const states = [
|
||||
ChosenPlanStateEnum.personalDietSet,
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
ChosenPlanStateEnum.stateInProgress,
|
||||
];
|
||||
|
||||
for (final state in states) {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(state, hasPersonalDiet: true),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: state,
|
||||
);
|
||||
|
||||
expect(
|
||||
banner,
|
||||
BannerType.changePersonalDiet,
|
||||
reason: 'state=${state.name}',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test('last_day_finished не просит менять диету (нет баннера)', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(
|
||||
ChosenPlanStateEnum.stateLastDayFinished,
|
||||
hasPersonalDiet: true,
|
||||
),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.stateLastDayFinished,
|
||||
);
|
||||
|
||||
expect(banner, isNot(BannerType.changePersonalDiet));
|
||||
expect(banner, isNull);
|
||||
});
|
||||
|
||||
test('done не просит менять диету (collectCharge)', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(
|
||||
ChosenPlanStateEnum.stateDone,
|
||||
hasPersonalDiet: true,
|
||||
),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.stateDone,
|
||||
);
|
||||
|
||||
expect(banner, isNot(BannerType.changePersonalDiet));
|
||||
expect(banner, BannerType.collectCharge);
|
||||
});
|
||||
});
|
||||
|
||||
group('resolveChatBanner — карта клиента', () {
|
||||
test('start_date_has_been_set просит оплатить', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.stateStartDateHasBeenSet),
|
||||
amIConsultant: false,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.stateStartDateHasBeenSet,
|
||||
);
|
||||
|
||||
expect(banner, BannerType.payment);
|
||||
});
|
||||
|
||||
test('pending_payment просит оплатить', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.statePendingPayment),
|
||||
amIConsultant: false,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.statePendingPayment,
|
||||
);
|
||||
|
||||
expect(banner, BannerType.payment);
|
||||
});
|
||||
|
||||
test('paid + notProposed не показывает diet-баннер клиенту', () {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
hasPersonalDiet: true,
|
||||
),
|
||||
amIConsultant: false,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.statePaid,
|
||||
);
|
||||
|
||||
expect(banner, isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'personal_diet_setted + proposed не показывает diet-баннер клиенту',
|
||||
() {
|
||||
final banner = resolveChatBanner(
|
||||
progress: _progress(
|
||||
ChosenPlanStateEnum.personalDietSet,
|
||||
hasPersonalDiet: true,
|
||||
),
|
||||
amIConsultant: false,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.personalDietSet,
|
||||
);
|
||||
|
||||
expect(banner, isNull);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('resolveChatBanner — терминальные фазы', () {
|
||||
test('canceled не показывает баннер', () {
|
||||
expect(
|
||||
resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.stateCanceledByTheInitiator),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.stateCanceledByTheInitiator,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.stateCanceledByTheInitiator),
|
||||
amIConsultant: false,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.stateCanceledByTheInitiator,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('payment_failed не показывает баннер', () {
|
||||
expect(
|
||||
resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.statePaymentFailed),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.statePaymentFailed,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.statePaymentFailed),
|
||||
amIConsultant: false,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.statePaymentFailed,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('нет progress или state — нет баннера', () {
|
||||
expect(
|
||||
resolveChatBanner(
|
||||
progress: null,
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: ChosenPlanStateEnum.statePaid,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
resolveChatBanner(
|
||||
progress: _progress(ChosenPlanStateEnum.statePaid),
|
||||
amIConsultant: true,
|
||||
hasCard: true,
|
||||
state: null,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/chat_consultation_header.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_progress_plank.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_progress.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/widgets/personal_diet_status_chip.dart';
|
||||
import 'package:eatvio_app/features/consultations/utils/consultation_progress_mapper.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_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Widget _wrap(Widget child) {
|
||||
return MaterialApp(
|
||||
theme: ThemeHelper().themeData(),
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [Locale('ru')],
|
||||
home: Scaffold(body: Center(child: child)),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('ChatConsultationHeader', () {
|
||||
testWidgets('вызывает onInfoTap по тапу на info', (tester) async {
|
||||
var tapped = 0;
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.stateInited,
|
||||
hasPersonalDiet: false,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_wrap(
|
||||
ChatConsultationHeader(
|
||||
progress: progress,
|
||||
showInfoButton: true,
|
||||
onInfoTap: () => tapped++,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.info_outline_rounded));
|
||||
await tester.pump();
|
||||
|
||||
expect(tapped, 1);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'при showProgressPlank=false и proposed видны заголовок и чип, плашки нет',
|
||||
(tester) async {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.personalDietSet,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
expect(progress.showProgressPlank, isFalse);
|
||||
expect(progress.dietStatus, PersonalDietStatus.proposed);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_wrap(
|
||||
ChatConsultationHeader(
|
||||
progress: progress,
|
||||
showInfoButton: false,
|
||||
onInfoTap: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final s = S.of(tester.element(find.byType(ChatConsultationHeader)));
|
||||
|
||||
expect(find.text(s.more_about_order), findsOneWidget);
|
||||
expect(find.byType(PersonalDietStatusChip), findsOneWidget);
|
||||
expect(find.text(s.personal_diet_proposed), findsOneWidget);
|
||||
expect(find.byType(ConsultationProgressPlank), findsNothing);
|
||||
expect(find.byIcon(Icons.check), findsNothing);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user