feat(tests): test coverage 60%

This commit is contained in:
misha
2025-12-23 10:59:56 +03:00
parent 38118055a5
commit 5b705dfb72
67 changed files with 8391 additions and 42 deletions

View File

@@ -0,0 +1,107 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/add_product_modal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
group('AddProductModal smoke', () {
DiaryState makeLoadedState({required bool withPortion}) {
const nutrition = NutritionDataModel(
dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
dailyNutritionRecommendation: NutritionModel(energy: 1, proteins: 1, fats: 1, carbohydrates: 1),
);
final diary = DiaryDayEntity(
id: 1,
date: '2025-01-01',
wakeup: '06:30:00',
retire: '22:30:00',
closed: 0,
comment: '',
typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: false, diaryState: null),
meals: [
MealModel(
id: 1,
time: '08:00',
typeNumber: 1,
satiety: 0,
mood: 0,
portions: withPortion
? [
const PortionModel(
id: 10,
productNutritionalValueId: null,
productCaloriesInfo: null,
productName: 'Apple',
quantity: 100,
portion: 'gr',
fromDiet: false,
portionState: PortionState.eaten,
),
]
: const [],
photos: const [],
),
],
chosenPlanId: null,
dayOfChosenDiet: null,
diet: null,
);
return DiaryState.loaded(diary: diary, nutrition: nutrition, selectedDay: DateTime(2025, 1, 1), selectedMeal: 1);
}
Widget wrap(Widget child, DiaryCubit cubit) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: BlocProvider<DiaryCubit>.value(
value: cubit,
child: Scaffold(body: child),
),
);
}
testWidgets('renders for adding a product (portion=null)', (tester) async {
final state = makeLoadedState(withPortion: false);
final cubit = FakeDiaryCubit(state);
await tester.pumpWidget(wrap(AddProductModal(state: state as DiaryStateLoaded, diaryCubit: cubit), cubit));
await tester.pump();
expect(find.text('Добавление блюда'), findsOneWidget);
expect(find.text('Добавить'), findsOneWidget);
await cubit.close();
});
testWidgets('renders for editing a product (portion!=null)', (tester) async {
final state = makeLoadedState(withPortion: true);
final cubit = FakeDiaryCubit(state);
final loaded = state as DiaryStateLoaded;
final portion = loaded.diary.meals.first.portions.first;
await tester.pumpWidget(wrap(AddProductModal(state: loaded, diaryCubit: cubit, portion: portion), cubit));
await tester.pump();
expect(find.text('Добавление блюда'), findsOneWidget);
expect(find.text('Сохранить'), findsOneWidget);
expect(find.byIcon(Icons.delete_outline_rounded), findsOneWidget);
await cubit.close();
});
});
}

View File

@@ -0,0 +1,45 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/calendar_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
group('CalendarWidget smoke', () {
Widget wrap(Widget child, DiaryCubit cubit) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: BlocProvider<DiaryCubit>.value(
value: cubit,
child: Scaffold(body: child),
),
);
}
testWidgets('renders', (tester) async {
final cubit = FakeDiaryCubit(const DiaryState.loading());
await tester.pumpWidget(wrap(const CalendarWidget(), cubit));
await tester.pump();
// Just make sure it builds.
expect(find.byType(CalendarWidget), findsOneWidget);
await cubit.close();
});
});
}

View File

@@ -0,0 +1,69 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/calories_chart_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
group('CaloriesChartWidget smoke', () {
Widget wrap(Widget child, DiaryCubit cubit) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: BlocProvider<DiaryCubit>.value(
value: cubit,
child: Scaffold(body: child),
),
);
}
testWidgets('renders chart and opens help dialog', (tester) async {
final diary = DiaryDayEntity(
id: 1,
date: '2025-01-01',
wakeup: '06:30:00',
retire: '22:30:00',
closed: 0,
comment: '',
typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: false, diaryState: null),
meals: const [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])],
chosenPlanId: null,
dayOfChosenDiet: null,
diet: null,
);
const nutrition = NutritionDataModel(
dailyNutritionIntake: NutritionModel(energy: 1200, proteins: 50, fats: 30, carbohydrates: 150),
dailyNutritionRecommendation: NutritionModel(energy: 2000, proteins: 100, fats: 70, carbohydrates: 250),
);
final state = DiaryState.loaded(diary: diary, nutrition: nutrition, selectedDay: DateTime(2025, 1, 1), selectedMeal: 1);
final cubit = FakeDiaryCubit(state);
await tester.pumpWidget(wrap(const CaloriesChartWidget(), cubit));
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(find.text('Калории'), findsWidgets);
expect(find.text('Белки'), findsWidgets);
expect(find.text('Жиры'), findsWidgets);
expect(find.text('Углеводы'), findsWidgets);
await tester.tap(find.byIcon(Icons.help_outline_rounded));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
await cubit.close();
});
});
}

View File

