Merge pull request 'Some fixes' (#873) from mso7nueh/eatvio_app:main into main

Reviewed-on: eatvio/eatvio_app#873
Reviewed-by: misha <misha@noreply.localhost>
This commit is contained in:
mso7nueh
2026-01-27 14:57:23 +00:00
33 changed files with 268 additions and 103 deletions

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ]
env:
flutter_version: "3.32.0"
flutter_version: "3.38.6"
java_version: "21.x"
flutter_channel: "stable"

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ]
env:
flutter_version: "3.38.5"
flutter_version: "3.38.6"
java_version: "21.x"
flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/"

View File

@@ -4,6 +4,7 @@ plugins {
id 'com.google.gms.google-services'
// END: FlutterFire Configuration
id "kotlin-android"
id "kotlin-parcelize"
id "dev.flutter.flutter-gradle-plugin"
id 'com.huawei.agconnect'
}
@@ -68,10 +69,10 @@ android {
}
dependencies {
implementation 'com.huawei.agconnect:agconnect-core:1.9.1.301'
implementation 'com.huawei.hms:push:6.12.0.300'
implementation 'com.huawei.agconnect:agconnect-core:1.9.5.300'
implementation 'com.huawei.hms:push:6.13.0.301'
// Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8
implementation 'com.huawei.hms:hianalytics:6.12.0.300'
implementation 'com.huawei.hms:hianalytics:6.12.0.301'
// BouncyCastle provider required by Huawei secure utils used transitively by HMS
implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1'
}

View File

@@ -79,7 +79,7 @@ class CubitRegistrations {
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl(), profileRepository: sl()));
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl(), prefs: sl()));
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
sl.registerFactory<RegistrationCubitV2>(() => RegistrationCubitV2(registrationRepository: sl()));
// Feed related cubits

View File

@@ -1 +1 @@
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown }
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown, isEmailVerificationSkipped }

View File

@@ -1,10 +1,13 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
@@ -59,9 +62,26 @@ class OtherRoutes {
try {
// Prefs уже инициализирован в locator_service, но на всякий случай проверяем
final prefs = await sl.getAsync<Prefs>();
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
if (!welcomeScreenWasShowed) {
return null;
}
if (await TokenStorage.haveTokenInStorage()) {
try {
final profileRepository = sl<ProfileRepository>();
final profile = await profileRepository.getMyProfile();
final firebaseApi = sl<FirebaseApi>();
await firebaseApi.registerPushToken();
if (profile.emailVerifiedAt != null || (prefs.getBool(PrefsKeys.isEmailVerificationSkipped) ?? false)) {
return RouteConstants.feedScreen;
}
return RouteConstants.verifyEmailScreen;
} catch (e) {
return '/auth-screen';
}
} else {
return '/auth-screen';
}

View File

@@ -1,10 +1,8 @@
import 'dart:developer';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
@@ -12,7 +10,6 @@ import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:go_router/go_router.dart';
class AuthCubit extends Cubit<AuthState> {
final AuthRepository authRepository;
@@ -40,23 +37,10 @@ class AuthCubit extends Cubit<AuthState> {
Future<void> initializeUser() async {
try {
await registerAnonymousPushToken();
if (await TokenStorage.haveTokenInStorage()) {
final profile = await profileRepository.getMyProfile();
if (profile.emailVerifiedAt != null) {
await registerPushToken();
FlutterNativeSplash.remove();
AppRouter.navigatorKey.currentContext?.go(RouteConstants.feedScreen);
return;
}
FlutterNativeSplash.remove();
AppRouter.navigatorKey.currentContext?.go(RouteConstants.verifyEmailScreen);
} else {
FlutterNativeSplash.remove();
}
FlutterNativeSplash.remove();
} catch (e) {
log('Error in initializeUser: $e');
FlutterNativeSplash.remove(); // Убираем сплэш даже при ошибке
rethrow;
FlutterNativeSplash.remove();
}
}

View File

@@ -278,7 +278,7 @@ class SocialAuthFacade {
),
title: Text(s.authorization),
),
body: Expanded(child: WebViewWidget(controller: controller)),
body: WebViewWidget(controller: controller),
),
),
),

View File

