forked from eatvio/eatvio_app
Merge remote-tracking branch 'upstream/main' into issue-1123
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,86 @@
|
||||
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(
|
||||
'personal_diet_setted показывает плашку и чип вместе с баннером замены',
|
||||
(tester) async {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.personalDietSet,
|
||||
hasPersonalDiet: true,
|
||||
personalDietName: 'Индивидуальный план питания',
|
||||
);
|
||||
expect(progress.showProgressPlank, isTrue);
|
||||
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.order_in_progress), findsNWidgets(2));
|
||||
expect(find.byType(PersonalDietStatusChip), findsOneWidget);
|
||||
expect(
|
||||
find.text(s.personal_diet_proposed_named('Индивидуальный план питания')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(ConsultationProgressPlank), findsOneWidget);
|
||||
expect(find.byIcon(Icons.check), findsNWidgets(2));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -174,11 +174,13 @@ void main() {
|
||||
'consultant_id': 1,
|
||||
'diaries': [],
|
||||
'personal_diet_id': null,
|
||||
'personal_diet_name': 'Индивидуальный план питания',
|
||||
};
|
||||
|
||||
final model = ConsultationModel.fromJson(json);
|
||||
|
||||
expect(model.chatId, 'direct-chat-uuid');
|
||||
expect(model.personalDietName, 'Индивидуальный план питания');
|
||||
});
|
||||
|
||||
test('fromJson allows missing chat_id', () {
|
||||
|
||||
@@ -153,6 +153,29 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('shows personal diet name in status chip', (tester) async {
|
||||
final consultation = mockConsultation.copyWith(
|
||||
personalDietId: 1,
|
||||
personalDietName: 'Индивидуальный план питания',
|
||||
);
|
||||
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(
|
||||
wrap(ConsultationCardWidget(consultationsModel: consultation)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final s = S.of(tester.element(find.byType(ConsultationCardWidget)));
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
s.personal_diet_proposed_named('Индивидуальный план питания'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('renders diary days', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_progress_plank.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('ConsultationProgressPlank', () {
|
||||
testWidgets('не строит шаги при showProgressPlank=false', (tester) async {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.stateConfirmedByConsultant,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
expect(progress.showProgressPlank, isFalse);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_wrap(ConsultationProgressPlank(progress: progress)),
|
||||
);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ConsultationProgressPlank),
|
||||
matching: find.byType(SizedBox),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byIcon(Icons.check), findsNothing);
|
||||
expect(find.byType(Text), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'рисует 4 шага с иконками и подписями при showProgressPlank=true',
|
||||
(tester) async {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.stateDone,
|
||||
hasPersonalDiet: false,
|
||||
);
|
||||
expect(progress.showProgressPlank, isTrue);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_wrap(ConsultationProgressPlank(progress: progress)),
|
||||
);
|
||||
|
||||
final s = S.of(tester.element(find.byType(ConsultationProgressPlank)));
|
||||
|
||||
expect(find.byIcon(Icons.check), findsNWidgets(4));
|
||||
expect(find.text(s.startDateHasBeenSet), findsOneWidget);
|
||||
expect(find.text(s.order_paid), findsOneWidget);
|
||||
expect(find.text(s.order_in_progress), findsOneWidget);
|
||||
expect(find.text(s.done), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('подписи шагов центрированы под своими кружками', (
|
||||
tester,
|
||||
) async {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.stateDone,
|
||||
hasPersonalDiet: false,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_wrap(
|
||||
SizedBox(
|
||||
width: 360,
|
||||
child: ConsultationProgressPlank(progress: progress),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
for (var index = 0; index < 4; index++) {
|
||||
final circle = tester.getCenter(
|
||||
find.byKey(ValueKey('consultation_step_circle_$index')),
|
||||
);
|
||||
final label = tester.getCenter(
|
||||
find.byKey(ValueKey('consultation_step_label_$index')),
|
||||
);
|
||||
expect(label.dx, closeTo(circle.dx, 0.5));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import 'package:eatvio_app/core/theme/theme_helper.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)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _chip(
|
||||
ChosenPlanStateEnum state, {
|
||||
bool hasPersonalDiet = true,
|
||||
String? personalDietName,
|
||||
}) {
|
||||
return PersonalDietStatusChip(
|
||||
progress: mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: hasPersonalDiet,
|
||||
personalDietName: personalDietName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('PersonalDietStatusChip', () {
|
||||
testWidgets('notRequired + active ничего не показывает', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_wrap(_chip(ChosenPlanStateEnum.stateInited, hasPersonalDiet: false)),
|
||||
);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(PersonalDietStatusChip),
|
||||
matching: find.byType(SizedBox),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(Text), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('proposed показывает personal_diet_proposed', (tester) async {
|
||||
await tester.pumpWidget(_wrap(_chip(ChosenPlanStateEnum.stateStarted)));
|
||||
|
||||
final s = S.of(tester.element(find.byType(PersonalDietStatusChip)));
|
||||
|
||||
expect(find.text(s.personal_diet_proposed), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('proposed с названием показывает имя диеты', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_wrap(
|
||||
_chip(
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
personalDietName: 'Индивидуальный план питания',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final s = S.of(tester.element(find.byType(PersonalDietStatusChip)));
|
||||
|
||||
expect(
|
||||
find.text(s.personal_diet_proposed_named('Индивидуальный план питания')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('notProposed показывает personal_diet_not_proposed', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(_wrap(_chip(ChosenPlanStateEnum.statePaid)));
|
||||
|
||||
final s = S.of(tester.element(find.byType(PersonalDietStatusChip)));
|
||||
|
||||
expect(find.text(s.personal_diet_not_proposed), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('canceled показывает orderCanceled', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_wrap(_chip(ChosenPlanStateEnum.stateCanceledByTheInitiator)),
|
||||
);
|
||||
|
||||
final s = S.of(tester.element(find.byType(PersonalDietStatusChip)));
|
||||
|
||||
expect(find.text(s.orderCanceled), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('paymentFailed показывает payment_failed', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_wrap(_chip(ChosenPlanStateEnum.statePaymentFailed)),
|
||||
);
|
||||
|
||||
final s = S.of(tester.element(find.byType(PersonalDietStatusChip)));
|
||||
|
||||
expect(find.text(s.payment_failed), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
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';
|
||||
|
||||
typedef _Expected = ({
|
||||
Set<ConsultationStep> steps,
|
||||
bool plank,
|
||||
PersonalDietStatus diet,
|
||||
ConsultationPhase phase,
|
||||
});
|
||||
|
||||
const _startDate = ConsultationStep.startDate;
|
||||
const _paid = ConsultationStep.paid;
|
||||
const _inProgress = ConsultationStep.inProgress;
|
||||
const _finished = ConsultationStep.finished;
|
||||
|
||||
final Map<ChosenPlanStateEnum, _Expected> _expectations = {
|
||||
ChosenPlanStateEnum.stateInited: (
|
||||
steps: {},
|
||||
plank: false,
|
||||
diet: PersonalDietStatus.notProposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateStartDateHasBeenSet: (
|
||||
steps: {_startDate},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.notProposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateInCoordination: (
|
||||
steps: {},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.notProposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateStarted: (
|
||||
steps: {_startDate, _paid, _inProgress},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateInProgress: (
|
||||
steps: {_startDate, _paid, _inProgress},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.statePendingPayment: (
|
||||
steps: {_startDate},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.notProposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateConfirmedByConsultant: (
|
||||
steps: {},
|
||||
plank: false,
|
||||
diet: PersonalDietStatus.notProposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.statePaid: (
|
||||
steps: {_startDate, _paid},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.notProposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateCanceledByTheInitiator: (
|
||||
steps: {},
|
||||
plank: false,
|
||||
diet: PersonalDietStatus.notRequired,
|
||||
phase: ConsultationPhase.canceled,
|
||||
),
|
||||
ChosenPlanStateEnum.stateLastDayStarted: (
|
||||
steps: {_startDate, _paid, _inProgress},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateLastDayFinished: (
|
||||
steps: {_startDate, _paid, _inProgress},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateComplained: (
|
||||
steps: {_startDate, _paid, _inProgress},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateDone: (
|
||||
steps: {_startDate, _paid, _inProgress, _finished},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.personalDietSet: (
|
||||
steps: {_startDate, _paid},
|
||||
plank: true,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.stateChargeWasCollected: (
|
||||
steps: {_startDate, _paid, _inProgress, _finished},
|
||||
plank: false,
|
||||
diet: PersonalDietStatus.proposed,
|
||||
phase: ConsultationPhase.active,
|
||||
),
|
||||
ChosenPlanStateEnum.statePaymentFailed: (
|
||||
steps: {_startDate},
|
||||
plank: false,
|
||||
diet: PersonalDietStatus.notRequired,
|
||||
phase: ConsultationPhase.paymentFailed,
|
||||
),
|
||||
};
|
||||
|
||||
void main() {
|
||||
group('mapConsultationProgress — все состояния', () {
|
||||
test('таблица покрывает ровно все 16 состояний', () {
|
||||
expect(_expectations.length, 16);
|
||||
expect(_expectations.keys.toSet(), ChosenPlanStateEnum.values.toSet());
|
||||
});
|
||||
|
||||
for (final entry in _expectations.entries) {
|
||||
test('${entry.key.name} маппится во все поля ConsultationProgress', () {
|
||||
final progress = mapConsultationProgress(
|
||||
state: entry.key,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(progress.completedSteps, unorderedEquals(entry.value.steps));
|
||||
expect(progress.showProgressPlank, entry.value.plank);
|
||||
expect(progress.dietStatus, entry.value.diet);
|
||||
expect(progress.phase, entry.value.phase);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
group('mapConsultationProgress — ключевые сценарии', () {
|
||||
test('in_coordination показывает плашку без завершённых шагов', () {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.stateInCoordination,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(progress.showProgressPlank, isTrue);
|
||||
expect(progress.completedSteps, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'personal_diet_setted показывает плашку и завершает startDate+paid',
|
||||
() {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.personalDietSet,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(progress.showProgressPlank, isTrue);
|
||||
expect(progress.completedSteps, unorderedEquals([_startDate, _paid]));
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'started/in_progress/last_day_started/last_day_finished/complained — 3 шага',
|
||||
() {
|
||||
const states = [
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
ChosenPlanStateEnum.stateInProgress,
|
||||
ChosenPlanStateEnum.stateLastDayStarted,
|
||||
ChosenPlanStateEnum.stateLastDayFinished,
|
||||
ChosenPlanStateEnum.stateComplained,
|
||||
];
|
||||
|
||||
for (final state in states) {
|
||||
final progress = mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(
|
||||
progress.completedSteps,
|
||||
unorderedEquals([_startDate, _paid, _inProgress]),
|
||||
reason: 'state=${state.name}',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test('done/charge_was_collected — 4 шага', () {
|
||||
const states = [
|
||||
ChosenPlanStateEnum.stateDone,
|
||||
ChosenPlanStateEnum.stateChargeWasCollected,
|
||||
];
|
||||
|
||||
for (final state in states) {
|
||||
final progress = mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(
|
||||
progress.completedSteps,
|
||||
unorderedEquals([_startDate, _paid, _inProgress, _finished]),
|
||||
reason: 'state=${state.name}',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'canceled — фаза canceled и dietStatus notRequired даже при диете',
|
||||
() {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.stateCanceledByTheInitiator,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(progress.phase, ConsultationPhase.canceled);
|
||||
expect(progress.isCanceled, isTrue);
|
||||
expect(progress.dietStatus, PersonalDietStatus.notRequired);
|
||||
},
|
||||
);
|
||||
|
||||
test('payment_failed — фаза paymentFailed и только startDate', () {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.statePaymentFailed,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(progress.phase, ConsultationPhase.paymentFailed);
|
||||
expect(progress.isPaymentFailed, isTrue);
|
||||
expect(progress.completedSteps, unorderedEquals([_startDate]));
|
||||
});
|
||||
|
||||
test('hasPersonalDiet=false даёт notRequired для любого состояния', () {
|
||||
for (final state in ChosenPlanStateEnum.values) {
|
||||
final progress = mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: false,
|
||||
);
|
||||
|
||||
expect(
|
||||
progress.dietStatus,
|
||||
PersonalDietStatus.notRequired,
|
||||
reason: 'state=${state.name}',
|
||||
);
|
||||
expect(progress.hasDietChip, isFalse, reason: 'state=${state.name}');
|
||||
}
|
||||
});
|
||||
|
||||
test('paid с диетой даёт notProposed', () {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.statePaid,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(progress.dietStatus, PersonalDietStatus.notProposed);
|
||||
});
|
||||
|
||||
test('personal_diet_setted/started/proposed дают proposed', () {
|
||||
const states = [
|
||||
ChosenPlanStateEnum.personalDietSet,
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
];
|
||||
|
||||
for (final state in states) {
|
||||
final progress = mapConsultationProgress(
|
||||
state: state,
|
||||
hasPersonalDiet: true,
|
||||
);
|
||||
|
||||
expect(
|
||||
progress.dietStatus,
|
||||
PersonalDietStatus.proposed,
|
||||
reason: 'state=${state.name}',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('personalDietName передаётся в ConsultationProgress', () {
|
||||
final progress = mapConsultationProgress(
|
||||
state: ChosenPlanStateEnum.personalDietSet,
|
||||
hasPersonalDiet: true,
|
||||
personalDietName: 'Индивидуальный план питания',
|
||||
);
|
||||
|
||||
expect(progress.personalDietName, 'Индивидуальный план питания');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -27,9 +27,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
createTestWidget(
|
||||
const ChosenDietWidgetContent(
|
||||
currentDietDay: 5,
|
||||
dietName: 'Test Diet',
|
||||
dietDuration: 14,
|
||||
dietId: 1,
|
||||
),
|
||||
),
|
||||
@@ -38,15 +36,14 @@ void main() {
|
||||
|
||||
expect(find.byType(ChosenDietWidgetContent), findsOneWidget);
|
||||
expect(find.text('Test Diet'), findsOneWidget);
|
||||
expect(find.textContaining('из 14'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('displays current day and duration', (tester) async {
|
||||
testWidgets('does not display current day and duration', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
createTestWidget(
|
||||
const ChosenDietWidgetContent(
|
||||
currentDietDay: 7,
|
||||
dietName: 'Test Diet',
|
||||
dietDuration: 21,
|
||||
dietId: 1,
|
||||
),
|
||||
),
|
||||
@@ -54,15 +51,14 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ChosenDietWidgetContent), findsOneWidget);
|
||||
expect(find.textContaining('из 21'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('displays diet name with underline', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
createTestWidget(
|
||||
const ChosenDietWidgetContent(
|
||||
currentDietDay: 1,
|
||||
dietName: 'My Diet',
|
||||
dietDuration: 7,
|
||||
dietId: 1,
|
||||
),
|
||||
),
|
||||
@@ -70,6 +66,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('My Diet'), findsOneWidget);
|
||||
expect(find.textContaining('из 7'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,6 +100,17 @@ void main() {
|
||||
await tester.pumpWidget(wrap(const ChosenDietWidget(), cubit));
|
||||
await tester.pump();
|
||||
expect(find.text('Diet'), findsOneWidget);
|
||||
expect(find.textContaining('из 7'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('ChosenDietWidget hides diet day while plan keeps its day', (tester) async {
|
||||
final cubit = FakeDiaryCubit(loadedState(hasDiet: true, hasPlan: true));
|
||||
await tester.pumpWidget(wrap(const Column(children: [ChosenPlanWidget(), ChosenDietWidget()]), cubit));
|
||||
await tester.pump();
|
||||
expect(find.text('Plan'), findsOneWidget);
|
||||
expect(find.textContaining('из 30'), findsOneWidget);
|
||||
expect(find.text('Diet'), findsOneWidget);
|
||||
expect(find.textContaining('из 7'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('AddProductButtonWidget renders for loaded state', (tester) async {
|
||||
|
||||
@@ -92,6 +92,7 @@ void main() {
|
||||
DiaryReminderForUsersNotification.fromJson(base, NotificationType.diaryReminderForUsers);
|
||||
DiaryReminderForUsersNotification.fromJson({'created_at': c, 'data': <String, dynamic>{}}, NotificationType.diaryReminderForUsers);
|
||||
PersonalDietIsDefinedToCustomerNotification.fromJson(base, NotificationType.personalDietIsDefinedToCustomer);
|
||||
PersonalDietIsDefinedToAuthorNotification.fromJson(base, NotificationType.personalDietIsDefinedToAuthor);
|
||||
ConsultantEducationUpdatedNotification.fromJson(base, NotificationType.consultantEducationUpdated);
|
||||
ComplaintRegisteredNotification.fromJson(base, NotificationType.complaintRegistered);
|
||||
ContentUnpublishedNotification.fromJson(base, NotificationType.contentUnpublished);
|
||||
|
||||
@@ -50,6 +50,7 @@ void main() {
|
||||
'stay-on-track-to-customer': NotificationType.stayOnTrackToCustomer,
|
||||
'diary-reminder-for-users': NotificationType.diaryReminderForUsers,
|
||||
'personal-diet-is-defined-to-customer': NotificationType.personalDietIsDefinedToCustomer,
|
||||
'personal-diet-is-defined-to-author': NotificationType.personalDietIsDefinedToAuthor,
|
||||
};
|
||||
|
||||
cases.forEach((key, expected) {
|
||||
@@ -671,6 +672,34 @@ void main() {
|
||||
expect(notification.type, NotificationType.personalizedDietIsDefined);
|
||||
});
|
||||
|
||||
test('PersonalDietIsDefinedToAuthorNotification.fromJson parses all fields', () {
|
||||
final json = {
|
||||
'chosen_plan_id': 12,
|
||||
'diet_id': 34,
|
||||
'plan_name': 'Plan',
|
||||
'user_name': 'Client',
|
||||
'consultant_name': 'Consultant',
|
||||
'diet_name': 'Personal diet',
|
||||
'role': 'consultant',
|
||||
'peer_slug': 'client-slug',
|
||||
'peer_image_path': 'avatars/client.png',
|
||||
'created_at': '2024-01-30T12:00:00Z',
|
||||
};
|
||||
|
||||
final notification = PersonalDietIsDefinedToAuthorNotification.fromJson(
|
||||
json,
|
||||
NotificationType.personalDietIsDefinedToAuthor,
|
||||
isRead: false,
|
||||
);
|
||||
|
||||
expect(notification.chosenPlanId, 12);
|
||||
expect(notification.dietId, 34);
|
||||
expect(notification.userName, 'Client');
|
||||
expect(notification.dietName, 'Personal diet');
|
||||
expect(notification.peerSlug, 'client-slug');
|
||||
expect(notification.type, NotificationType.personalDietIsDefinedToAuthor);
|
||||
});
|
||||
|
||||
test('ChosenPlanFinishedToClientNotification.fromJson parses all fields', () {
|
||||
final json = {
|
||||
'plan_name': 'Finished Plan',
|
||||
|
||||
@@ -1212,6 +1212,89 @@ void main() {
|
||||
expect(find.byType(NotificationWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays PersonalDietIsDefinedToAuthorNotification', (
|
||||
tester,
|
||||
) async {
|
||||
final notification = PersonalDietIsDefinedToAuthorNotification(
|
||||
type: NotificationType.personalDietIsDefinedToAuthor,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
chosenPlanId: 1,
|
||||
dietId: 1,
|
||||
planName: 'Plan',
|
||||
userName: 'Client',
|
||||
consultantName: 'Consultant',
|
||||
dietName: 'Test Diet',
|
||||
role: 'consultant',
|
||||
peerSlug: 'client-slug',
|
||||
peerImagePath: '',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(notification));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.text('Вы предложили персональную диету пользователю Client, выбранная диета: «Test Diet»'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'displays PersonalDietIsDefinedToAuthorNotification with fallback user name',
|
||||
(tester) async {
|
||||
final notification = PersonalDietIsDefinedToAuthorNotification(
|
||||
type: NotificationType.personalDietIsDefinedToAuthor,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
chosenPlanId: 1,
|
||||
dietId: 1,
|
||||
planName: 'Plan',
|
||||
userName: '',
|
||||
consultantName: 'Consultant',
|
||||
dietName: 'Test Diet',
|
||||
role: 'consultant',
|
||||
peerSlug: 'client-slug',
|
||||
peerImagePath: '',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(notification));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.text('Вы предложили персональную диету пользователю Пользователь, выбранная диета: «Test Diet»'),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'displays PersonalDietIsDefinedToAuthorNotification without diet name',
|
||||
(tester) async {
|
||||
final notification = PersonalDietIsDefinedToAuthorNotification(
|
||||
type: NotificationType.personalDietIsDefinedToAuthor,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
chosenPlanId: 1,
|
||||
dietId: 1,
|
||||
planName: 'Plan',
|
||||
userName: 'Client',
|
||||
consultantName: 'Consultant',
|
||||
dietName: '',
|
||||
role: 'consultant',
|
||||
peerSlug: 'client-slug',
|
||||
peerImagePath: '',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(notification));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.text('Вы предложили персональную диету пользователю Client'),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('displays UnknownNotification', (tester) async {
|
||||
final notification = UnknownNotification(
|
||||
type: NotificationType.unknown,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/notifications/models/notification_model.dart';
|
||||
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
@@ -1454,6 +1455,42 @@ void main() {
|
||||
expect(result, isNotNull);
|
||||
expect(result!['notifications'], isA<List>());
|
||||
expect(result['notifications'].length, 1);
|
||||
expect(result['notifications'].first, isA<PersonalDietIsDefinedToCustomerNotification>());
|
||||
});
|
||||
|
||||
test('getNotifications - обрабатывает PersonalDietIsDefinedToAuthor', () async {
|
||||
const page = 1;
|
||||
final response = Response(
|
||||
data: {
|
||||
'data': [
|
||||
{
|
||||
'type': 'personal-diet-is-defined-to-author',
|
||||
'data': {
|
||||
'diet_name': 'Test Diet',
|
||||
'diet_id': 1,
|
||||
'user_name': 'Client',
|
||||
},
|
||||
'read_at': null,
|
||||
'created_at': '2024-01-01T12:00:00Z',
|
||||
},
|
||||
],
|
||||
'current_page': 1,
|
||||
'last_page': 1,
|
||||
'per_page': 10,
|
||||
'total': 1,
|
||||
},
|
||||
statusCode: 200,
|
||||
requestOptions: RequestOptions(path: '/api/profile/notifications'),
|
||||
);
|
||||
|
||||
when(mockDio.get(any)).thenAnswer((_) async => response);
|
||||
|
||||
final result = await notificationsService.getNotifications(page: page);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!['notifications'], isA<List>());
|
||||
expect(result['notifications'].length, 1);
|
||||
expect(result['notifications'].first, isA<PersonalDietIsDefinedToAuthorNotification>());
|
||||
});
|
||||
|
||||
test('getNotifications - обрабатывает UserDailyReportCompleted', () async {
|
||||
|
||||
@@ -243,12 +243,12 @@ void main() {
|
||||
expect(find.byType(InfoByOrderScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
ChosenPlanModel donePlan() {
|
||||
ChosenPlanModel donePlan({ChosenPlanStateEnum state = ChosenPlanStateEnum.stateDone}) {
|
||||
return ChosenPlanModel(
|
||||
id: 1,
|
||||
optionType: 'options1',
|
||||
startDate: DateTime(2024, 1, 1),
|
||||
chosenPlanState: ChosenPlanStateEnum.stateDone,
|
||||
chosenPlanState: state,
|
||||
chatId: 'chat-1',
|
||||
consultantId: 1,
|
||||
plan: const PlanModel(
|
||||
@@ -320,6 +320,7 @@ void main() {
|
||||
|
||||
expect(find.byType(CollectChargeBanner), findsOneWidget);
|
||||
expect(find.text('Получить выплату'), findsOneWidget);
|
||||
expect(find.text('Отменить консультацию'), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -337,6 +338,22 @@ void main() {
|
||||
|
||||
expect(find.byType(CollectChargeBanner), findsNothing);
|
||||
expect(find.text('Получить выплату'), findsNothing);
|
||||
expect(find.text('Отменить консультацию'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows cancel action for cancellable post-paid plan', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
when(
|
||||
mockChatRepository.getChosenPlan(id: anyNamed('id')),
|
||||
).thenAnswer((_) async => donePlan(state: ChosenPlanStateEnum.stateStarted));
|
||||
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(createTestWidget(planId: 1));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
expect(find.text('Отменить консультацию'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ void main() {
|
||||
expect(cubit.state.consultationsWithDiet, isEmpty);
|
||||
expect(cubit.state.consultationsWithoutDiet, isEmpty);
|
||||
expect(cubit.state.dietName, isEmpty);
|
||||
expect(cubit.state.dietDuration, 0);
|
||||
expect(cubit.state.isLoading, isFalse);
|
||||
expect(cubit.state.error, isNull);
|
||||
});
|
||||
@@ -71,6 +72,7 @@ void main() {
|
||||
await cubit.setDietName(dietId);
|
||||
|
||||
expect(cubit.state.dietName, equals(dietName));
|
||||
expect(cubit.state.dietDuration, 30);
|
||||
expect(cubit.state.isLoading, isFalse);
|
||||
verify(mockDietRepository.getDiet(dietId)).called(1);
|
||||
});
|
||||
@@ -161,6 +163,7 @@ void main() {
|
||||
await cubit.getConsultationWithPersonalDiets(dietId);
|
||||
|
||||
expect(cubit.state.dietName, equals(dietName));
|
||||
expect(cubit.state.dietDuration, 30);
|
||||
expect(cubit.state.consultationsWithDiet.length, 1);
|
||||
expect(cubit.state.consultationsWithoutDiet.length, 1);
|
||||
expect(cubit.state.isLoading, isFalse);
|
||||
@@ -216,6 +219,8 @@ void main() {
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).called(1);
|
||||
verify(mockPersonalDietSuggestionRepository.getConsultationWithPersonalDiets()).called(2);
|
||||
verify(mockDietRepository.getDiet(dietId)).called(2);
|
||||
});
|
||||
|
||||
test('reattachDietToConsultation - успешное переприкрепление диеты', () async {
|
||||
@@ -265,6 +270,8 @@ void main() {
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).called(1);
|
||||
verify(mockPersonalDietSuggestionRepository.getConsultationWithPersonalDiets()).called(2);
|
||||
verify(mockDietRepository.getDiet(dietId)).called(2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ void main() {
|
||||
expect(state.consultationsWithDiet, isEmpty);
|
||||
expect(state.consultationsWithoutDiet, isEmpty);
|
||||
expect(state.dietName, '');
|
||||
expect(state.dietDuration, 0);
|
||||
expect(state.isLoading, isFalse);
|
||||
expect(state.error, isNull);
|
||||
});
|
||||
@@ -16,6 +17,7 @@ void main() {
|
||||
test('creates instance with custom values', () {
|
||||
final state = PersonalDietSuggestionState(
|
||||
dietName: 'Test Diet',
|
||||
dietDuration: 14,
|
||||
isLoading: true,
|
||||
error: 'Test error',
|
||||
);
|
||||
@@ -23,6 +25,7 @@ void main() {
|
||||
expect(state.consultationsWithDiet, isEmpty);
|
||||
expect(state.consultationsWithoutDiet, isEmpty);
|
||||
expect(state.dietName, 'Test Diet');
|
||||
expect(state.dietDuration, 14);
|
||||
expect(state.isLoading, isTrue);
|
||||
expect(state.error, 'Test error');
|
||||
});
|
||||
@@ -31,14 +34,15 @@ void main() {
|
||||
final original = PersonalDietSuggestionState();
|
||||
final updated = original.copyWith(
|
||||
dietName: 'New Diet',
|
||||
dietDuration: 7,
|
||||
isLoading: true,
|
||||
);
|
||||
|
||||
expect(updated.dietName, 'New Diet');
|
||||
expect(updated.dietDuration, 7);
|
||||
expect(updated.isLoading, isTrue);
|
||||
expect(updated.consultationsWithDiet, original.consultationsWithDiet);
|
||||
expect(updated.error, original.error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/app_config.dart';
|
||||
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/core/utils/localization_utils.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
||||
@@ -34,6 +35,7 @@ void main() {
|
||||
|
||||
final mockConsultation = ConsultationModel(
|
||||
id: 1,
|
||||
chatId: 'chat-1',
|
||||
chosenPlanState: ChosenPlanStateEnum.stateInProgress,
|
||||
planTitle: 'Test Plan',
|
||||
userName: 'Test User',
|
||||
@@ -68,6 +70,32 @@ void main() {
|
||||
path: '/personal-diet-suggestion',
|
||||
builder: (context, state) => const Scaffold(body: Text('Personal Diet')),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat-select-screen',
|
||||
name: 'chatSelectScreen',
|
||||
builder: (context, state) => const Scaffold(body: Text('Chats')),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat-screen',
|
||||
name: 'chatScreen',
|
||||
builder: (context, state) {
|
||||
final uuid = state.uri.queryParameters['uuid'];
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
GoRouter.of(context).pop();
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).go('/chat-select-screen');
|
||||
},
|
||||
),
|
||||
),
|
||||
body: Text('Chat $uuid'),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -201,7 +229,7 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('shows success bottom sheet after successful diet attachment', (tester) async {
|
||||
testWidgets('opens chat after successful diet attachment', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final cubit = MockPersonalDietSuggestionCubit();
|
||||
|
||||
@@ -226,7 +254,99 @@ void main() {
|
||||
await tester.tap(offerButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('«Test Diet» предложена пользователю Test User'), findsOneWidget);
|
||||
expect(find.text('Chat chat-1'), findsOneWidget);
|
||||
verify(cubit.attachDietToConsultation(consultationId: 1)).called(1);
|
||||
|
||||
await tester.tap(find.byType(BackButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Chats'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('opens chat after successful diet reattachment', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final cubit = MockPersonalDietSuggestionCubit();
|
||||
final consultationWithDiet = ConsultationModel(
|
||||
id: 1,
|
||||
chatId: 'chat-2',
|
||||
chosenPlanState: ChosenPlanStateEnum.stateInProgress,
|
||||
planTitle: 'Test Plan',
|
||||
userName: 'Test User',
|
||||
userSlug: 'test-user',
|
||||
consultantPayment: 1000.0,
|
||||
duration: 7,
|
||||
currentDiaryDay: 3,
|
||||
userProfilePhotoUrl: '/avatar.jpg',
|
||||
consultationOptionPrice: 500,
|
||||
startDate: DateTime(2025, 1, 1),
|
||||
endDate: DateTime(2025, 1, 8),
|
||||
consultantId: 1,
|
||||
diaries: [],
|
||||
personalDietId: 1,
|
||||
);
|
||||
|
||||
when(cubit.state).thenReturn(PersonalDietSuggestionState());
|
||||
when(cubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(cubit.reattachDietToConsultation(consultationId: 1)).thenAnswer((_) async => true);
|
||||
|
||||
await tester.pumpWidget(
|
||||
BlocProvider<PersonalDietSuggestionCubit>.value(
|
||||
value: cubit,
|
||||
child: wrap(ConsultationWidget(consultationModel: consultationWithDiet, dietName: 'Test Diet')),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final changeButton = find.text(S.of(tester.element(find.byType(ConsultationWidget))).changeDiet);
|
||||
await tester.tap(changeButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final offerButton = find.text(S.of(tester.element(find.byType(ConsultationWidget))).toOffer);
|
||||
await tester.tap(offerButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Chat chat-2'), findsOneWidget);
|
||||
verify(cubit.reattachDietToConsultation(consultationId: 1)).called(1);
|
||||
verifyNever(cubit.attachDietToConsultation(consultationId: 1));
|
||||
|
||||
await tester.tap(find.byType(BackButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Chats'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('stays on screen when chat id is missing after success', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final cubit = MockPersonalDietSuggestionCubit();
|
||||
|
||||
when(cubit.state).thenReturn(PersonalDietSuggestionState());
|
||||
when(cubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(cubit.attachDietToConsultation(consultationId: 1)).thenAnswer((_) async => true);
|
||||
|
||||
await tester.pumpWidget(
|
||||
BlocProvider<PersonalDietSuggestionCubit>.value(
|
||||
value: cubit,
|
||||
child: wrap(
|
||||
ConsultationWidget(
|
||||
consultationModel: mockConsultation.copyWith(chatId: null),
|
||||
dietName: 'Test Diet',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Предложить диету'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(S.of(tester.element(find.byType(ConsultationWidget))).toOffer));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Chat '), findsNothing);
|
||||
expect(find.text('Test User'), findsOneWidget);
|
||||
verify(cubit.attachDietToConsultation(consultationId: 1)).called(1);
|
||||
});
|
||||
});
|
||||
@@ -279,7 +399,7 @@ void main() {
|
||||
|
||||
expect(find.text('Test User'), findsOneWidget);
|
||||
expect(find.text('Test Plan'), findsOneWidget);
|
||||
expect(find.text('Старт: Test User'), findsOneWidget);
|
||||
expect(find.text('Старт: 01.01.2025'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -318,7 +438,71 @@ void main() {
|
||||
|
||||
expect(find.text('Test User'), findsOneWidget);
|
||||
expect(find.text('Test Plan'), findsOneWidget);
|
||||
expect(find.text('Старт: Test User'), findsNothing);
|
||||
expect(find.text('Старт: 01.01.2025'), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('disables CTA when plan duration differs from diet duration', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final cubit = MockPersonalDietSuggestionCubit();
|
||||
final consultation = ConsultationModel(
|
||||
id: 1,
|
||||
chosenPlanState: ChosenPlanStateEnum.stateInProgress,
|
||||
planTitle: 'Test Plan',
|
||||
userName: 'Test User',
|
||||
userSlug: 'test-user',
|
||||
consultantPayment: 1000.0,
|
||||
duration: 14,
|
||||
currentDiaryDay: 3,
|
||||
userProfilePhotoUrl: '/avatar.jpg',
|
||||
consultationOptionPrice: 500,
|
||||
startDate: DateTime(2025, 1, 1),
|
||||
endDate: DateTime(2025, 1, 15),
|
||||
consultantId: 1,
|
||||
diaries: [],
|
||||
personalDietId: null,
|
||||
);
|
||||
|
||||
when(cubit.state).thenReturn(PersonalDietSuggestionState());
|
||||
when(cubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
await tester.pumpWidget(
|
||||
BlocProvider<PersonalDietSuggestionCubit>.value(
|
||||
value: cubit,
|
||||
child: wrap(
|
||||
ConsultationWidget(
|
||||
consultationModel: consultation,
|
||||
dietName: 'Test Diet',
|
||||
dietDuration: 7,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final s = S.of(tester.element(find.byType(ConsultationWidget)));
|
||||
expect(
|
||||
find.text(
|
||||
s.personal_diet_duration_mismatch(
|
||||
7,
|
||||
getDayForm(7, s),
|
||||
14,
|
||||
getDayForm(14, s),
|
||||
),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
final button = tester.widget<OutlinedButton>(find.byType(OutlinedButton));
|
||||
expect(button.onPressed, isNull);
|
||||
expect(
|
||||
button.style?.backgroundColor?.resolve({WidgetState.disabled}),
|
||||
appTheme.dividerColor,
|
||||
);
|
||||
expect(
|
||||
tester.widget<Text>(find.text(s.suggestDiet)).style?.color,
|
||||
appTheme.gray,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/app_config.dart';
|
||||
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/core/utils/localization_utils.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_state.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart';
|
||||
@@ -84,5 +85,31 @@ void main() {
|
||||
|
||||
expect(find.textContaining('Test Diet Name'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows diet duration hint and empty state', (tester) async {
|
||||
final cubit = MockPersonalDietSuggestionCubit();
|
||||
const dietDuration = 7;
|
||||
final state = PersonalDietSuggestionState(
|
||||
isLoading: false,
|
||||
dietName: 'Test Diet',
|
||||
dietDuration: dietDuration,
|
||||
consultationsWithDiet: [],
|
||||
consultationsWithoutDiet: [],
|
||||
);
|
||||
|
||||
when(cubit.state).thenReturn(state);
|
||||
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
await tester.pumpWidget(wrap(const PersonalDietSuggestionScreenContent(), cubit));
|
||||
await tester.pump();
|
||||
|
||||
final s = S.of(tester.element(find.byType(PersonalDietSuggestionScreenContent)));
|
||||
expect(
|
||||
find.text(s.personal_diet_duration_hint(dietDuration, getDayForm(dietDuration, s))),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text(s.no_active_personal_diet_consultations), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,41 +68,33 @@ void main() {
|
||||
test('attachPersonalDiet - успешное прикрепление диеты', () async {
|
||||
const consultationId = 1;
|
||||
const dietId = 2;
|
||||
final body = {'diet_id': dietId};
|
||||
|
||||
when(mockService.attachPersonalDiet(
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).thenAnswer((_) async => {});
|
||||
when(mockService.attachPersonalDiet(consultationId, body))
|
||||
.thenAnswer((_) async => {});
|
||||
|
||||
await repository.attachPersonalDiet(
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
);
|
||||
|
||||
verify(mockService.attachPersonalDiet(
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).called(1);
|
||||
verify(mockService.attachPersonalDiet(consultationId, body)).called(1);
|
||||
});
|
||||
|
||||
test('reattachPersonalDiet - успешное переприкрепление диеты', () async {
|
||||
const consultationId = 1;
|
||||
const dietId = 2;
|
||||
final body = {'diet_id': dietId};
|
||||
|
||||
when(mockService.reattachPersonalDiet(
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).thenAnswer((_) async => {});
|
||||
when(mockService.reattachPersonalDiet(consultationId, body))
|
||||
.thenAnswer((_) async => {});
|
||||
|
||||
await repository.reattachPersonalDiet(
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
);
|
||||
|
||||
verify(mockService.reattachPersonalDiet(
|
||||
consultationId: consultationId,
|
||||
dietId: dietId,
|
||||
)).called(1);
|
||||
verify(mockService.reattachPersonalDiet(consultationId, body)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,55 +15,48 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
mockDio = MockDio();
|
||||
when(mockDio.options).thenReturn(BaseOptions(baseUrl: 'http://localhost'));
|
||||
service = PersonalDietSuggestionService(mockDio);
|
||||
});
|
||||
|
||||
test('getConsultationWithPersonalDiets - успешное получение консультаций', () async {
|
||||
final mockData = {
|
||||
'data': [],
|
||||
'links': {},
|
||||
'meta': {
|
||||
'current_page': 1,
|
||||
'last_page': 1,
|
||||
'total': 0,
|
||||
'per_page': 10,
|
||||
},
|
||||
};
|
||||
late RequestOptions capturedRequest;
|
||||
|
||||
when(mockDio.get(
|
||||
'/api/consultations',
|
||||
queryParameters: anyNamed('queryParameters'),
|
||||
)).thenAnswer((_) async => Response(
|
||||
requestOptions: RequestOptions(path: '/api/consultations'),
|
||||
statusCode: 200,
|
||||
data: mockData,
|
||||
));
|
||||
when(mockDio.fetch<Map<String, dynamic>>(any)).thenAnswer((invocation) async {
|
||||
capturedRequest = invocation.positionalArguments.first as RequestOptions;
|
||||
return Response<Map<String, dynamic>>(
|
||||
requestOptions: capturedRequest,
|
||||
statusCode: 200,
|
||||
data: mockData,
|
||||
);
|
||||
});
|
||||
|
||||
final result = await service.getConsultationWithPersonalDiets();
|
||||
|
||||
expect(result, isA<ConsultationPaginationModel>());
|
||||
verify(mockDio.get(
|
||||
'/api/consultations',
|
||||
queryParameters: {
|
||||
'filters[plan][has_personal_diet][\$personal_diet]': '1',
|
||||
'filters[last_state][\$notIn][]': [
|
||||
'done',
|
||||
'charge_was_collected',
|
||||
'complained',
|
||||
'last_day_finished',
|
||||
'canceled_by_the_initiator',
|
||||
'inited'
|
||||
],
|
||||
'sort[]': 'start_date:desc',
|
||||
},
|
||||
)).called(1);
|
||||
expect(capturedRequest.path, '/api/consultations');
|
||||
expect(capturedRequest.method, 'GET');
|
||||
expect(capturedRequest.queryParameters, {
|
||||
r'filters[plan][has_personal_diet][$personal_diet]': '1',
|
||||
r'filters[last_state][$notIn][]': [
|
||||
'done',
|
||||
'charge_was_collected',
|
||||
'complained',
|
||||
'last_day_finished',
|
||||
'last_day_started',
|
||||
'canceled_by_the_initiator',
|
||||
'inited',
|
||||
],
|
||||
r'sort[]': ['start_date:desc'],
|
||||
});
|
||||
verify(mockDio.fetch<Map<String, dynamic>>(any)).called(1);
|
||||
});
|
||||
|
||||
test('getConsultationWithPersonalDiets - пробрасывает ошибку', () async {
|
||||
when(mockDio.get(
|
||||
any,
|
||||
queryParameters: anyNamed('queryParameters'),
|
||||
)).thenThrow(DioException(
|
||||
when(mockDio.fetch<Map<String, dynamic>>(any)).thenThrow(DioException(
|
||||
requestOptions: RequestOptions(path: '/api/consultations'),
|
||||
));
|
||||
|
||||
@@ -75,74 +68,70 @@ void main() {
|
||||
|
||||
test('attachPersonalDiet - успешное прикрепление диеты', () async {
|
||||
const consultationId = 1;
|
||||
const dietId = 2;
|
||||
final body = {'diet_id': 2};
|
||||
late RequestOptions capturedRequest;
|
||||
|
||||
when(mockDio.post(
|
||||
'/api/chosen_plans/$consultationId/attach_diet',
|
||||
data: anyNamed('data'),
|
||||
)).thenAnswer((_) async => Response(
|
||||
requestOptions: RequestOptions(path: '/api/chosen_plans/$consultationId/attach_diet'),
|
||||
statusCode: 200,
|
||||
));
|
||||
when(mockDio.fetch<void>(any)).thenAnswer((invocation) async {
|
||||
capturedRequest = invocation.positionalArguments.first as RequestOptions;
|
||||
return Response<void>(
|
||||
requestOptions: capturedRequest,
|
||||
statusCode: 200,
|
||||
);
|
||||
});
|
||||
|
||||
await service.attachPersonalDiet(consultationId: consultationId, dietId: dietId);
|
||||
await service.attachPersonalDiet(consultationId, body);
|
||||
|
||||
verify(mockDio.post(
|
||||
'/api/chosen_plans/$consultationId/attach_diet',
|
||||
data: {'diet_id': dietId},
|
||||
)).called(1);
|
||||
expect(capturedRequest.path, '/api/chosen_plans/$consultationId/attach_diet');
|
||||
expect(capturedRequest.method, 'POST');
|
||||
expect(capturedRequest.data, body);
|
||||
verify(mockDio.fetch<void>(any)).called(1);
|
||||
});
|
||||
|
||||
test('attachPersonalDiet - пробрасывает ошибку', () async {
|
||||
const consultationId = 1;
|
||||
const dietId = 2;
|
||||
final body = {'diet_id': 2};
|
||||
|
||||
when(mockDio.post(
|
||||
any,
|
||||
data: anyNamed('data'),
|
||||
)).thenThrow(DioException(
|
||||
when(mockDio.fetch<void>(any)).thenThrow(DioException(
|
||||
requestOptions: RequestOptions(path: '/api/chosen_plans/$consultationId/attach_diet'),
|
||||
));
|
||||
|
||||
expect(
|
||||
() => service.attachPersonalDiet(consultationId: consultationId, dietId: dietId),
|
||||
() => service.attachPersonalDiet(consultationId, body),
|
||||
throwsA(isA<DioException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('reattachPersonalDiet - успешное повторное прикрепление диеты', () async {
|
||||
const consultationId = 1;
|
||||
const dietId = 3;
|
||||
final body = {'diet_id': 3};
|
||||
late RequestOptions capturedRequest;
|
||||
|
||||
when(mockDio.post(
|
||||
'/api/chosen_plans/$consultationId/re_attach_diet',
|
||||
data: anyNamed('data'),
|
||||
)).thenAnswer((_) async => Response(
|
||||
requestOptions: RequestOptions(path: '/api/chosen_plans/$consultationId/re_attach_diet'),
|
||||
statusCode: 200,
|
||||
));
|
||||
when(mockDio.fetch<void>(any)).thenAnswer((invocation) async {
|
||||
capturedRequest = invocation.positionalArguments.first as RequestOptions;
|
||||
return Response<void>(
|
||||
requestOptions: capturedRequest,
|
||||
statusCode: 200,
|
||||
);
|
||||
});
|
||||
|
||||
await service.reattachPersonalDiet(consultationId: consultationId, dietId: dietId);
|
||||
await service.reattachPersonalDiet(consultationId, body);
|
||||
|
||||
verify(mockDio.post(
|
||||
'/api/chosen_plans/$consultationId/re_attach_diet',
|
||||
data: {'diet_id': dietId},
|
||||
)).called(1);
|
||||
expect(capturedRequest.path, '/api/chosen_plans/$consultationId/re_attach_diet');
|
||||
expect(capturedRequest.method, 'POST');
|
||||
expect(capturedRequest.data, body);
|
||||
verify(mockDio.fetch<void>(any)).called(1);
|
||||
});
|
||||
|
||||
test('reattachPersonalDiet - пробрасывает ошибку', () async {
|
||||
const consultationId = 1;
|
||||
const dietId = 3;
|
||||
final body = {'diet_id': 3};
|
||||
|
||||
when(mockDio.post(
|
||||
any,
|
||||
data: anyNamed('data'),
|
||||
)).thenThrow(DioException(
|
||||
when(mockDio.fetch<void>(any)).thenThrow(DioException(
|
||||
requestOptions: RequestOptions(path: '/api/chosen_plans/$consultationId/re_attach_diet'),
|
||||
));
|
||||
|
||||
expect(
|
||||
() => service.reattachPersonalDiet(consultationId: consultationId, dietId: dietId),
|
||||
() => service.reattachPersonalDiet(consultationId, body),
|
||||
throwsA(isA<DioException>()),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -61,6 +61,47 @@ void main() {
|
||||
expect(ChosenPlanStateEnum.stateConfirmedByConsultant.isStateAfterPaidState(), isTrue);
|
||||
});
|
||||
|
||||
test('canShowCancelAction returns true for cancellable states', () {
|
||||
expect(ChosenPlanStateEnum.stateInited.canShowCancelAction, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateInCoordination.canShowCancelAction, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateStartDateHasBeenSet.canShowCancelAction, isTrue);
|
||||
expect(ChosenPlanStateEnum.statePendingPayment.canShowCancelAction, isTrue);
|
||||
expect(ChosenPlanStateEnum.statePaid.canShowCancelAction, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateStarted.canShowCancelAction, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateInProgress.canShowCancelAction, isTrue);
|
||||
});
|
||||
|
||||
test('canShowCancelAction returns false for non-cancellable states', () {
|
||||
expect(ChosenPlanStateEnum.personalDietSet.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateLastDayStarted.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateLastDayFinished.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateComplained.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateDone.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateChargeWasCollected.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateCanceledByTheInitiator.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.statePaymentFailed.canShowCancelAction, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateConfirmedByConsultant.canShowCancelAction, isFalse);
|
||||
});
|
||||
|
||||
test('isCancelAfterPaidState returns true only for post-paid cancellable states', () {
|
||||
expect(ChosenPlanStateEnum.statePaid.isCancelAfterPaidState, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateStarted.isCancelAfterPaidState, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateInProgress.isCancelAfterPaidState, isTrue);
|
||||
expect(ChosenPlanStateEnum.stateInited.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateInCoordination.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateStartDateHasBeenSet.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.statePendingPayment.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.statePaymentFailed.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateConfirmedByConsultant.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.personalDietSet.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateLastDayStarted.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateLastDayFinished.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateComplained.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateDone.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateChargeWasCollected.isCancelAfterPaidState, isFalse);
|
||||
expect(ChosenPlanStateEnum.stateCanceledByTheInitiator.isCancelAfterPaidState, isFalse);
|
||||
});
|
||||
|
||||
test('getActiveStates returns correct states', () {
|
||||
final activeStates = ChosenPlanStateEnum.getActiveStates();
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ void main() {
|
||||
'last_state': 'initied',
|
||||
'chat_id': 'chat-1',
|
||||
'consultant_id': 1,
|
||||
'personal_diet_name': 'Индивидуальный план питания',
|
||||
'plan': {
|
||||
'id': 1,
|
||||
'slug': 'test-plan',
|
||||
@@ -94,6 +95,7 @@ void main() {
|
||||
expect(chosenPlan.optionType, 'options1');
|
||||
expect(chosenPlan.chosenPlanState, ChosenPlanStateEnum.stateInited);
|
||||
expect(chosenPlan.chatId, 'chat-1');
|
||||
expect(chosenPlan.personalDietName, 'Индивидуальный план питания');
|
||||
});
|
||||
|
||||
test('toJson serializes ChosenPlanModel correctly', () {
|
||||
@@ -198,6 +200,66 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('chosenOption', () {
|
||||
const plan = 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,
|
||||
packet1Options: PacketOptions(items: ['one'], price: 100, short: 'packet-1', hasPersonalDiet: false),
|
||||
packet2Options: PacketOptions(items: ['two'], price: 200, short: 'packet-2', hasPersonalDiet: true),
|
||||
packet3Options: PacketOptions(items: ['three'], price: 300, short: 'packet-3', hasPersonalDiet: false),
|
||||
author: Author(
|
||||
authorSlug: 'author',
|
||||
authorName: 'Author Name',
|
||||
authorProfileIcon: 'https://example.com/icon.jpg',
|
||||
authorIsMe: false,
|
||||
isEntitySubscribedByMe: null,
|
||||
authorPlaceInRating: null,
|
||||
),
|
||||
totalLikes: 10,
|
||||
totalViews: 100,
|
||||
totalComments: 5,
|
||||
isFavorite: false,
|
||||
minPriceOfOptions: 100,
|
||||
maxPriceOfOptions: 300,
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
|
||||
ChosenPlanModel modelWith(String optionType) => ChosenPlanModel(
|
||||
id: 1,
|
||||
optionType: optionType,
|
||||
startDate: DateTime(2024, 1, 1),
|
||||
chosenPlanState: ChosenPlanStateEnum.stateInited,
|
||||
chatId: 'chat-1',
|
||||
consultantId: 1,
|
||||
plan: plan,
|
||||
);
|
||||
|
||||
test('возвращает первую опцию для options1', () {
|
||||
expect(modelWith('options1').chosenOption?.short, 'packet-1');
|
||||
});
|
||||
|
||||
test('возвращает вторую опцию для options2', () {
|
||||
expect(modelWith('options2').chosenOption?.short, 'packet-2');
|
||||
});
|
||||
|
||||
test('возвращает третью опцию для options3', () {
|
||||
expect(modelWith('options3').chosenOption?.short, 'packet-3');
|
||||
});
|
||||
|
||||
test('возвращает null для неизвестного значения', () {
|
||||
expect(modelWith('unknown').chosenOption, isNull);
|
||||
expect(modelWith('1').chosenOption, isNull);
|
||||
expect(modelWith('').chosenOption, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('parseChosenPlan', () {
|
||||
test('parseChosenPlan создает ChosenPlanModel из Map', () {
|
||||
final json = {
|
||||
@@ -207,6 +269,7 @@ void main() {
|
||||
'last_state': 'initied',
|
||||
'chat_id': 'chat-1',
|
||||
'consultant_id': 1,
|
||||
'personal_diet_name': 'Индивидуальный план питания',
|
||||
'plan': {
|
||||
'id': 1,
|
||||
'slug': 'test-plan',
|
||||
|
||||
Reference in New Issue
Block a user