@@ -0,0 +1,65 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/comment_for_consultant_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/day_comment_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit() : super(const DiaryState.loading());
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> implements ConsultantCommentsCubit {
FakeConsultantCommentsCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
group('Diary comment widgets smoke', () {
Widget wrap(Widget child, {DiaryCubit? diaryCubit, ConsultantCommentsCubit? commentsCubit}) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: MultiBlocProvider(
providers: [
if (diaryCubit != null) BlocProvider<DiaryCubit>.value(value: diaryCubit),
if (commentsCubit != null) BlocProvider<ConsultantCommentsCubit>.value(value: commentsCubit),
],
child: Scaffold(body: child),
),
);
}
testWidgets('CommentForConsultantWidget renders and accepts initial comment', (tester) async {
final cubit = FakeDiaryCubit();
await tester.pumpWidget(wrap(const CommentForConsultantWidget(comment: 'Hi'), diaryCubit: cubit));
await tester.pump();
expect(find.text('Дополнительная информация для консультанта'), findsOneWidget);
expect(find.text('Hi'), findsOneWidget);
});
testWidgets('DayCommentWidget shows validation error state', (tester) async {
final cubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: true));
await tester.pumpWidget(wrap(const DayCommentWidget(), commentsCubit: cubit));
await tester.pump();
expect(find.byType(DayCommentWidget), findsOneWidget);
expect(find.text('Минимум 3 символа'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,112 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/add_product_button_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/chosen_diet_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/chosen_plan_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
group('Diary chosen widgets smoke', () {
Widget wrap(Widget child, DiaryCubit cubit) {
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (_, _) => BlocProvider<DiaryCubit>.value(
value: cubit,
child: Scaffold(body: child),
),
),
GoRoute(path: '/about-diet', builder: (_, _) => const SizedBox.shrink()),
],
);
return MaterialApp.router(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
routerConfig: router,
);
}
DiaryStateLoaded loadedState({required bool hasDiet, required bool hasPlan}) {
final diary = DiaryDayEntity(
id: 1,
date: '2025-01-01',
wakeup: '07:00',
retire: '23:00',
closed: 0,
comment: '',
typeOfDiary: TypeOfDiaryModel(hasChosenDiet: hasDiet, hasChosenPlan: hasPlan, diaryState: DiaryStateEnum.started),
meals: const [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])],
chosenPlanId: hasPlan ? 1 : null,
dayOfChosenDiet: hasDiet ? 2 : null,
diet: hasDiet ? const DiaryDietModel(id: 10, duration: 7, name: 'Diet') : null,
);
const nutrition = NutritionDataModel(
dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
);
return DiaryState.loaded(
diary: diary,
nutrition: nutrition,
selectedDay: DateTime(2025, 1, 1),
selectedMeal: 1,
chosenPlanCurrentDay: hasPlan ? 3 : null,
chosenPlanName: hasPlan ? 'Plan' : null,
chosenPlanDuration: hasPlan ? 30 : null,
)
as DiaryStateLoaded;
}
testWidgets('ChosenPlanWidget renders when plan selected', (tester) async {
final cubit = FakeDiaryCubit(loadedState(hasDiet: false, hasPlan: true));
await tester.pumpWidget(wrap(const ChosenPlanWidget(), cubit));
await tester.pump();
expect(find.text('Plan'), findsOneWidget);
expect(find.textContaining('из 30'), findsOneWidget);
});
testWidgets('ChosenDietWidget renders and is tappable in loadedTomorrowWithDiet', (tester) async {
const futureDiary = DiaryFutureDietDayEntity(
date: '2025-01-02',
typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null),
meals: [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])],
diet: DiaryDietModel(id: 10, duration: 7, name: 'Diet'),
dayOfChosenDiet: 2,
);
final cubit = FakeDiaryCubit(DiaryState.loadedTomorrowWithDiet(diary: futureDiary, selectedDay: DateTime(2025, 1, 2), selectedMeal: 1));
await tester.pumpWidget(wrap(const ChosenDietWidget(), cubit));
await tester.pump();
expect(find.text('Diet'), findsOneWidget);
});
testWidgets('AddProductButtonWidget renders for loaded state', (tester) async {
final cubit = FakeDiaryCubit(loadedState(hasDiet: false, hasPlan: false));
await tester.pumpWidget(wrap(const AddProductButtonWidget(), cubit));
await tester.pump();
expect(find.text('Добавить'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,224 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/meal_comment_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/mood_rating_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/satiety_rating_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/time_of_meal_picker_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
int? fillTimeDiaryId;
String? fillTimeValue;
int? fillMoodValue;
int? fillSatietyValue;
@override
Future<void> fillTime({required int diaryId, required String time}) async {
fillTimeDiaryId = diaryId;
fillTimeValue = time;
}
@override
Future<void> fillMood({required int diaryId, required int mood}) async {
fillMoodValue = mood;
}
@override
Future<void> fillSatiety({required int diaryId, required int satiety}) async {
fillSatietyValue = satiety;
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> implements ConsultantCommentsCubit {
FakeConsultantCommentsCubit(super.initialState);
int? lastMealNumber;
String? lastMealComment;
@override
void setMealComment({required int mealNumber, required String comment}) {
lastMealNumber = mealNumber;
lastMealComment = comment;
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeAssetBundle extends CachingAssetBundle {
_FakeAssetBundle(this.assets);
final Map<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> load(String key) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return ByteData.sublistView(Uint8List.fromList(value.codeUnits));
}
}
void main() {
group('Diary rating/time widgets smoke', () {
const fakeSvg = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
final bundle = _FakeAssetBundle({
'assets/icons/satiety_emoji_left.svg': fakeSvg,
'assets/icons/satiety_emoji_right.svg': fakeSvg,
'assets/icons/mood_1.svg': fakeSvg,
'assets/icons/mood_2.svg': fakeSvg,
'assets/icons/error_icon.svg': fakeSvg,
});
Widget wrap({required DiaryCubit diaryCubit, required ConsultantCommentsCubit commentsCubit, required Widget child}) {
return DefaultAssetBundle(
bundle: bundle,
child: MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: MediaQuery(
data: const MediaQueryData(size: Size(500, 900)),
child: MultiBlocProvider(
providers: [
BlocProvider<DiaryCubit>.value(value: diaryCubit),
BlocProvider<ConsultantCommentsCubit>.value(value: commentsCubit),
],
child: Scaffold(body: child),
),
),
),
);
}
DiaryStateLoaded loadedState({
required int satiety,
required int mood,
bool consultantView = false,
bool diaryFinished = false,
bool diaryCommented = false,
}) {
final diary = DiaryDayEntity(
id: 1,
date: '2025-01-01',
wakeup: '07:00',
retire: '23:00',
closed: 0,
comment: '',
typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: true, diaryState: DiaryStateEnum.started),
meals: [MealModel(id: 1, time: '08:00:00', typeNumber: 1, satiety: satiety, mood: mood, portions: const [], photos: const [])],
chosenPlanId: 1,
dayOfChosenDiet: null,
diet: null,
);
const nutrition = NutritionDataModel(
dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
);
return DiaryState.loaded(
diary: diary,
nutrition: nutrition,
selectedDay: DateTime(2025, 1, 1),
selectedMeal: 1,
isConsultantView: consultantView,
isDiaryFinished: diaryFinished,
isDiaryCommented: diaryCommented,
consultantReview: ConsultantReviewModel(
planName: 'Plan',
planDuration: 10,
currentPlanDay: 1,
consultantComments: ConsultantComments(dayComment: null, mealsComments: const [], portionsComments: const []),
),
)
as DiaryStateLoaded;
}
testWidgets('TimeOfMealPickerWidget renders time and opens picker', (tester) async {
final diaryCubit = FakeDiaryCubit(loadedState(satiety: 0, mood: 0));
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const TimeOfMealPickerWidget()));
await tester.pump();
expect(find.text('Время приема пищи'), findsOneWidget);
expect(find.text('08:00'), findsOneWidget);
await tester.tap(find.text('08:00'));
await tester.pumpAndSettle();
expect(find.text('Сохранить'), findsOneWidget);
});
testWidgets('SatietyRatingWidget renders and is disabled for consultant', (tester) async {
final diaryCubit = FakeDiaryCubit(loadedState(satiety: 2, mood: 0, consultantView: true));
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const SatietyRatingWidget()));
await tester.pump();
expect(find.text('Сытость пользователя после приема пищи'), findsOneWidget);
});
testWidgets('MoodRatingWidget renders', (tester) async {
final diaryCubit = FakeDiaryCubit(loadedState(satiety: 0, mood: 3));
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const MoodRatingWidget()));
await tester.pump();
expect(find.text('Ваше настроение после приёма пищи'), findsOneWidget);
});
testWidgets('MealCommentWidget shows validation error when comment too short', (tester) async {
final diaryCubit = FakeDiaryCubit(loadedState(satiety: 0, mood: 0));
final commentsCubit = FakeConsultantCommentsCubit(
ConsultantCommentsState(
commentsModel: ConsultantCommentModel(
comment: '',
mealsComments: {'1': MealCommentModel(comment: 'a', typeNumber: 1)},
),
),
);
await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const MealCommentWidget(selectedMeal: 1)));
await tester.pump();
expect(find.text('Минимум 3 символа'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,207 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/mood_rating_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/satiety_rating_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/time_of_meal_picker_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
int? lastDiaryId;
String? lastTime;
int? lastSatiety;
int? lastMood;
@override
Future<void> fillTime({required int diaryId, required String time}) async {
lastDiaryId = diaryId;
lastTime = time;
}
@override
Future<void> fillSatiety({required int diaryId, required int satiety}) async {
lastDiaryId = diaryId;
lastSatiety = satiety;
}
@override
Future<void> fillMood({required int diaryId, required int mood}) async {
lastDiaryId = diaryId;
lastMood = mood;
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> implements ConsultantCommentsCubit {
FakeConsultantCommentsCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeAssetBundle extends CachingAssetBundle {
_FakeAssetBundle(this.assets);
final Map<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> load(String key) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return ByteData.sublistView(Uint8List.fromList(value.codeUnits));
}
}
void main() {
group('Diary time & rating interactions', () {
const fakeSvg = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
final bundle = _FakeAssetBundle({
'assets/icons/satiety_emoji_left.svg': fakeSvg,
'assets/icons/satiety_emoji_right.svg': fakeSvg,
'assets/icons/mood_1.svg': fakeSvg,
'assets/icons/mood_2.svg': fakeSvg,
});
Widget wrap({required DiaryCubit diaryCubit, required ConsultantCommentsCubit commentsCubit, required Widget child}) {
return DefaultAssetBundle(
bundle: bundle,
child: MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: MediaQuery(
data: const MediaQueryData(size: Size(500, 900)),
child: MultiBlocProvider(
providers: [
BlocProvider<DiaryCubit>.value(value: diaryCubit),
BlocProvider<ConsultantCommentsCubit>.value(value: commentsCubit),
],
child: Scaffold(body: child),
),
),
),
);
}
DiaryStateLoaded loadedState({required int satiety, required int mood}) {
final diary = DiaryDayEntity(
id: 1,
date: '2025-01-01',
wakeup: '07:00',
retire: '23:00',
closed: 0,
comment: '',
typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: true, diaryState: DiaryStateEnum.started),
meals: [MealModel(id: 1, time: '08:00:00', typeNumber: 1, satiety: satiety, mood: mood, portions: const [], photos: const [])],
chosenPlanId: 1,
dayOfChosenDiet: null,
diet: null,
);
const nutrition = NutritionDataModel(
dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
);
return DiaryState.loaded(
diary: diary,
nutrition: nutrition,
selectedDay: DateTime(2025, 1, 1),
selectedMeal: 1,
isConsultantView: false,
isDiaryFinished: false,
isDiaryCommented: false,
consultantReview: ConsultantReviewModel(
planName: 'Plan',
planDuration: 10,
currentPlanDay: 1,
consultantComments: ConsultantComments(dayComment: null, mealsComments: const [], portionsComments: const []),
),
)
as DiaryStateLoaded;
}
testWidgets('TimeOfMealPickerWidget calls fillTime on save', (tester) async {
final diaryCubit = FakeDiaryCubit(loadedState(satiety: 0, mood: 0));
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const TimeOfMealPickerWidget()));
await tester.pump();
// Open picker.
await tester.tap(find.text('08:00'));
await tester.pumpAndSettle();
await tester.tap(find.text('Сохранить'));
await tester.pump();
expect(diaryCubit.lastDiaryId, 1);
expect(diaryCubit.lastTime, isNotNull);
});
testWidgets('SatietyRatingWidget calls fillSatiety after rating change', (tester) async {
final diaryCubit = FakeDiaryCubit(loadedState(satiety: 0, mood: 0));
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const SatietyRatingWidget()));
await tester.pump();
final bar = find.byType(RatingBar);
expect(bar, findsOneWidget);
final box = tester.renderObject<RenderBox>(bar);
await tester.tapAt(box.localToGlobal(box.size.center(Offset.zero)));
await tester.pump(const Duration(milliseconds: 300)); // allow EasyDebounce to fire
expect(diaryCubit.lastSatiety, isNotNull);
});
testWidgets('MoodRatingWidget calls fillMood after rating change', (tester) async {
final diaryCubit = FakeDiaryCubit(loadedState(satiety: 0, mood: 0));
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const MoodRatingWidget()));
await tester.pump();
final bar = find.byType(RatingBar);
expect(bar, findsOneWidget);
final box = tester.renderObject<RenderBox>(bar);
await tester.tapAt(box.localToGlobal(box.size.center(Offset.zero)));
await tester.pump(const Duration(milliseconds: 300));
expect(diaryCubit.lastMood, isNotNull);
});
});
}