@@ -105,15 +105,6 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
],
),
),
const SizedBox(width: 12),
InfoBouble(
backgroundColor: appTheme.whiteA700,
children: [
SvgPicture.asset('assets/icons/comment2.svg', width: 13, height: 12),
const SizedBox(width: 8),
Text(widget.diet.totalComments.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
],
),
),
@@ -157,16 +148,11 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
},
itemCount: widget.diet.dailySchedule.length,
),
MainBackgroundBouble(
padding: const EdgeInsets.symmetric(vertical: 16),
const MainBackgroundBouble(
padding: EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Text('0 ${getCommentForm(widget.diet.totalComments, s)}', style: TextStyles.textMMedium.copyWith(color: appTheme.gray500)),
),
const SizedBox(height: 16),
SizedBox(height: 16),
],
),
),

View File

@@ -237,7 +237,16 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
final isActiveCookTimeValid = validateActiveCookTime();
final isEnergyPerServingsValid = validateEnergyPerServing();
return isTitleValid && isDescValid && isImageValid && areServingsValid && isCookTimeValid && isActiveCookTimeValid && isEnergyPerServingsValid;
final isStepsValid = validateSteps();
return isTitleValid &&
isDescValid &&
isImageValid &&
areServingsValid &&
isCookTimeValid &&
isActiveCookTimeValid &&
isEnergyPerServingsValid &&
isStepsValid;
}
bool validateTitle() {
@@ -328,4 +337,20 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
return await glossaryRepository.getGlossaryListByQuery(query);
}
bool validateSteps() {
bool isValid = true;
for (var step in state.entity.steps) {
if (step.description == null || step.description!.trim().isEmpty) {
isValid = false;
break;
}
}
if (!isValid) {
emit(state.copyWith(stepsValidationError: 'Заполните описание для всех шагов'));
} else {
emit(state.copyWith(stepsValidationError: null));
}
return isValid;
}
}

View File

@@ -15,6 +15,7 @@ abstract class CreateRecipeState with _$CreateRecipeState {
String? servingsValidationError,
String? energyPerServingValidationError,
String? imageValidationError,
String? stepsValidationError,
}) = _CreateRecipeState;
}

View File

@@ -131,6 +131,7 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
if (widget.portionIndex != null)
InkWell(
onTap: () {
FocusScope.of(context).unfocus();
widget.createRecipeCubit.removeIngredient(widget.portionIndex!);
Navigator.of(context).pop();
},
@@ -189,6 +190,7 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
Ingredient(name: portion.name, unit: portion.portion.getValue(), portion: portion.quantity.toString(), productId: glossary.id),
);
if (context.mounted) {
FocusScope.of(context).unfocus();
Navigator.of(context).pop();
}
} else {}

View File

@@ -75,6 +75,7 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
ListTile(
key: AppKeys.recipeCreateScreenAddIngredientButton,
onTap: () {
FocusManager.instance.primaryFocus?.unfocus();
nameController.clear();
gramsController.clear();
pcsController.clear();

View File

@@ -55,6 +55,13 @@ class _StepsWidgetState extends State<StepsWidget> {
),
),
),
if (state.stepsValidationError != null) ...[
const SizedBox(height: 8),
Text(
state.stepsValidationError!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
],
const SizedBox(height: 12),
ListView.separated(
shrinkWrap: true,

View File

@@ -18,5 +18,8 @@ abstract class DiaryDayEntity with _$DiaryDayEntity {
@JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet,
@JsonKey(name: "chosen_diet", fromJson: parseDiet) required DiaryDietModel? diet,
}) = _DiaryDayEntity;
const DiaryDayEntity._();
bool get hasProducts => meals.any((meal) => meal.portions.isNotEmpty);
}

View File

