forked from eatvio/eatvio_app
feat(tests): test coverage 60%
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user