View File

@@ -0,0 +1,82 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'glossary_moderation_request_modal_widget_test.mocks.dart';
@GenerateNiceMocks([MockSpec<DiaryCubit>()])
void main() {
group('GlossaryModerationRequestModalWidget', () {
Widget wrap(Widget child) => MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
testWidgets('renders alert state and switches to addNewProduct', (tester) async {
final cubit = MockDiaryCubit();
await tester.pumpWidget(wrap(GlossaryModerationRequestModalWidget(productName: 'Apple', diaryCubit: cubit)));
await tester.pump();
expect(find.text('Добавление нового продукта'), findsOneWidget);
expect(find.text('Записать'), findsOneWidget);
await tester.tap(find.text('Записать'));
await tester.pump();
expect(find.text('Сохранить'), findsOneWidget);
});
testWidgets('alert -> refuse triggers sendProductToModeration and success', (tester) async {
final cubit = MockDiaryCubit();
when(
cubit.sendProductToModeration(
name: anyNamed('name'),
energy: anyNamed('energy'),
proteins: anyNamed('proteins'),
fats: anyNamed('fats'),
carbohydrates: anyNamed('carbohydrates'),
),
).thenAnswer(
(_) async => const GlossaryModerationModel(
glossary: GlossaryModel(
id: 1,
name: 'Apple',
grInPortion: 0,
energyOn100Gr: 52,
proteinsOn100Gr: 0,
fatsOn100Gr: 0,
carbohydratesOn100Gr: 14,
energyOnPortion: 0,
proteinsOnPortion: 0,
fatsOnPortion: 0,
carbohydratesOnPortion: 0,
),
),
);
await tester.pumpWidget(wrap(GlossaryModerationRequestModalWidget(productName: 'Apple', diaryCubit: cubit)));
await tester.pump();
await tester.tap(find.text('Отказаться'));
await tester.pump();
expect(find.textContaining('Продукт успешно отправлен'), findsOneWidget);
verify(
cubit.sendProductToModeration(
name: anyNamed('name'),
energy: anyNamed('energy'),
proteins: anyNamed('proteins'),
fats: anyNamed('fats'),
carbohydrates: anyNamed('carbohydrates'),
),
).called(1);
});
});
}