@@ -50,11 +50,14 @@ class DiaryCubit extends Cubit<DiaryState> {
Future<bool> setSelectedDay(DateTime day) async {
final prevDay = selectedDay;
final prevMeal = selectedMeal;
selectedDay = day;
selectedMeal = 1;
final canSelect = await fetchDiaryV2(needLoading: true);
if (!canSelect) {
selectedDay = prevDay;
selectedMeal = prevMeal;
}
return canSelect;
@@ -62,34 +65,40 @@ class DiaryCubit extends Cubit<DiaryState> {
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId}) async {
final currentState = state;
if (needLoading) {
emit(const DiaryState.loading());
}
if (diaryId != null) {
final diaryDayById = await diaryRepository.getDiaryDay(diaryId);
final diaryEntity = diaryDayById.toEntity();
try {
if (needLoading) {
emit(const DiaryState.loading());
}
if (diaryId != null) {
final diaryDayById = await diaryRepository.getDiaryDay(diaryId);
final diaryEntity = diaryDayById.toEntity();
if (diaryEntity != null) {
setDiary(diaryDay: diaryEntity, diaryId: diaryId);
return true;
}
}
final diaryDay = await getDiary();
if (diaryDay == null) {
emit(const DiaryState.error(errorMessage: 'Diary not found'));
return true;
}
final diaryEntity = diaryDay.toEntity();
if (diaryEntity != null) {
setDiary(diaryDay: diaryEntity, diaryId: diaryId);
return true;
}
if (diaryDay.typeOfDiary.hasChosenDiet == true && diaryDay.typeOfDiary.diaryState == null) {
setDiaryForDietOnly(diaryDay.toDiaryFutureDayEntity());
return true;
}
emit(currentState);
return false;
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(DiaryState.error(errorMessage: e.toString()));
return false;
}
final diaryDay = await getDiary();
if (diaryDay == null) {
emit(const DiaryState.error(errorMessage: 'Diary not found'));
return true;
}
final diaryEntity = diaryDay.toEntity();
if (diaryEntity != null) {
setDiary(diaryDay: diaryEntity, diaryId: diaryId);
return true;
}
if (diaryDay.typeOfDiary.hasChosenDiet == true && diaryDay.typeOfDiary.diaryState == null) {
setDiaryForDietOnly(diaryDay.toDiaryFutureDayEntity());
return true;
}
emit(currentState);
return false;
}
Future<void> updateCurrentDiary() async {
@@ -109,13 +118,16 @@ class DiaryCubit extends Cubit<DiaryState> {
await diaryRepository.createDiaryDay(day: selectedDay.toISO8601WithTimeZoneOffset());
final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset());
return diaryDay;
} on DioException catch (e) {
if (e.response?.statusCode == 422) {
final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset());
return diaryDay;
} catch (e, st) {
log(e.toString(), stackTrace: st);
if (e is DioException) {
if (e.response?.statusCode == 422) {
final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset());
return diaryDay;
}
}
return null;
}
return null;
}
void setSelectedMeal(int meal) {

View File

@@ -127,6 +127,7 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
child: _isDiaryFinishing
? const Center(child: Loader())
: FinishDayButtonWidget(
hasProducts: state.diary.hasProducts,
onPressed: () async {
setState(() {
_isDiaryFinishing = true;

View File

@@ -1,20 +1,89 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:flutter/material.dart';
class FinishDayButtonWidget extends StatefulWidget {
final Future<void> Function() onPressed;
final bool hasProducts;
const FinishDayButtonWidget({super.key, required this.onPressed});
const FinishDayButtonWidget({super.key, required this.onPressed, required this.hasProducts});
@override
State<FinishDayButtonWidget> createState() => _FinishDayButtonWidgetState();
}
class _FinishDayButtonWidgetState extends State<FinishDayButtonWidget> {
void _onTap() {
if (!widget.hasProducts) {
showErrorDialog(
context,
title: 'Не добавлено продуктов',
description: 'Добавьте хотя бы один продукт в любой из приёмов пищи, чтобы закончить день.',
);
return;
}
_showConfirmationBottomSheet();
}
void _showConfirmationBottomSheet() {
showBottomSheetWithBlur(
context: context,
height: null,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
const BottomSheetHandlerBar(),
const SizedBox(height: 20),
Text('Закончить день', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(height: 16),
Text(
'После завершения дня, его уже нельзя будет отредактировать',
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
CustomOutlinedButton(
text: 'Закончить',
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () async {
Navigator.of(context).pop();
await widget.onPressed();
},
),
const SizedBox(height: 8),
CustomOutlinedButton(
text: 'Отмена',
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
buttonStyle: const ButtonStyle(
side: WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(Color(0xFFEAEAEA)),
),
onPressed: () => Navigator.of(context).pop(),
),
const SizedBox(height: 40),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: widget.onPressed,
onTap: _onTap,
borderRadius: BorderRadius.circular(20),
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 8),

View File

@@ -86,9 +86,10 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
fats: 1,
carbohydrates: 1,
))?.glossary;
setState(() {
glossaryState = GlossaryState.success;
});
if (context.mounted) {
// ignore: use_build_context_synchronously
Navigator.of(context).pop(glossary);
}
},
borderRadius: BorderRadius.circular(20),
child: Ink(

View File

@@ -21,7 +21,11 @@ class DiaryRepository {
Future<DiaryDayModel> getDiaryByDate(String date) async {
try {
Set<int> ids = {};
final DiaryDayModel diaryDay = (await getDiaryIndexByDate(date)).last;
final diaryDays = await getDiaryIndexByDate(date);
if (diaryDays.isEmpty) {
throw Exception('No diary entry found for $date');
}
final DiaryDayModel diaryDay = diaryDays.last;
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {

View File

@@ -228,7 +228,9 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
),
const SizedBox(width: 12),
switch (_selectedChipIndex) {
1 => AddItemButton(onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name)),
1 => AddItemButton(
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name) : null,
),
2 => AddItemButton(
onTap: state.profile?.roles.contains('consultant') ?? false
? () async {

View File

@@ -1,3 +1,5 @@
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
@@ -5,8 +7,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class VerifyEmailCubit extends Cubit<VerifyEmailState> {
final ProfileRepository profileRepository;
final Prefs prefs;
VerifyEmailCubit({required this.profileRepository}) : super(const VerifyEmailState.loading());
VerifyEmailCubit({required this.profileRepository, required this.prefs}) : super(const VerifyEmailState.loading());
Future<void> sendEmailCode() async {
try {
@@ -48,4 +51,9 @@ class VerifyEmailCubit extends Cubit<VerifyEmailState> {
emit(const VerifyEmailState.changeEmailError());
}
}
void skipVerification() {
prefs.setBool(PrefsKeys.isEmailVerificationSkipped, true);
emit(const VerifyEmailState.skipped());
}
}

View File

@@ -23,4 +23,6 @@ abstract class VerifyEmailState with _$VerifyEmailState {
const factory VerifyEmailState.changeEmailSuccess() = VerifyEmailStateChangeEmailSuccess;
const factory VerifyEmailState.changeEmailError() = VerifyEmailStateChangeEmailError;
const factory VerifyEmailState.skipped() = VerifyEmailStateSkipped;
}

View File

@@ -101,6 +101,8 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
// Диалог закроется автоматически в ChangeEmailDialog при успешной смене
} else if (state is VerifyEmailStateChangeEmailError) {
showErrorToast('Ошибка при смене email. Попробуйте снова.');
} else if (state is VerifyEmailStateSkipped) {
goRouter.pushNamed('feedScreen');
}
},
builder: (context, state) {
@@ -163,6 +165,19 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
);
},
),
const SizedBox(height: 16),
CustomOutlinedButton(
text: 'Позже',
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black900),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.gray.withValues(alpha: 0.1)),
),
onPressed: () {
context.read<VerifyEmailCubit>().skipVerification();
},
),
],
),
),

View File

@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
version: 1.3.12+93
version: 1.3.13+94
publish_to: 'none'
environment:
@@ -43,7 +43,7 @@ dependencies:
flutter_keyboard_visibility: ^6.0.0
share_plus: ^12.0.1
flutter_html: ^3.0.0
file_picker: ^10.3.8
file_picker: 10.3.8
audioplayers: ^6.5.1
intl: ^0.20.2
mask_text_input_formatter: ^2.9.0

View File

@@ -4,11 +4,12 @@ import 'package:flutter_test/flutter_test.dart';
void main() {
group('PrefsKeys', () {
test('has all expected values', () {
expect(PrefsKeys.values.length, 4);
expect(PrefsKeys.values.length, 5);
expect(PrefsKeys.values, contains(PrefsKeys.hasAnonymousTokenBeenSent));
expect(PrefsKeys.values, contains(PrefsKeys.welcomeScreenWasShowed));
expect(PrefsKeys.values, contains(PrefsKeys.registrationDate));
expect(PrefsKeys.values, contains(PrefsKeys.marketingBottomSheetHasBeenShown));
expect(PrefsKeys.values, contains(PrefsKeys.isEmailVerificationSkipped));
});
test('can be compared', () {
@@ -21,6 +22,7 @@ void main() {
expect(PrefsKeys.welcomeScreenWasShowed.name, 'welcomeScreenWasShowed');
expect(PrefsKeys.registrationDate.name, 'registrationDate');
expect(PrefsKeys.marketingBottomSheetHasBeenShown.name, 'marketingBottomSheetHasBeenShown');
expect(PrefsKeys.isEmailVerificationSkipped.name, 'isEmailVerificationSkipped');
});
});
}

View File

@@ -4,11 +4,12 @@ import 'package:flutter_test/flutter_test.dart';
void main() {
group('PrefsKeys', () {
test('все значения enum определены', () {
expect(PrefsKeys.values.length, 4);
expect(PrefsKeys.values.length, 5);
expect(PrefsKeys.values, contains(PrefsKeys.hasAnonymousTokenBeenSent));
expect(PrefsKeys.values, contains(PrefsKeys.welcomeScreenWasShowed));
expect(PrefsKeys.values, contains(PrefsKeys.registrationDate));
expect(PrefsKeys.values, contains(PrefsKeys.marketingBottomSheetHasBeenShown));
expect(PrefsKeys.values, contains(PrefsKeys.isEmailVerificationSkipped));
});
test('имена ключей корректны', () {
@@ -16,6 +17,7 @@ void main() {
expect(PrefsKeys.welcomeScreenWasShowed.name, 'welcomeScreenWasShowed');
expect(PrefsKeys.registrationDate.name, 'registrationDate');
expect(PrefsKeys.marketingBottomSheetHasBeenShown.name, 'marketingBottomSheetHasBeenShown');
expect(PrefsKeys.isEmailVerificationSkipped.name, 'isEmailVerificationSkipped');
});
});
}

View File

@@ -137,7 +137,6 @@ void main() {
expect(find.text('Test Diet Name'), findsOneWidget);
expect(find.text('100'), findsOneWidget); // totalViews
expect(find.text('10'), findsOneWidget); // totalLikes
expect(find.text('5'), findsOneWidget); // totalComments
});
});
});