View File

@@ -0,0 +1,216 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart';
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/meal_button_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
int? lastSelectedMeal;
@override
void setSelectedMeal(int meal) {
lastSelectedMeal = meal;
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
group('MealButtonWidget', () {
Widget wrap(DiaryCubit cubit, Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: MediaQuery(
data: const MediaQueryData(size: Size(500, 900)),
child: BlocProvider<DiaryCubit>.value(
value: cubit,
child: Scaffold(body: child),
),
),
);
}
DiaryStateLoaded loadedState({required int selectedMeal, required List<List<PortionModel>> mealsPortions, bool withConsultantComment = false}) {
const caloriesInfo = ProductCaloriesInfoModel(
id: 1,
productName: 'P',
calories: 100,
proteins: 10,
fats: 10,
carbohydrates: 10,
energyOnPortion: 100,
proteinsOnPortion: 10,
fatsOnPortion: 10,
carbohydratesOnPortion: 10,
);
final meals = List<MealModel>.generate(
5,
(i) => MealModel(
id: i + 1,
time: '08:00',
typeNumber: i + 1,
satiety: 0,
mood: 0,
portions: mealsPortions.length > i
? mealsPortions[i]
: const [
PortionModel(
id: 1,
productNutritionalValueId: 1,
productCaloriesInfo: caloriesInfo,
productName: 'P',
quantity: 100,
portion: 'gr',
fromDiet: false,
portionState: PortionState.eaten,
),
],
photos: const [],
),
);
final diary = DiaryDayEntity(
id: 1,
date: '2025-01-01',
wakeup: '07:00',
retire: '23:00',
closed: 0,
comment: '',
typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: true, diaryState: DiaryStateEnum.started),
meals: meals,
chosenPlanId: 1,
dayOfChosenDiet: null,
diet: null,
);
const nutrition = NutritionDataModel(
dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
);
return DiaryState.loaded(
diary: diary,
nutrition: nutrition,
selectedDay: DateTime(2025, 1, 1),
selectedMeal: selectedMeal,
isConsultantView: false,
isDiaryFinished: false,
isDiaryCommented: false,
consultantReview: ConsultantReviewModel(
planName: 'Plan',
planDuration: 10,
currentPlanDay: 1,
consultantComments: ConsultantComments(
dayComment: null,
mealsComments: withConsultantComment ? [MealComment(comment: 'c', typeNo: 2)] : const [],
portionsComments: const [],
),
),
)
as DiaryStateLoaded;
}
testWidgets('shows calories for loaded state and calls setSelectedMeal on tap', (tester) async {
const caloriesInfo = ProductCaloriesInfoModel(
id: 1,
productName: 'P',
calories: 100,
proteins: 10,
fats: 10,
carbohydrates: 10,
energyOnPortion: 100,
proteinsOnPortion: 10,
fatsOnPortion: 10,
carbohydratesOnPortion: 10,
);
const portion = PortionModel(
id: 1,
productNutritionalValueId: 1,
productCaloriesInfo: caloriesInfo,
productName: 'P',
quantity: 100,
portion: 'gr',
fromDiet: false,
portionState: PortionState.eaten,
);
final cubit = FakeDiaryCubit(
loadedState(
selectedMeal: 1,
mealsPortions: const [
[portion],
[],
[],
[],
[],
],
),
);
await tester.pumpWidget(wrap(cubit, const MealButtonWidget(mealNumber: 1)));
await tester.pump();
expect(find.text('100 ккал'), findsOneWidget);
await tester.tap(find.text('1'));
await tester.pump();
expect(cubit.lastSelectedMeal, 1);
});
testWidgets('shows dash calories when meal empty', (tester) async {
final cubit = FakeDiaryCubit(loadedState(selectedMeal: 1, mealsPortions: const [[], [], [], [], []]));
await tester.pumpWidget(wrap(cubit, const MealButtonWidget(mealNumber: 3)));
await tester.pump();
expect(find.text('— ккал'), findsOneWidget);
});
testWidgets('works for tomorrow-with-diet state', (tester) async {
const caloriesInfo = ProductCaloriesInfoModel(
id: 1,
productName: 'P',
calories: 100,
proteins: 10,
fats: 10,
carbohydrates: 10,
energyOnPortion: 100,
proteinsOnPortion: 10,
fatsOnPortion: 10,
carbohydratesOnPortion: 10,
);
const portion = PortionModel(
id: 1,
productNutritionalValueId: 1,
productCaloriesInfo: caloriesInfo,
productName: 'P',
quantity: 100,
portion: 'gr',
fromDiet: true,
portionState: PortionState.eatenFromDiet,
);
const diary = DiaryFutureDietDayEntity(
date: '2025-01-02',
typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null),
meals: [
MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [portion], photos: []),
],
diet: null,
dayOfChosenDiet: 1,
);
final cubit = FakeDiaryCubit(DiaryState.loadedTomorrowWithDiet(diary: diary, selectedDay: DateTime(2025, 1, 2), selectedMeal: 1));
await tester.pumpWidget(wrap(cubit, const MealButtonWidget(mealNumber: 1)));
await tester.pump();
expect(find.text('100 ккал'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,107 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/meal_comment_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> implements ConsultantCommentsCubit {
FakeConsultantCommentsCubit(super.initialState);
int? lastMealNumber;
String? lastMealComment;
@override
void setMealComment({required int mealNumber, required String comment}) {
lastMealNumber = mealNumber;
lastMealComment = comment;
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeAssetBundle extends CachingAssetBundle {
_FakeAssetBundle(this.assets);
final Map<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> load(String key) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return ByteData.sublistView(Uint8List.fromList(value.codeUnits));
}
}
void main() {
group('MealCommentWidget', () {
const fakeSvg = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
final bundle = _FakeAssetBundle({'assets/icons/error_icon.svg': fakeSvg});
Widget wrap(ConsultantCommentsCubit cubit, Widget child) {
return DefaultAssetBundle(
bundle: bundle,
child: MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: BlocProvider<ConsultantCommentsCubit>.value(
value: cubit,
child: Scaffold(body: child),
),
),
);
}
testWidgets('shows validation error when comment too short', (tester) async {
final cubit = FakeConsultantCommentsCubit(
ConsultantCommentsState(
commentsModel: ConsultantCommentModel(
comment: '',
mealsComments: {'1': MealCommentModel(comment: 'a', typeNumber: 1)},
),
),
);
await tester.pumpWidget(wrap(cubit, const MealCommentWidget(selectedMeal: 1)));
await tester.pump();
expect(find.text('Минимум 3 символа'), findsOneWidget);
});
testWidgets('renders and shows comment title', (tester) async {
final cubit = FakeConsultantCommentsCubit(
ConsultantCommentsState(
commentsModel: ConsultantCommentModel(
comment: '',
mealsComments: {'1': MealCommentModel(comment: 'ok', typeNumber: 1)},
),
),
);
await tester.pumpWidget(wrap(cubit, const MealCommentWidget(selectedMeal: 1)));
await tester.pump();
expect(find.textContaining(S.current.commentForMeal), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,173 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/meal_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> implements ConsultantCommentsCubit {
FakeConsultantCommentsCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeAssetBundle extends CachingAssetBundle {
_FakeAssetBundle(this.assets);
final Map<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> load(String key) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return ByteData.sublistView(Uint8List.fromList(value.codeUnits));
}
}
void main() {
group('MealWidget smoke', () {
const fakeSvg = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
final bundle = _FakeAssetBundle({
'assets/icons/satiety_emoji_left.svg': fakeSvg,
'assets/icons/satiety_emoji_right.svg': fakeSvg,
'assets/icons/mood_1.svg': fakeSvg,
'assets/icons/mood_2.svg': fakeSvg,
'assets/icons/error_icon.svg': fakeSvg,
'assets/no_one_image_was_added.svg': fakeSvg,
});
Widget wrap({required DiaryCubit diaryCubit, required ConsultantCommentsCubit commentsCubit, required Widget child}) {
return DefaultAssetBundle(
bundle: bundle,
child: MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: MediaQuery(
data: const MediaQueryData(size: Size(500, 900)),
child: MultiBlocProvider(
providers: [
BlocProvider<DiaryCubit>.value(value: diaryCubit),
BlocProvider<ConsultantCommentsCubit>.value(value: commentsCubit),
],
child: Scaffold(body: SingleChildScrollView(child: child)),
),
),
),
);
}
DiaryStateLoaded loadedState({required bool consultantView, required bool showMealCommentFromReview}) {
const meals = [
MealModel(id: 1, time: '08:00:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: []),
MealModel(id: 2, time: '09:00:00', typeNumber: 2, satiety: 0, mood: 0, portions: [], photos: []),
MealModel(id: 3, time: '10:00:00', typeNumber: 3, satiety: 0, mood: 0, portions: [], photos: []),
MealModel(id: 4, time: '11:00:00', typeNumber: 4, satiety: 0, mood: 0, portions: [], photos: []),
MealModel(id: 5, time: '12:00:00', typeNumber: 5, satiety: 0, mood: 0, portions: [], photos: []),
];
final diary = DiaryDayEntity(
id: 1,
date: '2025-01-01',
wakeup: '07:00',
retire: '23:00',
closed: 0,
comment: '',
typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: false, diaryState: DiaryStateEnum.started),
meals: meals,
chosenPlanId: null,
dayOfChosenDiet: null,
diet: null,
);
const nutrition = NutritionDataModel(
dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
);
final review = ConsultantReviewModel(
planName: 'Plan',
planDuration: 10,
currentPlanDay: 1,
consultantComments: ConsultantComments(
dayComment: null,
mealsComments: showMealCommentFromReview ? [MealComment(comment: 'Comment', typeNo: 1)] : const [],
portionsComments: const [],
),
);
return DiaryState.loaded(
diary: diary,
nutrition: nutrition,
selectedDay: DateTime(2025, 1, 1),
selectedMeal: 1,
isConsultantView: consultantView,
isDiaryFinished: false,
isDiaryCommented: false,
consultantReview: review,
)
as DiaryStateLoaded;
}
testWidgets('renders base meal widget content', (tester) async {
final diaryCubit = FakeDiaryCubit(loadedState(consultantView: false, showMealCommentFromReview: false));
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const MealWidget(showMood: true, showAddButton: true)));
await tester.pump();
expect(find.text('Прием пищи'), findsOneWidget);
expect(find.byType(MealWidget), findsOneWidget);
});
testWidgets('renders consultant meal comment widgets', (tester) async {
final diaryCubit = FakeDiaryCubit(loadedState(consultantView: true, showMealCommentFromReview: true));
final commentsCubit = FakeConsultantCommentsCubit(
ConsultantCommentsState(
commentsModel: ConsultantCommentModel(comment: '', mealsComments: const {}),
),
);
await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const MealWidget(showMood: false, showAddButton: false)));
await tester.pump();
expect(find.textContaining(S.current.mealComment), findsOneWidget);
expect(find.text('Comment'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,233 @@
import 'dart:async';
import 'dart:io';
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/features/diary/entities/diary_day_entity.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/photo_gallery_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_svg/flutter_svg.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClient implements HttpClient {
bool _autoUncompress = true;
@override
bool get autoUncompress => _autoUncompress;
@override
set autoUncompress(bool value) {
_autoUncompress = value;
}
@override
Future<HttpClientRequest> getUrl(Uri url) async => _FakeHttpClientRequest();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientRequest implements HttpClientRequest {
@override
Future<HttpClientResponse> close() async => _FakeHttpClientResponse();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientResponse extends Stream<List<int>> implements HttpClientResponse {
// 1x1 transparent PNG.
static final List<int> _png = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
0x42,
0x60,
0x82,
];
@override
int get statusCode => 200;
@override
int get contentLength => _png.length;
@override
HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed;
@override
StreamSubscription<List<int>> listen(void Function(List<int>)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) {
return Stream<List<int>>.fromIterable([_png]).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
group('PhotoGalleryWidget smoke', () {
setUpAll(() async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
});
setUp(() async {
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
Widget wrap(DiaryCubit cubit) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: BlocProvider<DiaryCubit>.value(
value: cubit,
child: const Scaffold(body: PhotoGalleryWidget()),
),
);
}
DiaryStateLoaded loadedState({required List<DiaryPhotoModel> photos, bool isConsultantView = false}) {
final diary = DiaryDayEntity(
id: 1,
date: '2025-01-01',
wakeup: '06:30:00',
retire: '22:30:00',
closed: 0,
comment: '',
typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: false, diaryState: null),
meals: [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: const [], photos: photos)],
chosenPlanId: null,
dayOfChosenDiet: null,
diet: null,
);
const nutrition = NutritionDataModel(
dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
dailyNutritionRecommendation: NutritionModel(energy: 1, proteins: 1, fats: 1, carbohydrates: 1),
);
return DiaryState.loaded(
diary: diary,
nutrition: nutrition,
selectedDay: DateTime(2025, 1, 1),
selectedMeal: 1,
isConsultantView: isConsultantView,
isDiaryFinished: false,
isDiaryCommented: false,
)
as DiaryStateLoaded;
}
testWidgets('renders empty placeholder when no photos', (tester) async {
final state = loadedState(photos: const []);
final cubit = FakeDiaryCubit(state);
await tester.pumpWidget(wrap(cubit));
await tester.pump();
expect(find.byType(PhotoGalleryWidget), findsOneWidget);
expect(find.byType(SvgPicture), findsOneWidget);
await cubit.close();
});
testWidgets('renders gallery when photos exist', (tester) async {
final photos = [const DiaryPhotoModel(id: 1, originalName: 'a.png', path: 'chat_files/a.png', size: 1)];
final state = loadedState(photos: photos);
final cubit = FakeDiaryCubit(state);
await HttpOverrides.runZoned(() async {
await tester.pumpWidget(wrap(cubit));
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(find.byType(PhotoGalleryWidget), findsOneWidget);
}, createHttpClient: (_) => _FakeHttpClient());
await cubit.close();
});
});
}

View File

@@ -0,0 +1,112 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/portion_comment_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> implements ConsultantCommentsCubit {
FakeConsultantCommentsCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeAssetBundle extends CachingAssetBundle {
_FakeAssetBundle(this.assets);
final Map<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> load(String key) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return ByteData.sublistView(Uint8List.fromList(value.codeUnits));
}
}
void main() {
group('PortionCommentWidget', () {
const fakeSvg = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
final bundle = _FakeAssetBundle({'assets/icons/error_icon.svg': fakeSvg});
Widget wrap(ConsultantCommentsCubit cubit, Widget child) {
return DefaultAssetBundle(
bundle: bundle,
child: MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: BlocProvider<ConsultantCommentsCubit>.value(
value: cubit,
child: Scaffold(body: child),
),
),
);
}
testWidgets('shows CommentContainer when disabled and comment provided', (tester) async {
final cubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
await tester.pumpWidget(wrap(cubit, const PortionCommentWidget(enabled: false, portionId: 1, comment: 'ok')));
await tester.pump();
expect(find.text('ok'), findsOneWidget);
});
testWidgets('shows validation error when enabled and comment too short', (tester) async {
final cubit = FakeConsultantCommentsCubit(
ConsultantCommentsState(
commentsModel: ConsultantCommentModel(
comment: '',
portions: [PortionCommentModel(id: 1, comment: 'a')],
),
),
);
await tester.pumpWidget(wrap(cubit, const PortionCommentWidget(enabled: true, portionId: 1)));
await tester.pump();
expect(find.text('Минимум 3 символа'), findsOneWidget);
});
testWidgets('updates controller text on portionId change', (tester) async {
final cubit = FakeConsultantCommentsCubit(
ConsultantCommentsState(
commentsModel: ConsultantCommentModel(
comment: '',
portions: [
PortionCommentModel(id: 1, comment: 'first'),
PortionCommentModel(id: 2, comment: 'second'),
],
),
),
);
await tester.pumpWidget(wrap(cubit, const PortionCommentWidget(enabled: true, portionId: 1)));
await tester.pump();
expect(find.text('first'), findsOneWidget);
await tester.pumpWidget(wrap(cubit, const PortionCommentWidget(enabled: true, portionId: 2)));
await tester.pump();
expect(find.text('second'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,244 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart';
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/comment_container.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/portion_comment_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> implements ConsultantCommentsCubit {
FakeConsultantCommentsCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeAssetBundle extends CachingAssetBundle {
_FakeAssetBundle(this.assets);
final Map<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> load(String key) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return ByteData.sublistView(Uint8List.fromList(value.codeUnits));
}
}
void main() {
group('ProductWidget smoke', () {
const fakeSvg = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
final bundle = _FakeAssetBundle({'assets/icons/revert_product.svg': fakeSvg, 'assets/icons/edit.svg': fakeSvg});
Widget wrap({required DiaryCubit diaryCubit, required ConsultantCommentsCubit consultantCommentsCubit, required Widget child}) {
return DefaultAssetBundle(
bundle: bundle,
child: MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: MultiBlocProvider(
providers: [
BlocProvider<DiaryCubit>.value(value: diaryCubit),
BlocProvider<ConsultantCommentsCubit>.value(value: consultantCommentsCubit),
],
child: Scaffold(body: child),
),
),
);
}
DiaryStateLoaded loadedState({required PortionModel portion, required bool consultantView}) {
final diary = DiaryDayEntity(
id: 1,
date: '2025-01-01',
wakeup: '07:00',
retire: '23:00',
closed: 0,
comment: '',
typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: true, diaryState: DiaryStateEnum.started),
meals: [
MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [portion], photos: const []),
],
chosenPlanId: 1,
dayOfChosenDiet: null,
diet: null,
);
const nutrition = NutritionDataModel(
dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
);
return DiaryState.loaded(
diary: diary,
nutrition: nutrition,
selectedDay: DateTime(2025, 1, 1),
selectedMeal: 1,
isConsultantView: consultantView,
isDiaryFinished: false,
isDiaryCommented: false,
consultantReview: ConsultantReviewModel(
planName: 'Plan',
planDuration: 10,
currentPlanDay: 1,
consultantComments: ConsultantComments(dayComment: null, mealsComments: const [], portionsComments: const []),
),
)
as DiaryStateLoaded;
}
testWidgets('renders tomorrow-with-diet tile', (tester) async {
const portion = PortionModel(
id: 1,
productNutritionalValueId: null,
productCaloriesInfo: null,
productName: 'Apple',
quantity: 1,
portion: 'piece',
fromDiet: true,
portionState: PortionState.filledDiet,
);
final diaryCubit = FakeDiaryCubit(
DiaryState.loadedTomorrowWithDiet(
diary: const DiaryFutureDietDayEntity(
date: '2025-01-02',
typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null),
meals: [
MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [portion], photos: []),
],
diet: null,
dayOfChosenDiet: 1,
),
selectedDay: DateTime(2025, 1, 2),
selectedMeal: 1,
),
);
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
await tester.pumpWidget(
wrap(
diaryCubit: diaryCubit,
consultantCommentsCubit: commentsCubit,
child: const ProductWidget(portion: portion),
),
);
await tester.pump();
expect(find.text('Apple'), findsOneWidget);
expect(find.byType(ListTile), findsOneWidget);
});
testWidgets('renders eatenDeleted state with revert icon', (tester) async {
const portion = PortionModel(
id: 1,
productNutritionalValueId: null,
productCaloriesInfo: null,
productName: 'Deleted',
quantity: 100,
portion: 'gr',
fromDiet: false,
portionState: PortionState.eatenDeleted,
);
final diaryCubit = FakeDiaryCubit(loadedState(portion: portion, consultantView: false));
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
await tester.pumpWidget(
wrap(
diaryCubit: diaryCubit,
consultantCommentsCubit: commentsCubit,
child: const ProductWidget(portion: portion),
),
);
await tester.pump();
expect(find.text('Deleted'), findsOneWidget);
expect(find.byType(ProductWidget), findsOneWidget);
});
testWidgets('consultant view toggles comment widget and shows existing comment container', (tester) async {
const portion = PortionModel(
id: 1,
productNutritionalValueId: null,
productCaloriesInfo: null,
productName: 'Portion',
quantity: 100,
portion: 'gr',
fromDiet: false,
portionState: PortionState.filledDiet,
);
final state = loadedState(portion: portion, consultantView: true).copyWith(
consultantReview: ConsultantReviewModel(
planName: 'Plan',
planDuration: 10,
currentPlanDay: 1,
consultantComments: ConsultantComments(
dayComment: null,
mealsComments: const [],
portionsComments: [PortionComment(portionId: 1, comment: 'Looks good')],
),
),
);
final diaryCubit = FakeDiaryCubit(state);
final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
await tester.pumpWidget(
wrap(
diaryCubit: diaryCubit,
consultantCommentsCubit: commentsCubit,
child: const ProductWidget(portion: portion),
),
);
await tester.pump();
expect(find.byType(CommentContainer), findsOneWidget);
expect(find.text('Looks good'), findsOneWidget);
// Tap edit icon to enable PortionCommentWidget.
await tester.tap(find.byType(GestureDetector).first);
await tester.pump();
expect(find.byType(PortionCommentWidget), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,131 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/meal_summary_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/products_list_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
group('ProductsListWidget smoke', () {
Widget wrap(DiaryCubit cubit, Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: BlocProvider<DiaryCubit>.value(
value: cubit,
child: Scaffold(body: child),
),
);
}
testWidgets('renders tomorrow-with-diet list with summary', (tester) async {
const caloriesInfo = ProductCaloriesInfoModel(
id: 1,
productName: 'Apple',
calories: 52,
proteins: 0.3,
fats: 0.2,
carbohydrates: 14,
energyOnPortion: 52,
proteinsOnPortion: 0.3,
fatsOnPortion: 0.2,
carbohydratesOnPortion: 14,
);
const portion = PortionModel(
id: 1,
productNutritionalValueId: 1,
productCaloriesInfo: caloriesInfo,
productName: 'Apple',
quantity: 100,
portion: 'gr',
fromDiet: true,
portionState: PortionState.eatenFromDiet,
);
const futureDiary = DiaryFutureDietDayEntity(
date: '2025-01-02',
typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null),
meals: [
MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [portion], photos: []),
],
diet: null,
dayOfChosenDiet: 1,
);
final state = DiaryState.loadedTomorrowWithDiet(diary: futureDiary, selectedDay: DateTime(2025, 1, 2), selectedMeal: 1);
final cubit = FakeDiaryCubit(state);
await tester.pumpWidget(wrap(cubit, const ProductsListWidget()));
await tester.pump();
expect(find.byType(ProductsListWidget), findsOneWidget);
expect(find.text('Apple'), findsOneWidget);
expect(find.byType(MealSummaryWidget), findsOneWidget);
await cubit.close();
});
test('MealSummaryWidget.calculateMealTotals ignores deleted and non-eaten diet items', () {
const caloriesInfo = ProductCaloriesInfoModel(
id: 1,
productName: 'P',
calories: 100,
proteins: 10,
fats: 10,
carbohydrates: 10,
energyOnPortion: 100,
proteinsOnPortion: 10,
fatsOnPortion: 10,
carbohydratesOnPortion: 10,
);
const portions = [
PortionModel(
id: 1,
productNutritionalValueId: 1,
productCaloriesInfo: caloriesInfo,
productName: 'A',
quantity: 100,
portion: 'gr',
fromDiet: true,
portionState: PortionState.eatenFromDiet,
),
PortionModel(
id: 2,
productNutritionalValueId: 1,
productCaloriesInfo: caloriesInfo,
productName: 'B',
quantity: 100,
portion: 'gr',
fromDiet: true,
portionState: PortionState.filledDiet, // not eaten -> ignored
),
PortionModel(
id: 3,
productNutritionalValueId: 1,
productCaloriesInfo: caloriesInfo,
productName: 'C',
quantity: 100,
portion: 'gr',
fromDiet: false,
portionState: PortionState.eatenDeleted, // deleted -> ignored
),
];
final totals = MealSummaryWidget.calculateMealTotals(portions);
expect(totals['calories']!.round(), 100);
expect(totals['proteins']!.round(), 10);
expect(totals['fats']!.round(), 10);
expect(totals['carbohydrates']!.round(), 10);
});
});
}

View File

@@ -0,0 +1,72 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/wakeup_and_retire_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
group('WakeupAndRetireWidget', () {
Widget wrap(Widget child, DiaryCubit cubit) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: BlocProvider<DiaryCubit>.value(
value: cubit,
child: Scaffold(body: child),
),
);
}
testWidgets('hidden when isEnabled=false', (tester) async {
final cubit = FakeDiaryCubit(const DiaryState.loading());
await tester.pumpWidget(wrap(const WakeupAndRetireWidget(isEnabled: false), cubit));
await tester.pump();
expect(find.text('Подъём'), findsNothing);
await cubit.close();
});
testWidgets('renders wakeup/retire when state loaded', (tester) async {
final diary = DiaryDayEntity(
id: 1,
date: '2025-01-01',
wakeup: '06:30:00',
retire: '22:30:00',
closed: 0,
comment: '',
typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: false, diaryState: null),
meals: const [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])],
chosenPlanId: null,
dayOfChosenDiet: null,
diet: null,
);
const nutrition = NutritionDataModel(
dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0),
dailyNutritionRecommendation: NutritionModel(energy: 1, proteins: 1, fats: 1, carbohydrates: 1),
);
final state = DiaryState.loaded(diary: diary, nutrition: nutrition, selectedDay: DateTime(2025, 1, 1), selectedMeal: 1);
final cubit = FakeDiaryCubit(state);
await tester.pumpWidget(wrap(const WakeupAndRetireWidget(isEnabled: true), cubit));
await tester.pump();
expect(find.text('Подъём'), findsOneWidget);
expect(find.text('Уход ко сну'), findsOneWidget);
expect(find.textContaining('06:30'), findsOneWidget);
expect(find.textContaining('22:30'), findsOneWidget);
await cubit.close();
});
});
}