View File

@@ -433,6 +433,7 @@ void main() {
final tempFile = File('${Directory.systemTemp.path}/test_image.jpg');
await tempFile.writeAsBytes([1, 2, 3]);
cubit.replaceImage(tempFile);
cubit.replaceDescriptionInStep('Step 1 description', 0);
expect(cubit.validateAll(), true);
});

View File

@@ -220,7 +220,7 @@ void main() {
testWidgets('should display FinishDayButtonWidget', (WidgetTester tester) async {
// Arrange
final widget = FinishDayButtonWidget(onPressed: () async {});
final widget = FinishDayButtonWidget(onPressed: () async {}, hasProducts: true);
// Act
await tester.pumpWidget(createTestWidget(widget));
@@ -234,7 +234,7 @@ void main() {
// Arrange
diaryCubit.emit(DiaryState.loaded(diary: testDiaryEntity, nutrition: testNutrition, selectedDay: DateTime(2024, 1, 15), selectedMeal: 1));
final widget = FinishDayButtonWidget(onPressed: () async {});
final widget = FinishDayButtonWidget(onPressed: () async {}, hasProducts: true);
await tester.pumpWidget(createTestWidget(widget));
await tester.pumpAndSettle();
@@ -244,7 +244,8 @@ void main() {
await tester.pump();
// Assert
expect(find.text('Закончить день'), findsOneWidget);
// Теперь на экране 2 текста "Закончить день": один на кнопке, другой в заголовке bottom sheet
expect(find.text('Закончить день'), findsNWidgets(2));
});
testWidgets('should display MealWidget with mood rating when showMood is true', (WidgetTester tester) async {

View File

@@ -13,7 +13,7 @@ void main() {
}
testWidgets('renders finish day button widget', (tester) async {
await tester.pumpWidget(wrap(FinishDayButtonWidget(onPressed: () async {})));
await tester.pumpWidget(wrap(FinishDayButtonWidget(onPressed: () async {}, hasProducts: true)));
await tester.pump();
expect(find.byType(FinishDayButtonWidget), findsOneWidget);

View File

@@ -33,7 +33,7 @@ void main() {
expect(find.text('Сохранить'), findsOneWidget);
});
testWidgets('alert -> refuse triggers sendProductToModeration and success', (tester) async {
testWidgets('alert -> refuse triggers sendProductToModeration and closes modal', (tester) async {
final cubit = MockDiaryCubit();
when(
cubit.sendProductToModeration(
@@ -65,9 +65,11 @@ void main() {
await tester.pump();
await tester.tap(find.text('Отказаться'));
await tester.pump();
await tester.pumpAndSettle(); // Wait for navigation animation
expect(find.textContaining('Продукт успешно отправлен'), findsOneWidget);
expect(find.textContaining('Продукт успешно отправлен'), findsNothing);
expect(find.byType(GlossaryModerationRequestModalWidget), findsNothing);
verify(
cubit.sendProductToModeration(
name: anyNamed('name'),

View File

@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
@@ -10,16 +12,18 @@ import 'package:mockito/mockito.dart';
import 'verify_email_cubit_test.mocks.dart';
@GenerateNiceMocks([MockSpec<ProfileRepository>()])
@GenerateNiceMocks([MockSpec<ProfileRepository>(), MockSpec<Prefs>()])
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('VerifyEmailCubit', () {
VerifyEmailCubit? cubit;
late MockProfileRepository mockRepository;
late MockPrefs mockPrefs;
setUp(() {
mockRepository = MockProfileRepository();
mockPrefs = MockPrefs();
});
tearDown(() async {
@@ -30,13 +34,13 @@ void main() {
});
test('начальное состояние - loading', () {
cubit = VerifyEmailCubit(profileRepository: mockRepository);
cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs);
expect(cubit!.state, isA<VerifyEmailStateLoading>());
});
test('sendEmailCode - успешная отправка кода', () async {
cubit = VerifyEmailCubit(profileRepository: mockRepository);
cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs);
const testProfile = ProfileModel(
id: 1,
@@ -69,7 +73,7 @@ void main() {
});
test('tryVerify - успешная верификация', () async {
cubit = VerifyEmailCubit(profileRepository: mockRepository);
cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs);
const testProfile = ProfileModel(
id: 1,
@@ -111,7 +115,7 @@ void main() {
});
test('tryVerify - неверный код', () async {
cubit = VerifyEmailCubit(profileRepository: mockRepository);
cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs);
const testProfile = ProfileModel(
id: 1,
@@ -145,7 +149,7 @@ void main() {
});
test('changeEmail - успешная смена email', () async {
cubit = VerifyEmailCubit(profileRepository: mockRepository);
cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs);
const testProfile = ProfileModel(
id: 1,
@@ -175,10 +179,20 @@ void main() {
expect(cubit!.state, isA<VerifyEmailStateLoaded>());
final loadedState = cubit!.state as VerifyEmailStateLoaded;
expect(loadedState.email, 'new@example.com');
expect(loadedState.email, 'new@example.com');
});
test('skipVerification - пропуск верификации', () {
cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs);
cubit!.skipVerification();
verify(mockPrefs.setBool(PrefsKeys.isEmailVerificationSkipped, true)).called(1);
expect(cubit!.state, isA<VerifyEmailStateSkipped>());
});
test('sendEmailCode - обработка ошибки', () async {
cubit = VerifyEmailCubit(profileRepository: mockRepository);
cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs);
when(mockRepository.getMyProfile()).thenThrow(Exception('Network error'));
@@ -190,7 +204,7 @@ void main() {
});
test('tryVerify - обработка ошибки при верификации', () async {
cubit = VerifyEmailCubit(profileRepository: mockRepository);
cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs);
when(mockRepository.tryVerifyEmailByCode('123456')).thenThrow(Exception('Network error'));
@@ -200,7 +214,7 @@ void main() {
});
test('changeEmail - обработка ошибки', () async {
cubit = VerifyEmailCubit(profileRepository: mockRepository);
cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs);
when(mockRepository.sendEmailCode(newEmail: 'new@example.com')).thenThrow(Exception('Network error'));