merge upstream

This commit is contained in:
2026-01-28 15:45:54 +00:00
133 changed files with 15053 additions and 898 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

@@ -54,11 +54,13 @@ jobs:
- name: Flutter build
run: |
flutter upgrade
flutter clean
flutter pub get
flutter pub upgrade
flutter pub run build_runner build --delete-conflicting-outputs
# Configure Flutter for release mode
pod repo update
flutter build ios --config-only --release
cd ios
pod update

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

@@ -83,8 +83,12 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
}
Future<List<ConsultantCertPhotoModel>> _fetchVisibleCertPhotos() async {
final photos = await consultantRepository.getConsultantCertPhotos();
return _onlyVisible(photos);
try {
final photos = await consultantRepository.getConsultantCertPhotos();
return _onlyVisible(photos);
} catch (e) {
return [];
}
}
List<ConsultantCertPhotoModel> _onlyVisible(List<ConsultantCertPhotoModel> photos) {

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

@@ -3,6 +3,7 @@ import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
setUpAll(() async {
@@ -61,5 +62,37 @@ void main() {
expect(config.supportedLocales, isA<List>());
expect(config.supportedLocales.isNotEmpty, true);
});
test('baseUrl is initialized correctly for each build type', () {
final prodConfig = AppConfig(buildType: AppBuildType.prod);
final devConfig = AppConfig(buildType: AppBuildType.dev);
final testConfig = AppConfig(buildType: AppBuildType.tests);
expect(prodConfig.baseUrl, isA<String>());
expect(devConfig.baseUrl, isA<String>());
expect(testConfig.baseUrl, isA<String>());
expect(devConfig.baseUrl, testConfig.baseUrl);
});
test('setBaseUrl can be called multiple times', () {
final config = AppConfig(buildType: AppBuildType.prod);
const url1 = 'https://url1.com';
const url2 = 'https://url2.com';
config.setBaseUrl(url1);
expect(config.baseUrl, url1);
config.setBaseUrl(url2);
expect(config.baseUrl, url2);
});
test('globalBlocProviders contains expected providers', () {
final config = AppConfig(buildType: AppBuildType.prod);
final providers = config.globalBlocProviders;
expect(providers.length, greaterThan(0));
// Проверяем, что список содержит элементы
expect(providers.first, isA<BlocProvider>());
});
});
}

View File

@@ -106,5 +106,34 @@ FIREBASE_IOS_BUNDLE_ID=com.test.app
// Ожидаемо в тестовом окружении
}
});
test('AppBuildType enum has all expected values', () {
expect(AppBuildType.values.length, 3);
expect(AppBuildType.values, contains(AppBuildType.prod));
expect(AppBuildType.values, contains(AppBuildType.tests));
expect(AppBuildType.values, contains(AppBuildType.dev));
});
test('AppBuildType enum can be compared', () {
expect(AppBuildType.prod == AppBuildType.prod, true);
expect(AppBuildType.prod == AppBuildType.dev, false);
expect(AppBuildType.tests == AppBuildType.dev, false);
});
test('AppInitializer has static methods', () {
expect(AppInitializer.initialize, isA<Function>());
expect(AppInitializer.runAppWithSentry, isA<Function>());
});
test('initialize method signature is correct', () async {
// Проверяем, что метод принимает buildType
// В тестовом окружении Firebase может падать, но это ожидаемо
try {
await AppInitializer.initialize(buildType: AppBuildType.tests);
} catch (e) {
// Ожидаемо в тестовом окружении - Firebase не может быть инициализирован
// Главное, что метод был вызван
}
});
});
}

View File

@@ -61,5 +61,56 @@ void main() {
expect(routes.length, greaterThan(0));
});
test('appRouter имеет правильную конфигурацию маршрутов', () {
expect(AppRouter.appRouter.configuration.routes, isNotEmpty);
});
test('navigateToRegisterViaSocial - создает правильный extra map', () {
const authResult = AuthResultEntity(accessToken: 'test_token', refreshToken: 'refresh_token', userId: '1', clientId: 'test_client_id');
const provider = 'yandex';
expect(() => AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider), returnsNormally);
});
test('appRouter содержит все типы маршрутов', () {
final routes = AppRouter.appRouter.configuration.routes;
// Проверяем, что есть маршруты разных типов
expect(routes, isNotEmpty);
// Проверяем, что есть хотя бы один маршрут
expect(routes.length, greaterThan(0));
});
test('appRouter имеет правильную конфигурацию', () {
expect(AppRouter.appRouter.configuration, isNotNull);
expect(AppRouter.appRouter.configuration.routes, isNotEmpty);
});
test('navigateToRegisterViaSocial создает правильную структуру extra', () {
const authResult = AuthResultEntity(
accessToken: 'test_token',
refreshToken: 'refresh_token',
userId: '1',
clientId: 'test_client_id',
);
const provider = 'yandex';
expect(() => AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider), returnsNormally);
});
test('navigateToRegisterViaSocial работает с разными провайдерами', () {
const authResult = AuthResultEntity(
accessToken: 'test_token',
refreshToken: 'refresh_token',
userId: '1',
clientId: 'test_client_id',
);
expect(() => AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'yandex'), returnsNormally);
expect(() => AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'vk'), returnsNormally);
expect(() => AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'google'), returnsNormally);
});
});
}

View File

@@ -125,6 +125,34 @@ void main() {
expect(AppConstants.commentsEndpoint, '/comments');
});
});
group('Regex validation', () {
test('email regex validates correct emails', () {
final regex = RegExp(AppConstants.emailRegex);
expect(regex.hasMatch('test@example.com'), isTrue);
expect(regex.hasMatch('user.name@domain.co.uk'), isTrue);
expect(regex.hasMatch('invalid-email'), isFalse);
expect(regex.hasMatch('@example.com'), isFalse);
});
test('phone regex validates correct phones', () {
final regex = RegExp(AppConstants.phoneRegex);
expect(regex.hasMatch('+1234567890'), isTrue);
expect(regex.hasMatch('1234567890'), isTrue);
expect(regex.hasMatch('+123'), isTrue);
expect(regex.hasMatch('invalid'), isFalse);
expect(regex.hasMatch('+0123456789'), isFalse);
});
test('url regex validates correct URLs', () {
final regex = RegExp(AppConstants.urlRegex);
expect(regex.hasMatch('https://example.com'), isTrue);
expect(regex.hasMatch('http://www.example.com'), isTrue);
expect(regex.hasMatch('https://example.com/path?query=value'), isTrue);
expect(regex.hasMatch('invalid-url'), isFalse);
expect(regex.hasMatch('ftp://example.com'), isFalse);
});
});
});
}

View File

@@ -105,5 +105,14 @@ void main() {
final result = await cubit.executeFailingOperationWithState();
expect(result, isNull);
});
test('handleError logs error without throwing', () {
expect(() => cubit.handleError(Exception('Test error')), returnsNormally);
});
test('handleError logs error with stack trace', () {
final stackTrace = StackTrace.current;
expect(() => cubit.handleError(Exception('Test error'), stackTrace), returnsNormally);
});
});
}

View File

@@ -26,5 +26,18 @@ void main() {
// Const instances should be equal
expect(state1, state2);
});
test('MarketingCubitState can be used in equality checks', () {
const state1 = MarketingCubitState();
const state2 = MarketingCubitState();
expect(state1 == state2, true);
expect(state1.hashCode, state2.hashCode);
});
test('MarketingCubitState toString returns correct representation', () {
const state = MarketingCubitState();
expect(state.toString(), contains('MarketingCubitState'));
});
});
}

View File

@@ -59,6 +59,35 @@ void main() {
expect(StateUtils.getErrorMessage('error: test'), isNotNull);
expect(StateUtils.getErrorMessage('Error: test message'), isNotNull);
});
test('getErrorMessage extracts error message from complex state string', () {
const errorState = 'StateError(error: Network error occurred)';
final message = StateUtils.getErrorMessage(errorState);
expect(message, isNotNull);
expect(message, contains('Network'));
});
test('getErrorMessage returns null for empty error state', () {
expect(StateUtils.getErrorMessage(''), isNull);
});
test('isLoading handles case variations', () {
expect(StateUtils.isLoading('isLoading'), isTrue);
expect(StateUtils.isLoading('state_is_loading'), isTrue);
expect(StateUtils.isLoading('StateLoading'), isTrue);
});
test('isLoaded handles case variations', () {
expect(StateUtils.isLoaded('isLoaded'), isTrue);
expect(StateUtils.isLoaded('state_is_loaded'), isTrue);
expect(StateUtils.isLoaded('StateLoaded'), isTrue);
});
test('isError handles case variations', () {
expect(StateUtils.isError('isError'), isTrue);
expect(StateUtils.isError('state_is_error'), isTrue);
expect(StateUtils.isError('StateError'), isTrue);
});
});
}

View File

@@ -0,0 +1,77 @@
import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('SelectionPopupModel', () {
test('creates instance with required fields', () {
const model = SelectionPopupModel(title: 'Test Title');
expect(model.title, 'Test Title');
expect(model.id, isNull);
expect(model.value, isNull);
expect(model.isSelected, isFalse);
});
test('creates instance with all fields', () {
const model = SelectionPopupModel(
id: 1,
title: 'Test Title',
value: 'test_value',
isSelected: true,
);
expect(model.id, 1);
expect(model.title, 'Test Title');
expect(model.value, 'test_value');
expect(model.isSelected, isTrue);
});
test('creates instance with default isSelected false', () {
const model = SelectionPopupModel(title: 'Test Title');
expect(model.isSelected, isFalse);
});
test('creates instance with isSelected true', () {
const model = SelectionPopupModel(
title: 'Test Title',
isSelected: true,
);
expect(model.isSelected, isTrue);
});
test('creates instance with different value types', () {
const model1 = SelectionPopupModel(title: 'Test', value: 'string');
const model2 = SelectionPopupModel(title: 'Test', value: 123);
const model3 = SelectionPopupModel(title: 'Test', value: true);
expect(model1.value, 'string');
expect(model2.value, 123);
expect(model3.value, true);
});
test('equality works correctly', () {
const model1 = SelectionPopupModel(id: 1, title: 'Test', isSelected: true);
const model2 = SelectionPopupModel(id: 1, title: 'Test', isSelected: true);
const model3 = SelectionPopupModel(id: 2, title: 'Test', isSelected: true);
expect(model1, equals(model2));
expect(model1, isNot(equals(model3)));
});
test('copyWith creates new instance with updated values', () {
const original = SelectionPopupModel(title: 'Original');
final updated = original.copyWith(
id: 1,
isSelected: true,
value: 'new_value',
);
expect(updated.id, 1);
expect(updated.title, 'Original');
expect(updated.isSelected, isTrue);
expect(updated.value, 'new_value');
});
});
}

View File

@@ -22,5 +22,19 @@ void main() {
expect(BaseState.initial.index, 2);
expect(BaseState.success.index, 3);
});
test('can access by name', () {
expect(BaseState.values.byName('loading'), BaseState.loading);
expect(BaseState.values.byName('error'), BaseState.error);
expect(BaseState.values.byName('initial'), BaseState.initial);
expect(BaseState.values.byName('success'), BaseState.success);
});
test('toString returns correct name', () {
expect(BaseState.loading.toString(), 'BaseState.loading');
expect(BaseState.error.toString(), 'BaseState.error');
expect(BaseState.initial.toString(), 'BaseState.initial');
expect(BaseState.success.toString(), 'BaseState.success');
});
});
}

View File

@@ -26,5 +26,17 @@ void main() {
expect(ComplainedContent.posts.index, 4);
expect(ComplainedContent.comment.index, 5);
});
test('can access by name', () {
expect(ComplainedContent.values.byName('diet'), ComplainedContent.diet);
expect(ComplainedContent.values.byName('plan'), ComplainedContent.plan);
expect(ComplainedContent.values.byName('recipe'), ComplainedContent.recipe);
});
test('toString returns correct name', () {
expect(ComplainedContent.diet.toString(), 'ComplainedContent.diet');
expect(ComplainedContent.plan.toString(), 'ComplainedContent.plan');
expect(ComplainedContent.recipe.toString(), 'ComplainedContent.recipe');
});
});
}

View File

@@ -30,5 +30,23 @@ void main() {
expect(FiltersComparisonEnum.equal == FiltersComparisonEnum.equal, true);
expect(FiltersComparisonEnum.equal == FiltersComparisonEnum.notEqual, false);
});
test('has correct index', () {
expect(FiltersComparisonEnum.equal.index, 0);
expect(FiltersComparisonEnum.notEqual.index, 1);
expect(FiltersComparisonEnum.greaterThan.index, 2);
expect(FiltersComparisonEnum.lessThan.index, 3);
});
test('can access by name', () {
expect(FiltersComparisonEnum.values.byName('equal'), FiltersComparisonEnum.equal);
expect(FiltersComparisonEnum.values.byName('notEqual'), FiltersComparisonEnum.notEqual);
expect(FiltersComparisonEnum.values.byName('greaterThan'), FiltersComparisonEnum.greaterThan);
});
test('toString returns correct name', () {
expect(FiltersComparisonEnum.equal.toString(), 'FiltersComparisonEnum.equal');
expect(FiltersComparisonEnum.notEqual.toString(), 'FiltersComparisonEnum.notEqual');
});
});
}

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

@@ -20,5 +20,17 @@ void main() {
expect(ProductType.plan.index, 1);
expect(ProductType.recipe.index, 2);
});
test('can access by name', () {
expect(ProductType.values.byName('diet'), ProductType.diet);
expect(ProductType.values.byName('plan'), ProductType.plan);
expect(ProductType.values.byName('recipe'), ProductType.recipe);
});
test('toString returns correct name', () {
expect(ProductType.diet.toString(), 'ProductType.diet');
expect(ProductType.plan.toString(), 'ProductType.plan');
expect(ProductType.recipe.toString(), 'ProductType.recipe');
});
});
}

View File

@@ -116,5 +116,48 @@ void main() {
test('has yandexRedirectUri getter', () {
expect(EnvConfig.yandexRedirectUri, isA<String>());
});
test('all getters return strings', () {
expect(EnvConfig.baseUrl, isA<String>());
expect(EnvConfig.devBaseUrl, isA<String>());
expect(EnvConfig.sentryDsn, isA<String>());
expect(EnvConfig.appMetricaApiKey, isA<String>());
expect(EnvConfig.firebaseProjectId, isA<String>());
expect(EnvConfig.firebaseDatabaseUrl, isA<String>());
expect(EnvConfig.firebaseStorageBucket, isA<String>());
expect(EnvConfig.firebaseMessagingSenderId, isA<String>());
expect(EnvConfig.firebaseAndroidApiKey, isA<String>());
expect(EnvConfig.firebaseAndroidAppId, isA<String>());
expect(EnvConfig.firebaseIosApiKey, isA<String>());
expect(EnvConfig.firebaseIosAppId, isA<String>());
expect(EnvConfig.firebaseIosBundleId, isA<String>());
expect(EnvConfig.appStoreUrl, isA<String>());
expect(EnvConfig.rustoreUrl, isA<String>());
expect(EnvConfig.telegramSupportUrl, isA<String>());
expect(EnvConfig.supportEmail, isA<String>());
expect(EnvConfig.testEmail, isA<String>());
expect(EnvConfig.testPassword, isA<String>());
expect(EnvConfig.emailValidationApiUrl, isA<String>());
expect(EnvConfig.googleCheckUrl, isA<String>());
expect(EnvConfig.yandexCheckUrl, isA<String>());
expect(EnvConfig.vkClientIdIos, isA<String>());
expect(EnvConfig.vkClientIdAndroid, isA<String>());
expect(EnvConfig.yandexClientId, isA<String>());
expect(EnvConfig.yandexClientSecret, isA<String>());
expect(EnvConfig.yandexRedirectUri, isA<String>());
});
test('getters return empty string when env variable is not set', () {
// В тестах мы загружаем только DEV_BASE_URL и BASE_URL
// Остальные должны вернуть пустую строку
expect(EnvConfig.sentryDsn, '');
expect(EnvConfig.appMetricaApiKey, '');
expect(EnvConfig.firebaseProjectId, '');
});
test('baseUrl and devBaseUrl are set from env', () {
expect(EnvConfig.baseUrl, 'http://localhost');
expect(EnvConfig.devBaseUrl, 'http://localhost');
});
});
}

View File

@@ -208,5 +208,223 @@ void main() {
}
}
});
test('compressFilesList - обрабатывает список с изображениями разных форматов', () async {
final jpegFile = File('test_file.jpg');
final pngFile = File('test_file.png');
final gifFile = File('test_file.gif');
final nonImageFile = File('test_file.txt');
try {
// Создаем минимальные файлы
await jpegFile.writeAsBytes([0xFF, 0xD8, 0xFF, 0xE0]);
await pngFile.writeAsBytes([0x89, 0x50, 0x4E, 0x47]);
await gifFile.writeAsBytes([0x47, 0x49, 0x46, 0x38]);
await nonImageFile.writeAsBytes([0x54, 0x45, 0x58, 0x54]);
final testFiles = [jpegFile, pngFile, gifFile, nonImageFile];
final result = await ImageCompressUtility.compressFilesList(testFiles);
expect(result, isA<List<File>>());
expect(result.length, 4);
// В тестовом окружении все файлы возвращаются как есть
expect(result[0].path, jpegFile.path);
expect(result[1].path, pngFile.path);
expect(result[2].path, gifFile.path);
expect(result[3].path, nonImageFile.path);
} finally {
for (final file in [jpegFile, pngFile, gifFile, nonImageFile]) {
if (await file.exists()) {
await file.delete();
}
}
}
});
test('compressFilesList - обрабатывает пустой список', () async {
final result = await ImageCompressUtility.compressFilesList([]);
expect(result, isA<List<File>>());
expect(result.length, 0);
});
test('compressFilesList - обрабатывает список только с не-изображениями', () async {
final txtFile = File('test_file.txt');
final pdfFile = File('test_file.pdf');
try {
await txtFile.writeAsBytes([0x54, 0x45, 0x58, 0x54]);
await pdfFile.writeAsBytes([0x25, 0x50, 0x44, 0x46]);
final testFiles = [txtFile, pdfFile];
final result = await ImageCompressUtility.compressFilesList(testFiles);
expect(result, isA<List<File>>());
expect(result.length, 2);
expect(result[0].path, txtFile.path);
expect(result[1].path, pdfFile.path);
} finally {
for (final file in [txtFile, pdfFile]) {
if (await file.exists()) {
await file.delete();
}
}
}
});
test('compressFilesList - обрабатывает файлы с разными расширениями изображений', () async {
final jpgFile = File('test_file.jpg');
final jpegFile = File('test_file.jpeg');
final pngFile = File('test_file.PNG'); // uppercase
final bmpFile = File('test_file.bmp');
final webpFile = File('test_file.webp');
try {
await jpgFile.writeAsBytes([0xFF, 0xD8]);
await jpegFile.writeAsBytes([0xFF, 0xD8]);
await pngFile.writeAsBytes([0x89, 0x50]);
await bmpFile.writeAsBytes([0x42, 0x4D]);
await webpFile.writeAsBytes([0x52, 0x49, 0x46, 0x46]);
final testFiles = [jpgFile, jpegFile, pngFile, bmpFile, webpFile];
final result = await ImageCompressUtility.compressFilesList(testFiles);
expect(result, isA<List<File>>());
expect(result.length, 5);
} finally {
for (final file in [jpgFile, jpegFile, pngFile, bmpFile, webpFile]) {
if (await file.exists()) {
await file.delete();
}
}
}
});
test('compressFile - обрабатывает PNG файл', () async {
final pngFile = File('test_file.png');
try {
// Минимальный валидный PNG
final pngBytes = [
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
0x00, 0x00, 0x00, 0x0D, // IHDR chunk length
0x49, 0x48, 0x44, 0x52, // IHDR
0x00, 0x00, 0x00, 0x01, // width
0x00, 0x00, 0x00, 0x01, // height
0x08, 0x02, 0x00, 0x00, 0x00, // bit depth, color type, etc.
0x90, 0x77, 0x53, 0xDE, // CRC
0x00, 0x00, 0x00, 0x00, // IEND chunk length
0x49, 0x45, 0x4E, 0x44, // IEND
0xAE, 0x42, 0x60, 0x82, // CRC
];
await pngFile.writeAsBytes(pngBytes);
final result = await ImageCompressUtility.compressFile(pngFile);
expect(result, isA<File>());
expect(result.path, pngFile.path);
} finally {
if (await pngFile.exists()) {
await pngFile.delete();
}
}
});
test('compressFile - обрабатывает файл с расширением в верхнем регистре', () async {
final jpgFile = File('test_file.JPG');
try {
final jpegBytes = [0xFF, 0xD8, 0xFF, 0xE0];
await jpgFile.writeAsBytes(jpegBytes);
final result = await ImageCompressUtility.compressFile(jpgFile);
expect(result, isA<File>());
expect(result.path, jpgFile.path);
} finally {
if (await jpgFile.exists()) {
await jpgFile.delete();
}
}
});
test('compressFilesList - обрабатывает смешанный список изображений и не-изображений', () async {
final jpgFile = File('test_file.jpg');
final txtFile = File('test_file.txt');
final pngFile = File('test_file.png');
try {
await jpgFile.writeAsBytes([0xFF, 0xD8]);
await txtFile.writeAsBytes([0x54, 0x45, 0x58, 0x54]);
await pngFile.writeAsBytes([0x89, 0x50]);
final testFiles = [jpgFile, txtFile, pngFile];
final result = await ImageCompressUtility.compressFilesList(testFiles);
expect(result, isA<List<File>>());
expect(result.length, 3);
expect(result[0].path, jpgFile.path);
expect(result[1].path, txtFile.path);
expect(result[2].path, pngFile.path);
} finally {
for (final file in [jpgFile, txtFile, pngFile]) {
if (await file.exists()) {
await file.delete();
}
}
}
});
test('compressFilesList - обрабатывает файлы с длинными путями', () async {
final longPathFile = File('very_long_path_name_for_testing_image_compression.jpg');
try {
await longPathFile.writeAsBytes([0xFF, 0xD8]);
final testFiles = [longPathFile];
final result = await ImageCompressUtility.compressFilesList(testFiles);
expect(result, isA<List<File>>());
expect(result.length, 1);
expect(result[0].path, longPathFile.path);
} finally {
if (await longPathFile.exists()) {
await longPathFile.delete();
}
}
});
test('compressFilesList - обрабатывает файлы с точками в имени', () async {
final fileWithDots = File('test.file.name.jpg');
try {
await fileWithDots.writeAsBytes([0xFF, 0xD8]);
final testFiles = [fileWithDots];
final result = await ImageCompressUtility.compressFilesList(testFiles);
expect(result, isA<List<File>>());
expect(result.length, 1);
expect(result[0].path, fileWithDots.path);
} finally {
if (await fileWithDots.exists()) {
await fileWithDots.delete();
}
}
});
test('compressFilesList - обрабатывает файлы без расширения', () async {
final fileNoExt = File('test_file_no_extension');
try {
await fileNoExt.writeAsBytes([0xFF, 0xD8]);
final testFiles = [fileNoExt];
final result = await ImageCompressUtility.compressFilesList(testFiles);
expect(result, isA<List<File>>());
expect(result.length, 1);
expect(result[0].path, fileNoExt.path);
} finally {
if (await fileNoExt.exists()) {
await fileNoExt.delete();
}
}
});
});
}

View File

@@ -85,5 +85,79 @@ void main() {
}
expect(cubit.isClosed, true);
});
test('state is initialized correctly', () {
expect(cubit.state, isA<InternetConnectionState>());
expect(cubit.state.isInternetAvailable, isA<bool>());
});
test('cubit can be created multiple times', () async {
// Обрабатываем асинхронные исключения при создании нескольких экземпляров
final completer1 = Completer<void>();
final completer2 = Completer<void>();
late InternetConnectionCubit cubit1;
late InternetConnectionCubit cubit2;
runZonedGuarded(() {
cubit1 = InternetConnectionCubit();
Future.delayed(const Duration(milliseconds: 300)).then((_) {
if (!completer1.isCompleted) {
completer1.complete();
}
});
}, (error, stack) {
if (!completer1.isCompleted) {
completer1.complete();
}
});
runZonedGuarded(() {
cubit2 = InternetConnectionCubit();
Future.delayed(const Duration(milliseconds: 300)).then((_) {
if (!completer2.isCompleted) {
completer2.complete();
}
});
}, (error, stack) {
if (!completer2.isCompleted) {
completer2.complete();
}
});
await completer1.future;
await completer2.future;
expect(cubit1, isA<InternetConnectionCubit>());
expect(cubit2, isA<InternetConnectionCubit>());
expect(cubit1 != cubit2, true);
try {
await cubit1.close();
await cubit2.close();
} catch (e) {
// Игнорируем ошибки при закрытии
}
});
test('checkConnection method exists and can be called', () async {
final completer = Completer<void>();
runZonedGuarded(() async {
try {
await cubit.checkConnection();
} catch (e) {
// Игнорируем исключения
}
if (!completer.isCompleted) {
completer.complete();
}
}, (error, stack) {
if (!completer.isCompleted) {
completer.complete();
}
});
await completer.future;
expect(cubit.state, isA<InternetConnectionState>());
});
});
}

View File

@@ -48,6 +48,22 @@ void main() {
expect(state1 == state2, false);
});
test('copyWith creates new instance with updated values', () {
final original = InternetConnectionState(isInternetAvailable: true);
final updated = original.copyWith(isInternetAvailable: false);
expect(updated.isInternetAvailable, false);
expect(original.isInternetAvailable, true);
});
test('copyWith keeps original values when not updated', () {
final original = InternetConnectionState(isInternetAvailable: true);
final updated = original.copyWith();
expect(updated.isInternetAvailable, true);
expect(original.isInternetAvailable, updated.isInternetAvailable);
});
});
}

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,26 @@ void main() {
expect(PrefsKeys.welcomeScreenWasShowed.name, 'welcomeScreenWasShowed');
expect(PrefsKeys.registrationDate.name, 'registrationDate');
expect(PrefsKeys.marketingBottomSheetHasBeenShown.name, 'marketingBottomSheetHasBeenShown');
expect(PrefsKeys.isEmailVerificationSkipped.name, 'isEmailVerificationSkipped');
});
test('can be compared', () {
expect(PrefsKeys.hasAnonymousTokenBeenSent == PrefsKeys.hasAnonymousTokenBeenSent, true);
expect(PrefsKeys.hasAnonymousTokenBeenSent == PrefsKeys.welcomeScreenWasShowed, false);
});
test('has correct index', () {
expect(PrefsKeys.hasAnonymousTokenBeenSent.index, 0);
expect(PrefsKeys.welcomeScreenWasShowed.index, 1);
expect(PrefsKeys.registrationDate.index, 2);
expect(PrefsKeys.marketingBottomSheetHasBeenShown.index, 3);
});
test('can access by name', () {
expect(PrefsKeys.values.byName('hasAnonymousTokenBeenSent'), PrefsKeys.hasAnonymousTokenBeenSent);
expect(PrefsKeys.values.byName('welcomeScreenWasShowed'), PrefsKeys.welcomeScreenWasShowed);
expect(PrefsKeys.values.byName('registrationDate'), PrefsKeys.registrationDate);
expect(PrefsKeys.values.byName('marketingBottomSheetHasBeenShown'), PrefsKeys.marketingBottomSheetHasBeenShown);
});
});
}

View File

@@ -135,6 +135,192 @@ void main() {
expect(blogPostRoute.builder, isNotNull);
});
test('aboutPlanScreen builder обрабатывает slug из query параметров с navigateToComments false', () {
final routes = ContentRoutes.createRoutes();
final planRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/about-plan-screen',
) as GoRoute;
expect(planRoute.builder, isNotNull);
expect(planRoute.name, 'aboutPlanScreen');
});
test('aboutRecipesScreen builder обрабатывает slug из query параметров с navigateToComments false', () {
final routes = ContentRoutes.createRoutes();
final recipeRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/about-recipes-screen',
) as GoRoute;
expect(recipeRoute.builder, isNotNull);
expect(recipeRoute.name, 'aboutRecipesScreen');
});
test('aboutDietScreen builder обрабатывает slug из query параметров с index null', () {
final routes = ContentRoutes.createRoutes();
final dietRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/about-diet-screen',
) as GoRoute;
expect(dietRoute.builder, isNotNull);
expect(dietRoute.name, 'aboutDietScreen');
});
test('blogPostScreen builder обрабатывает id из query параметров как int', () {
final routes = ContentRoutes.createRoutes();
final blogPostRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/blog-post-screen',
) as GoRoute;
expect(blogPostRoute.builder, isNotNull);
expect(blogPostRoute.name, 'blogPostScreen');
});
test('blogPostScreen builder обрабатывает id из extra как int', () {
final routes = ContentRoutes.createRoutes();
final blogPostRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/blog-post-screen',
) as GoRoute;
expect(blogPostRoute.builder, isNotNull);
});
test('aboutPlanScreen builder обрабатывает extra данные с index', () {
final routes = ContentRoutes.createRoutes();
final planRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/about-plan-screen',
) as GoRoute;
expect(planRoute.builder, isNotNull);
});
test('aboutRecipesScreen builder обрабатывает extra данные с index', () {
final routes = ContentRoutes.createRoutes();
final recipeRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/about-recipes-screen',
) as GoRoute;
expect(recipeRoute.builder, isNotNull);
});
test('aboutDietScreen builder обрабатывает extra данные с id и slug', () {
final routes = ContentRoutes.createRoutes();
final dietRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/about-diet-screen',
) as GoRoute;
expect(dietRoute.builder, isNotNull);
});
test('blogPostCreateScreen builder обрабатывает null extra', () {
final routes = ContentRoutes.createRoutes();
final blogPostCreateRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/blog-post-create-screen',
) as GoRoute;
expect(blogPostCreateRoute.builder, isNotNull);
});
test('aboutPlanScreen builder обрабатывает slug из query параметров с navigateToComments false', () {
final routes = ContentRoutes.createRoutes();
final planRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/about-plan-screen',
) as GoRoute;
expect(planRoute.builder, isNotNull);
expect(planRoute.name, 'aboutPlanScreen');
});
test('aboutRecipesScreen builder обрабатывает slug из query параметров с navigateToComments false', () {
final routes = ContentRoutes.createRoutes();
final recipeRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/about-recipes-screen',
) as GoRoute;
expect(recipeRoute.builder, isNotNull);
expect(recipeRoute.name, 'aboutRecipesScreen');
});
test('aboutDietScreen builder обрабатывает slug из query параметров с index null', () {
final routes = ContentRoutes.createRoutes();
final dietRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/about-diet-screen',
) as GoRoute;
expect(dietRoute.builder, isNotNull);
expect(dietRoute.name, 'aboutDietScreen');
});
test('blogPostScreen builder обрабатывает id из query параметров как int', () {
final routes = ContentRoutes.createRoutes();
final blogPostRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/blog-post-screen',
) as GoRoute;
expect(blogPostRoute.builder, isNotNull);
expect(blogPostRoute.name, 'blogPostScreen');
});
test('blogPostScreen builder обрабатывает id из extra как int', () {
final routes = ContentRoutes.createRoutes();
final blogPostRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/blog-post-screen',
) as GoRoute;
expect(blogPostRoute.builder, isNotNull);
});
test('aboutPlanScreen builder обрабатывает extra данные с index', () {
final routes = ContentRoutes.createRoutes();
final planRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/about-plan-screen',
) as GoRoute;
expect(planRoute.builder, isNotNull);
});
test('aboutRecipesScreen builder обрабатывает extra данные с index', () {
final routes = ContentRoutes.createRoutes();
final recipeRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/about-recipes-screen',
) as GoRoute;
expect(recipeRoute.builder, isNotNull);
});
test('aboutDietScreen builder обрабатывает extra данные с id и slug', () {
final routes = ContentRoutes.createRoutes();
final dietRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/about-diet-screen',
) as GoRoute;
expect(dietRoute.builder, isNotNull);
});
test('blogPostCreateScreen builder обрабатывает extra данные с FeedBlogPostModel', () {
final routes = ContentRoutes.createRoutes();
final blogPostCreateRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/blog-post-create-screen',
) as GoRoute;
expect(blogPostCreateRoute.builder, isNotNull);
});
test('all routes have correct names', () {
final routes = ContentRoutes.createRoutes();
final planRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/about-plan-screen') as GoRoute;
final recipeRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/about-recipes-screen') as GoRoute;
final dietRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/about-diet-screen') as GoRoute;
final blogPostCreateRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/blog-post-create-screen') as GoRoute;
final blogPostRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/blog-post-screen') as GoRoute;
expect(planRoute.name, 'aboutPlanScreen');
expect(recipeRoute.name, 'aboutRecipesScreen');
expect(dietRoute.name, 'aboutDietScreen');
expect(blogPostCreateRoute.name, 'blogPostCreateScreen');
expect(blogPostRoute.name, 'blogPostScreen');
});
});
}

View File

@@ -14,6 +14,162 @@ void main() {
final routes = MainNavigationRoutes.createRoutes();
expect(routes.first, isA<ShellRoute>());
});
test('ShellRoute содержит маршрут myProfileScreen', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
final myProfileRoute = shellRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == '/my-profile-screen',
orElse: () => throw Exception('Route not found'),
);
expect(myProfileRoute, isA<GoRoute>());
expect((myProfileRoute as GoRoute).name, 'myProfileScreen');
});
test('ShellRoute содержит маршрут feedScreen', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
final feedRoute = shellRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == '/feed-screen',
orElse: () => throw Exception('Route not found'),
);
expect(feedRoute, isA<GoRoute>());
expect((feedRoute as GoRoute).name, 'feedScreen');
});
test('ShellRoute содержит маршрут diaryScreen', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
final diaryRoute = shellRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == '/diary-screen',
orElse: () => throw Exception('Route not found'),
);
expect(diaryRoute, isA<GoRoute>());
expect((diaryRoute as GoRoute).name, 'diaryScreen');
});
test('ShellRoute содержит маршрут consultationsScreen', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
final consultationsRoute = shellRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == '/consultations-screen',
orElse: () => throw Exception('Route not found'),
);
expect(consultationsRoute, isA<GoRoute>());
});
test('ShellRoute содержит маршрут chatSelectScreen', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
final chatSelectRoute = shellRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == '/chat-select-screen',
orElse: () => throw Exception('Route not found'),
);
expect(chatSelectRoute, isA<GoRoute>());
expect((chatSelectRoute as GoRoute).name, 'chatSelectScreen');
});
test('ShellRoute содержит маршрут notificationsScreen', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
final notificationsRoute = shellRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == '/notifications-screen',
orElse: () => throw Exception('Route not found'),
);
expect(notificationsRoute, isA<GoRoute>());
expect((notificationsRoute as GoRoute).name, 'notificationsScreen');
});
test('ShellRoute содержит маршрут menuScreen', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
final menuRoute = shellRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == '/menu-screen',
orElse: () => throw Exception('Route not found'),
);
expect(menuRoute, isA<GoRoute>());
expect((menuRoute as GoRoute).name, 'menuScreen');
});
test('feedScreen имеет redirect функцию', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
final feedRoute = shellRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == '/feed-screen',
) as GoRoute;
expect(feedRoute.redirect, isNotNull);
});
test('menuScreen использует BlocBuilder для ProfileAvatarCubit', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
final menuRoute = shellRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == '/menu-screen',
) as GoRoute;
expect(menuRoute.pageBuilder, isNotNull);
});
test('ShellRoute builder инициализирует ProfileAvatarCubit', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
expect(shellRoute.builder, isNotNull);
});
test('ShellRoute builder возвращает BottomNavigationBarWidget', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
expect(shellRoute.builder, isNotNull);
});
test('feedScreen redirect инициализирует WsManager', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
final feedRoute = shellRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == '/feed-screen',
) as GoRoute;
expect(feedRoute.redirect, isNotNull);
});
test('menuScreen pageBuilder использует BlocBuilder', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
final menuRoute = shellRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == '/menu-screen',
) as GoRoute;
expect(menuRoute.pageBuilder, isNotNull);
});
test('all routes use NoTransitionPage', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
for (final route in shellRoute.routes) {
if (route is GoRoute && route.pageBuilder != null) {
expect(route.pageBuilder, isNotNull);
}
}
});
test('ShellRoute contains all expected routes', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
expect(shellRoute.routes.length, 7);
expect(shellRoute.routes, isA<List<RouteBase>>());
});
test('ShellRoute builder checks ProfileAvatarStateLoading', () {
final routes = MainNavigationRoutes.createRoutes();
final shellRoute = routes.first as ShellRoute;
expect(shellRoute.builder, isNotNull);
});
});
}

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/routing/other_routes.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
@@ -77,5 +78,149 @@ void main() {
routes.firstWhere((route) => route is GoRoute && route.path == '/update-screen', orElse: () => throw Exception('Route not found')) as GoRoute;
expect(updateRoute.redirect, isNotNull);
});
test('createRoutes содержит маршрут chatScreen', () {
final routes = OtherRoutes.createRoutes();
final chatRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/chat-screen', orElse: () => throw Exception('Route not found'));
expect(chatRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут emptyChatScreen', () {
final routes = OtherRoutes.createRoutes();
final emptyChatRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/empty-chat-screen',
orElse: () => throw Exception('Route not found'),
);
expect(emptyChatRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут createDietScreen', () {
final routes = OtherRoutes.createRoutes();
final createDietRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/create-diet-screen',
orElse: () => throw Exception('Route not found'),
);
expect(createDietRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут dietPreviewScreen', () {
final routes = OtherRoutes.createRoutes();
final dietPreviewRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/create-diet-screen/preview',
orElse: () => throw Exception('Route not found'),
);
expect(dietPreviewRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут myDietsScreen', () {
final routes = OtherRoutes.createRoutes();
final myDietsRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/my-diets-screen', orElse: () => throw Exception('Route not found'));
expect(myDietsRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут myPlansScreen', () {
final routes = OtherRoutes.createRoutes();
final myPlansRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/my-plans-screen', orElse: () => throw Exception('Route not found'));
expect(myPlansRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут myRecipesScreen', () {
final routes = OtherRoutes.createRoutes();
final myRecipesRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/my-recipes-screen',
orElse: () => throw Exception('Route not found'),
);
expect(myRecipesRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут userFinanceScreen', () {
final routes = OtherRoutes.createRoutes();
final userFinanceRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/user-finance-screen',
orElse: () => throw Exception('Route not found'),
);
expect(userFinanceRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут consultationsArchiveScreen', () {
final routes = OtherRoutes.createRoutes();
final consultationsArchiveRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/consultations-archive-screen',
orElse: () => throw Exception('Route not found'),
);
expect(consultationsArchiveRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут consultantFinancesScreen', () {
final routes = OtherRoutes.createRoutes();
final consultantFinancesRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/consultant-finances-screen',
orElse: () => throw Exception('Route not found'),
);
expect(consultantFinancesRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут userPurchasesScreen', () {
final routes = OtherRoutes.createRoutes();
final userPurchasesRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/user-purchases-screen',
orElse: () => throw Exception('Route not found'),
);
expect(userPurchasesRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут stepperBecomeConsultantScreen', () {
final routes = OtherRoutes.createRoutes();
final stepperBecomeConsultantRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/stepper-become-consultant-screen',
orElse: () => throw Exception('Route not found'),
);
expect(stepperBecomeConsultantRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут photoViewScreen', () {
final routes = OtherRoutes.createRoutes();
final photoViewRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == RouteConstants.photoViewScreen,
orElse: () => throw Exception('Route not found'),
);
expect(photoViewRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут userDetailsScreen', () {
final routes = OtherRoutes.createRoutes();
final userDetailsRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/user-details-screen',
orElse: () => throw Exception('Route not found'),
);
expect(userDetailsRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут personalDietSuggestionScreen', () {
final routes = OtherRoutes.createRoutes();
final personalDietSuggestionRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/personal-diet-suggestion',
orElse: () => throw Exception('Route not found'),
);
expect(personalDietSuggestionRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут blockedUsersScreen', () {
final routes = OtherRoutes.createRoutes();
final blockedUsersRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/blocked-users-screen',
orElse: () => throw Exception('Route not found'),
);
expect(blockedUsersRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут notificationsSettingsScreen', () {
final routes = OtherRoutes.createRoutes();
final notificationsSettingsRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/notifications-settings-screen',
orElse: () => throw Exception('Route not found'),
);
expect(notificationsSettingsRoute, isA<GoRoute>());
});
});
}

View File

@@ -98,6 +98,29 @@ void main() {
expect(RouteConstants.consultantFinancesScreen, '/consultant-finances-screen');
expect(RouteConstants.userPurchasesScreen, '/user-purchases-screen');
});
test('all route constants are non-empty strings', () {
expect(RouteConstants.authScreen.isNotEmpty, isTrue);
expect(RouteConstants.feedScreen.isNotEmpty, isTrue);
expect(RouteConstants.profileScreen.isNotEmpty, isTrue);
expect(RouteConstants.settingsScreen.isNotEmpty, isTrue);
expect(RouteConstants.welcomeScreen.isNotEmpty, isTrue);
expect(RouteConstants.updateScreen.isNotEmpty, isTrue);
});
test('route constants have correct format', () {
// Auth routes should start with / or be relative
expect(RouteConstants.authScreen.startsWith('/'), isTrue);
expect(RouteConstants.verifyScreen.startsWith('/'), isTrue);
// Main navigation routes should start with /
expect(RouteConstants.feedScreen.startsWith('/'), isTrue);
expect(RouteConstants.diaryScreen.startsWith('/'), isTrue);
// Content routes should start with /
expect(RouteConstants.aboutPlanScreen.startsWith('/'), isTrue);
expect(RouteConstants.blogPostScreen.startsWith('/'), isTrue);
});
});
}

View File

@@ -21,6 +21,94 @@ void main() {
final settingsRoute = routes.first as GoRoute;
expect(settingsRoute.routes, isNotEmpty);
});
test('createRoutes содержит маршрут generalInfoScreen', () {
final routes = SettingsRoutes.createRoutes();
final settingsRoute = routes.first as GoRoute;
final generalInfoRoute = settingsRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == 'general-info-screen',
orElse: () => throw Exception('Route not found'),
);
expect(generalInfoRoute, isA<GoRoute>());
expect((generalInfoRoute as GoRoute).name, 'generalInfoScreen');
});
test('createRoutes содержит маршрут otherInfoScreen', () {
final routes = SettingsRoutes.createRoutes();
final settingsRoute = routes.first as GoRoute;
final otherInfoRoute = settingsRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == 'other-info-screen',
orElse: () => throw Exception('Route not found'),
);
expect(otherInfoRoute, isA<GoRoute>());
expect((otherInfoRoute as GoRoute).name, 'otherInfoScreen');
});
test('createRoutes содержит маршрут physicalActivityScreen', () {
final routes = SettingsRoutes.createRoutes();
final settingsRoute = routes.first as GoRoute;
final physicalActivityRoute = settingsRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == 'physical-activity-screen',
orElse: () => throw Exception('Route not found'),
);
expect(physicalActivityRoute, isA<GoRoute>());
expect((physicalActivityRoute as GoRoute).name, 'physicalActivityScreen');
});
test('createRoutes содержит маршрут physicalFormScreen', () {
final routes = SettingsRoutes.createRoutes();
final settingsRoute = routes.first as GoRoute;
final physicalFormRoute = settingsRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == 'physical-form-screen',
orElse: () => throw Exception('Route not found'),
);
expect(physicalFormRoute, isA<GoRoute>());
expect((physicalFormRoute as GoRoute).name, 'physicalFormScreen');
});
test('createRoutes содержит маршрут pressureScreen', () {
final routes = SettingsRoutes.createRoutes();
final settingsRoute = routes.first as GoRoute;
final pressureRoute = settingsRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == 'pressure-screen',
orElse: () => throw Exception('Route not found'),
);
expect(pressureRoute, isA<GoRoute>());
expect((pressureRoute as GoRoute).name, 'pressureScreen');
});
test('createRoutes содержит маршрут changePasswordScreen', () {
final routes = SettingsRoutes.createRoutes();
final settingsRoute = routes.first as GoRoute;
final changePasswordRoute = settingsRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == 'change-password-screen',
orElse: () => throw Exception('Route not found'),
);
expect(changePasswordRoute, isA<GoRoute>());
expect((changePasswordRoute as GoRoute).name, 'changePasswordScreen');
});
test('createRoutes содержит маршрут changeEmailScreen', () {
final routes = SettingsRoutes.createRoutes();
final settingsRoute = routes.first as GoRoute;
final changeEmailRoute = settingsRoute.routes.firstWhere(
(route) => route is GoRoute && route.path == 'change-email-screen',
orElse: () => throw Exception('Route not found'),
);
expect(changeEmailRoute, isA<GoRoute>());
expect((changeEmailRoute as GoRoute).name, 'changeEmailScreen');
});
test('all nested routes have builder functions', () {
final routes = SettingsRoutes.createRoutes();
final settingsRoute = routes.first as GoRoute;
for (final route in settingsRoute.routes) {
if (route is GoRoute) {
expect(route.builder, isNotNull);
}
}
});
});
}

View File

@@ -118,5 +118,75 @@ void main() {
expect(result, isA<String>());
});
});
group('DateTimeExtension additional tests', () {
test('format handles UTC dates correctly', () {
final utcDate = DateTime.utc(2023, 12, 25, 10, 30);
final result = utcDate.format('yyyy-MM-dd');
expect(result, isA<String>());
expect(result.isNotEmpty, isTrue);
});
test('format handles local dates correctly', () {
final localDate = DateTime(2023, 12, 25, 10, 30);
final result = localDate.format('yyyy-MM-dd');
expect(result, '2023-12-25');
});
test('format with locale parameter', () {
final date = DateTime(2023, 12, 25);
final result = date.format('dd MMMM', 'ru');
expect(result, isA<String>());
expect(result.isNotEmpty, isTrue);
});
test('isYesterday handles UTC dates', () {
final utcYesterday = DateTime.utc(DateTime.now().year, DateTime.now().month, DateTime.now().day)
.subtract(const Duration(days: 1));
expect(utcYesterday.isYesterday(), isTrue);
});
test('isToday handles UTC dates', () {
final utcToday = DateTime.utc(DateTime.now().year, DateTime.now().month, DateTime.now().day);
expect(utcToday.isToday(), isTrue);
});
test('toISO8601WithTimeZoneOffset includes correct sign', () {
final date = DateTime(2023, 12, 25, 10, 30, 45);
final result = date.toISO8601WithTimeZoneOffset();
expect(result, contains('2023-12-25T10:30:45'));
expect(result.length, greaterThan(19)); // Should have timezone offset
});
});
group('formatBuildDate edge cases', () {
test('handles date with only date part', () {
final result = formatBuildDate('2023-12-25');
expect(result, '25.12.2023');
});
test('handles date with time part', () {
final result = formatBuildDate('2023-12-25 23:59:59');
expect(result, '25.12.2023');
});
test('handles malformed date string', () {
// Функция разбирает строку с 3 частями через дефис и переставляет их
// 'not-a-date' -> ['not', 'a', 'date'] -> 'date.a.not'
final result = formatBuildDate('not-a-date');
expect(result, 'date.a.not');
});
test('handles string that cannot be parsed as date', () {
// Строка с другим количеством дефисов не будет разобрана
final result = formatBuildDate('not-a-valid-date-string');
expect(result, 'not-a-valid-date-string');
});
test('handles date with extra spaces', () {
final result = formatBuildDate('2023-12-25 10:30:00');
expect(result, '25.12.2023');
});
});
});
}

View File

@@ -53,5 +53,205 @@ void main() {
expect(image.contains('assets/') || image.contains(ImageConstant.imagePath), isTrue);
}
});
test('personal cabinet images are defined', () {
expect(ImageConstant.imgEdit, isNotEmpty);
expect(ImageConstant.imgStatistic, isNotEmpty);
expect(ImageConstant.imgStatisticGray200, isNotEmpty);
expect(ImageConstant.imgMegaphone, isNotEmpty);
});
test('common ellipse images are defined', () {
expect(ImageConstant.imgEllipse1, isNotEmpty);
expect(ImageConstant.imgEllipse2, isNotEmpty);
expect(ImageConstant.imgEllipse3, isNotEmpty);
expect(ImageConstant.imgEllipse11, isNotEmpty);
expect(ImageConstant.imgEllipse125x25, isNotEmpty);
expect(ImageConstant.imgEllipse132x32, isNotEmpty);
expect(ImageConstant.imgEllipse134x34, isNotEmpty);
expect(ImageConstant.imgEllipse142x42, isNotEmpty);
});
test('common UI images are defined', () {
expect(ImageConstant.imgGrid, isNotEmpty);
expect(ImageConstant.imgSearch, isNotEmpty);
expect(ImageConstant.imgArea1, isNotEmpty);
expect(ImageConstant.imgLine1, isNotEmpty);
expect(ImageConstant.imgLine2, isNotEmpty);
expect(ImageConstant.imgLine3, isNotEmpty);
expect(ImageConstant.imgEye, isNotEmpty);
expect(ImageConstant.imgFavoriteWhiteA700, isNotEmpty);
expect(ImageConstant.img, isNotEmpty);
});
test('navigation images are defined', () {
expect(ImageConstant.imgDiary, isNotEmpty);
expect(ImageConstant.imgFeed, isNotEmpty);
expect(ImageConstant.imgChat, isNotEmpty);
expect(ImageConstant.imgLogo, isNotEmpty);
expect(ImageConstant.imgBell, isNotEmpty);
expect(ImageConstant.imgSettings, isNotEmpty);
expect(ImageConstant.imgRecipes, isNotEmpty);
expect(ImageConstant.imgDiets, isNotEmpty);
expect(ImageConstant.imgPlans, isNotEmpty);
expect(ImageConstant.imgFinance, isNotEmpty);
expect(ImageConstant.imgSupport, isNotEmpty);
expect(ImageConstant.imgMic, isNotEmpty);
expect(ImageConstant.imgSend, isNotEmpty);
expect(ImageConstant.imgAttach, isNotEmpty);
expect(ImageConstant.imgExit, isNotEmpty);
});
test('navigation color variants are defined', () {
expect(ImageConstant.imgNavBlueGray400, isNotEmpty);
expect(ImageConstant.imgNavBlueGray40024x23, isNotEmpty);
expect(ImageConstant.imgNavPrimary, isNotEmpty);
expect(ImageConstant.imgNavBlueGray40024x25, isNotEmpty);
});
test('icon images are defined', () {
expect(ImageConstant.imgIconMoreHorizontal, isNotEmpty);
expect(ImageConstant.imgIconMessageSquare, isNotEmpty);
expect(ImageConstant.imgIconHeart, isNotEmpty);
expect(ImageConstant.imgIconShare2, isNotEmpty);
});
test('new icons from 21 08 2024 are defined', () {
expect(ImageConstant.icnBell, isNotEmpty);
expect(ImageConstant.icnCcal, isNotEmpty);
expect(ImageConstant.icnChat, isNotEmpty);
expect(ImageConstant.icnClock, isNotEmpty);
expect(ImageConstant.icnComment, isNotEmpty);
expect(ImageConstant.icnConsultation, isNotEmpty);
expect(ImageConstant.icnCopy, isNotEmpty);
expect(ImageConstant.icnDiary, isNotEmpty);
expect(ImageConstant.icnDietCarbs, isNotEmpty);
expect(ImageConstant.icnDietLight, isNotEmpty);
expect(ImageConstant.icnExit, isNotEmpty);
expect(ImageConstant.icnEye, isNotEmpty);
expect(ImageConstant.icnFaceHappy, isNotEmpty);
expect(ImageConstant.icnFaceSad, isNotEmpty);
expect(ImageConstant.icnFavoritePosts, isNotEmpty);
expect(ImageConstant.icnFavoriteRecipes, isNotEmpty);
expect(ImageConstant.icnFeed, isNotEmpty);
expect(ImageConstant.icnFinance, isNotEmpty);
expect(ImageConstant.icnGlossary, isNotEmpty);
expect(ImageConstant.icnGym, isNotEmpty);
expect(ImageConstant.icnLikeFilled, isNotEmpty);
expect(ImageConstant.icnLikePlus, isNotEmpty);
expect(ImageConstant.icnLike, isNotEmpty);
expect(ImageConstant.icnMeat, isNotEmpty);
expect(ImageConstant.icnPaste, isNotEmpty);
expect(ImageConstant.icnPlans, isNotEmpty);
expect(ImageConstant.icnPosts, isNotEmpty);
expect(ImageConstant.icnProfileSettings, isNotEmpty);
expect(ImageConstant.icnRating, isNotEmpty);
expect(ImageConstant.icnRecipes, isNotEmpty);
expect(ImageConstant.icnScale, isNotEmpty);
expect(ImageConstant.icnSend, isNotEmpty);
expect(ImageConstant.icnShare, isNotEmpty);
expect(ImageConstant.icnSpoonFork, isNotEmpty);
expect(ImageConstant.icnStepsGraph, isNotEmpty);
expect(ImageConstant.icnSteps, isNotEmpty);
expect(ImageConstant.icnSubscribers, isNotEmpty);
expect(ImageConstant.icnSupport, isNotEmpty);
expect(ImageConstant.icnDevTools, isNotEmpty);
expect(ImageConstant.icnVeggies, isNotEmpty);
});
test('all image constants use correct path format', () {
final allImageConstants = [
ImageConstant.imgEdit,
ImageConstant.imgStatistic,
ImageConstant.imgStatisticGray200,
ImageConstant.imgMegaphone,
ImageConstant.imgEllipse1,
ImageConstant.imgEllipse2,
ImageConstant.imgEllipse3,
ImageConstant.imgEllipse11,
ImageConstant.imgEllipse125x25,
ImageConstant.imgEllipse132x32,
ImageConstant.imgEllipse134x34,
ImageConstant.imgEllipse142x42,
ImageConstant.imgGrid,
ImageConstant.imgSearch,
ImageConstant.imgArea1,
ImageConstant.imgLine1,
ImageConstant.imgLine2,
ImageConstant.imgLine3,
ImageConstant.imgEye,
ImageConstant.imgFavoriteWhiteA700,
ImageConstant.img,
ImageConstant.imgDiary,
ImageConstant.imgFeed,
ImageConstant.imgChat,
ImageConstant.imgLogo,
ImageConstant.imgBell,
ImageConstant.imgSettings,
ImageConstant.imgRecipes,
ImageConstant.imgDiets,
ImageConstant.imgPlans,
ImageConstant.imgFinance,
ImageConstant.imgSupport,
ImageConstant.imgMic,
ImageConstant.imgSend,
ImageConstant.imgAttach,
ImageConstant.imgExit,
ImageConstant.imgNavBlueGray400,
ImageConstant.imgNavBlueGray40024x23,
ImageConstant.imgNavPrimary,
ImageConstant.imgNavBlueGray40024x25,
ImageConstant.imgIconMoreHorizontal,
ImageConstant.imgIconMessageSquare,
ImageConstant.imgIconHeart,
ImageConstant.imgIconShare2,
ImageConstant.imgRectangle14250x311,
ImageConstant.icnBell,
ImageConstant.icnCcal,
ImageConstant.icnChat,
ImageConstant.icnClock,
ImageConstant.icnComment,
ImageConstant.icnConsultation,
ImageConstant.icnCopy,
ImageConstant.icnDiary,
ImageConstant.icnDietCarbs,
ImageConstant.icnDietLight,
ImageConstant.icnExit,
ImageConstant.icnEye,
ImageConstant.icnFaceHappy,
ImageConstant.icnFaceSad,
ImageConstant.icnFavoritePosts,
ImageConstant.icnFavoriteRecipes,
ImageConstant.icnFeed,
ImageConstant.icnFinance,
ImageConstant.icnGlossary,
ImageConstant.icnGym,
ImageConstant.icnLikeFilled,
ImageConstant.icnLikePlus,
ImageConstant.icnLike,
ImageConstant.icnMeat,
ImageConstant.icnPaste,
ImageConstant.icnPlans,
ImageConstant.icnPosts,
ImageConstant.icnProfileSettings,
ImageConstant.icnRating,
ImageConstant.icnRecipes,
ImageConstant.icnScale,
ImageConstant.icnSend,
ImageConstant.icnShare,
ImageConstant.icnSpoonFork,
ImageConstant.icnStepsGraph,
ImageConstant.icnSteps,
ImageConstant.icnSubscribers,
ImageConstant.icnSupport,
ImageConstant.icnDevTools,
ImageConstant.icnVeggies,
];
for (final image in allImageConstants) {
expect(image, isNotEmpty);
expect(image.contains('assets/') || image.contains(ImageConstant.imagePath), isTrue);
}
});
});
}

View File

@@ -362,6 +362,191 @@ void main() {
),
);
});
testWidgets('returns correct form for 21-24 steps', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Builder(
builder: (context) {
final result21 = getStepForm(21);
final result22 = getStepForm(22);
final result23 = getStepForm(23);
final result24 = getStepForm(24);
expect(result21, isA<String>());
expect(result22, isA<String>());
expect(result23, isA<String>());
expect(result24, isA<String>());
return Container();
},
),
),
);
});
});
group('Edge cases for pluralization', () {
test('getDayForm handles 0 days', () {
final result = getDayForm(0, s);
expect(result, isA<String>());
});
test('getDayForm handles 100+ days', () {
final result = getDayForm(100, s);
expect(result, isA<String>());
final result101 = getDayForm(101, s);
expect(result101, isA<String>());
});
test('getCommentForm handles 0 comments', () {
final result = getCommentForm(0, s);
expect(result, isA<String>());
});
test('getCommentForm handles 100+ comments', () {
final result = getCommentForm(100, s);
expect(result, isA<String>());
final result101 = getCommentForm(101, s);
expect(result101, isA<String>());
});
testWidgets('getMinuteForm handles 0 minutes', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Builder(
builder: (context) {
final result = getMinuteForm(0);
expect(result, isA<String>());
return Container();
},
),
),
);
});
testWidgets('getMinuteForm handles 100+ minutes', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Builder(
builder: (context) {
final result = getMinuteForm(100);
expect(result, isA<String>());
final result101 = getMinuteForm(101);
expect(result101, isA<String>());
return Container();
},
),
),
);
});
testWidgets('getHourForm handles 0 hours', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Builder(
builder: (context) {
final result = getHourForm(0);
expect(result, isA<String>());
return Container();
},
),
),
);
});
testWidgets('getHourForm handles 100+ hours', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Builder(
builder: (context) {
final result = getHourForm(100);
expect(result, isA<String>());
final result101 = getHourForm(101);
expect(result101, isA<String>());
return Container();
},
),
),
);
});
testWidgets('getStepForm handles 0 steps', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Builder(
builder: (context) {
final result = getStepForm(0);
expect(result, isA<String>());
return Container();
},
),
),
);
});
testWidgets('getStepForm handles 100+ steps', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Builder(
builder: (context) {
final result = getStepForm(100);
expect(result, isA<String>());
final result101 = getStepForm(101);
expect(result101, isA<String>());
return Container();
},
),
),
);
});
});
});
}

View File

@@ -0,0 +1,202 @@
import 'package:eatvio_app/core/utils/markdown_utils.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('MarkdownUtils', () {
test('processMarkdown - обрабатывает простой текст без изменений', () {
const text = 'Простой текст без форматирования';
final result = MarkdownUtils.processMarkdown(text);
expect(result, text);
});
test('processMarkdown - преобразует URL в markdown ссылки', () {
const text = 'Посетите https://example.com для получения информации';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[https://example.com](https://example.com)'));
});
test('processMarkdown - преобразует www URL в markdown ссылки', () {
const text = 'Посетите www.example.com для получения информации';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[www.example.com](https://www.example.com)'));
});
test('processMarkdown - преобразует http URL в markdown ссылки', () {
const text = 'Посетите http://example.com для получения информации';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[http://example.com](http://example.com)'));
});
test('processMarkdown - не преобразует URL внутри существующих markdown ссылок', () {
const text = 'Ссылка [https://example.com](https://example.com) уже в формате markdown';
final result = MarkdownUtils.processMarkdown(text);
// URL не должен быть преобразован дважды
expect(result, contains('[https://example.com](https://example.com)'));
// Проверяем, что нет дублирования
final matches = RegExp(r'\[https://example\.com\]\(https://example\.com\)').allMatches(result);
expect(matches.length, 1);
});
test('processMarkdown - исправляет неправильный синтаксис markdown ссылок', () {
const text = 'Неправильная ссылка [https://example.com](url)';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[https://example.com](https://example.com)'));
expect(result, isNot(contains('(url)')));
});
test('processMarkdown - исправляет неправильный синтаксис с www', () {
const text = 'Неправильная ссылка [www.example.com](url)';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[www.example.com](https://www.example.com)'));
});
test('processMarkdown - не исправляет ссылки без http или www', () {
const text = 'Ссылка [example.com](url) не должна быть исправлена';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[example.com](url)'));
});
test('processMarkdown - преобразует жирный текст **текст** в <b>текст</b>', () {
const text = 'Это **жирный текст** в markdown';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('<b>жирный текст</b>'));
expect(result, isNot(contains('**жирный текст**')));
});
test('processMarkdown - обрабатывает несколько жирных фрагментов', () {
const text = 'Это **первый** и **второй** жирный текст';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('<b>первый</b>'));
expect(result, contains('<b>второй</b>'));
});
test('processMarkdown - обрабатывает URL и жирный текст вместе', () {
const text = 'Посетите https://example.com для **важной** информации';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[https://example.com](https://example.com)'));
expect(result, contains('<b>важной</b>'));
});
test('processMarkdown - обрабатывает несколько URL в тексте', () {
const text = 'Ссылки: https://example.com и www.test.com';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[https://example.com](https://example.com)'));
expect(result, contains('[www.test.com](https://www.test.com)'));
});
test('processMarkdown - обрабатывает URL с параметрами', () {
const text = 'Ссылка https://example.com?param=value&other=test';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[https://example.com?param=value&other=test](https://example.com?param=value&other=test)'));
});
test('processMarkdown - обрабатывает URL в конце предложения', () {
const text = 'Посетите https://example.com.';
final result = MarkdownUtils.processMarkdown(text);
// Точка включается в URL, так как регулярное выражение захватывает её
expect(result, contains('[https://example.com.](https://example.com.)'));
});
test('processMarkdown - обрабатывает URL в скобках', () {
const text = 'Ссылка (https://example.com) в скобках';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[https://example.com](https://example.com)'));
});
test('processMarkdown - обрабатывает пустую строку', () {
const text = '';
final result = MarkdownUtils.processMarkdown(text);
expect(result, '');
});
test('processMarkdown - обрабатывает текст только с пробелами', () {
const text = ' ';
final result = MarkdownUtils.processMarkdown(text);
expect(result, ' ');
});
test('processMarkdown - обрабатывает URL с портом', () {
const text = 'Ссылка https://example.com:8080/path';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[https://example.com:8080/path](https://example.com:8080/path)'));
});
test('processMarkdown - обрабатывает URL с якорем', () {
const text = 'Ссылка https://example.com#section';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[https://example.com#section](https://example.com#section)'));
});
test('processMarkdown - обрабатывает сложный текст с множеством элементов', () {
const text = 'Это **важный** текст с https://example.com и **другим** жирным текстом. Также www.test.com';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('<b>важный</b>'));
expect(result, contains('<b>другим</b>'));
expect(result, contains('[https://example.com](https://example.com)'));
expect(result, contains('[www.test.com](https://www.test.com)'));
});
test('processMarkdown - обрабатывает URL с подчеркиванием', () {
const text = 'Ссылка https://example.com/path_with_underscore';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[https://example.com/path_with_underscore](https://example.com/path_with_underscore)'));
});
test('processMarkdown - обрабатывает URL с дефисом', () {
const text = 'Ссылка https://example-site.com/path';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[https://example-site.com/path](https://example-site.com/path)'));
});
test('processMarkdown - обрабатывает несколько жирных фрагментов подряд', () {
const text = '**первый****второй**';
final result = MarkdownUtils.processMarkdown(text);
// Обрабатывает только первый найденный фрагмент
expect(result, isA<String>());
});
test('processMarkdown - обрабатывает URL внутри текста с жирным форматированием', () {
const text = '**Важно**: посетите https://example.com';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('<b>Важно</b>'));
expect(result, contains('[https://example.com](https://example.com)'));
});
test('processMarkdown - обрабатывает неправильный синтаксис с http', () {
const text = 'Ссылка [http://example.com](wrong)';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[http://example.com](http://example.com)'));
});
test('processMarkdown - обрабатывает URL в начале строки', () {
const text = 'https://example.com это ссылка';
final result = MarkdownUtils.processMarkdown(text);
expect(result, startsWith('[https://example.com](https://example.com)'));
});
test('processMarkdown - обрабатывает URL в конце строки', () {
const text = 'Ссылка https://example.com';
final result = MarkdownUtils.processMarkdown(text);
expect(result, endsWith('[https://example.com](https://example.com)'));
});
test('processMarkdown - обрабатывает текст только с жирным форматированием', () {
const text = '**Только жирный текст**';
final result = MarkdownUtils.processMarkdown(text);
expect(result, '<b>Только жирный текст</b>');
});
test('processMarkdown - обрабатывает текст только с URL', () {
const text = 'https://example.com';
final result = MarkdownUtils.processMarkdown(text);
expect(result, '[https://example.com](https://example.com)');
});
test('processMarkdown - обрабатывает смешанный контент с правильными ссылками', () {
const text = 'Текст с [правильной ссылкой](https://example.com) и https://other.com';
final result = MarkdownUtils.processMarkdown(text);
expect(result, contains('[правильной ссылкой](https://example.com)'));
expect(result, contains('[https://other.com](https://other.com)'));
});
});
}

View File

@@ -35,5 +35,56 @@ void main() {
expect(loadNewContent, isA<Function>());
});
testWidgets('loadNewContent triggers load when near bottom', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: NotificationListener<ScrollNotification>(
onNotification: (notification) {
loadNewContent(notification, () async{});
return true;
},
child: SingleChildScrollView(
child: Container(height: 2000),
),
),
),
),
);
// Scroll near bottom
await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -1900));
await tester.pump();
// loadNewContent should be called
expect(loadNewContent, isA<Function>());
});
testWidgets('loadNewContent does not trigger load when not near bottom', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: NotificationListener<ScrollNotification>(
onNotification: (notification) {
loadNewContent(notification, () async{});
return true;
},
child: SingleChildScrollView(
child: Container(height: 2000),
),
),
),
),
);
// Scroll a little bit
await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -100));
await tester.pump();
expect(loadNewContent, isA<Function>());
});
});
}

View File

@@ -38,6 +38,29 @@ void main() {
expect('unknown'.parseGender(), 'unknown');
expect(''.parseGender(), '');
});
test('returns original string for case variations', () {
expect('Male'.parseGender(), 'Male');
expect('Female'.parseGender(), 'Female');
expect('MALE'.parseGender(), 'MALE');
expect('FEMALE'.parseGender(), 'FEMALE');
});
});
group('capitalize edge cases', () {
test('handles strings with numbers', () {
expect('hello123'.capitalize(), 'Hello123');
expect('123hello'.capitalize(), '123hello');
});
test('handles strings with special characters', () {
expect('hello-world'.capitalize(), 'Hello-world');
expect('hello_world'.capitalize(), 'Hello_world');
});
test('handles strings with spaces', () {
expect('hello world'.capitalize(), 'Hello world');
});
});
});
}

View File

@@ -68,5 +68,47 @@ void main() {
showSuccessToast(message);
}
});
test('showInfoToast handles empty string', () {
showInfoToast('');
});
test('showErrorToast handles empty string', () {
showErrorToast('');
});
test('showSuccessToast handles empty string', () {
showSuccessToast('');
});
test('showInfoToast handles long messages', () {
showInfoToast('A' * 200);
});
test('showErrorToast handles long messages', () {
showErrorToast('A' * 200);
});
test('showSuccessToast handles long messages', () {
showSuccessToast('A' * 200);
});
test('showInfoToast handles messages with special characters', () {
showInfoToast('Message with special chars: !@#\$%^&*()');
});
test('showErrorToast handles messages with special characters', () {
showErrorToast('Error with special chars: !@#\$%^&*()');
});
test('showSuccessToast handles messages with special characters', () {
showSuccessToast('Success with special chars: !@#\$%^&*()');
});
test('showInternetConnectionToast can be called multiple times', () {
showInternetConnectionToast();
showInternetConnectionToast();
showInternetConnectionToast();
});
});
}

View File

@@ -109,5 +109,141 @@ void main() {
expect(ValidationUtils.isValidUsername('user name'), isFalse);
});
});
group('getValidationErrorMessage', () {
test('returns error for null value', () {
expect(ValidationUtils.getValidationErrorMessage(null, ValidationType.email), 'Поле не может быть пустым');
expect(ValidationUtils.getValidationErrorMessage(null, ValidationType.phone), 'Поле не может быть пустым');
expect(ValidationUtils.getValidationErrorMessage(null, ValidationType.url), 'Поле не может быть пустым');
});
test('returns error for empty value', () {
expect(ValidationUtils.getValidationErrorMessage('', ValidationType.email), 'Поле не может быть пустым');
expect(ValidationUtils.getValidationErrorMessage('', ValidationType.password), 'Поле не может быть пустым');
expect(ValidationUtils.getValidationErrorMessage('', ValidationType.username), 'Поле не может быть пустым');
});
test('returns null for valid email', () {
expect(ValidationUtils.getValidationErrorMessage('test@example.com', ValidationType.email), isNull);
});
test('returns error for invalid email', () {
expect(ValidationUtils.getValidationErrorMessage('invalid', ValidationType.email), 'Введите корректный email');
});
test('returns null for valid phone', () {
expect(ValidationUtils.getValidationErrorMessage('+79991234567', ValidationType.phone), isNull);
});
test('returns error for invalid phone', () {
expect(ValidationUtils.getValidationErrorMessage('abc', ValidationType.phone), 'Введите корректный номер телефона');
});
test('returns null for valid URL', () {
expect(ValidationUtils.getValidationErrorMessage('https://example.com', ValidationType.url), isNull);
});
test('returns error for invalid URL', () {
expect(ValidationUtils.getValidationErrorMessage('not a url', ValidationType.url), 'Введите корректный URL');
});
test('returns null for valid password', () {
expect(ValidationUtils.getValidationErrorMessage('password123', ValidationType.password), isNull);
});
test('returns error for invalid password', () {
expect(ValidationUtils.getValidationErrorMessage('short', ValidationType.password), 'Пароль должен содержать минимум 8 символов, включая буквы и цифры');
});
test('returns null for valid username', () {
expect(ValidationUtils.getValidationErrorMessage('user123', ValidationType.username), isNull);
});
test('returns error for invalid username', () {
expect(ValidationUtils.getValidationErrorMessage('ab', ValidationType.username), 'Имя пользователя должно содержать от 3 до 20 символов (буквы, цифры, подчеркивания)');
});
test('returns null for valid title', () {
expect(ValidationUtils.getValidationErrorMessage('Valid Title', ValidationType.title), isNull);
});
test('returns error for invalid title', () {
expect(ValidationUtils.getValidationErrorMessage('', ValidationType.title), 'Поле не может быть пустым');
final longTitle = 'a' * (AppConstants.maxTitleLength + 1);
expect(ValidationUtils.getValidationErrorMessage(longTitle, ValidationType.title), 'Заголовок не может быть пустым и должен содержать не более ${AppConstants.maxTitleLength} символов');
});
test('returns null for valid description', () {
expect(ValidationUtils.getValidationErrorMessage('Valid description', ValidationType.description), isNull);
expect(ValidationUtils.getValidationErrorMessage('', ValidationType.description), 'Поле не может быть пустым');
});
test('returns error for invalid description', () {
final longDescription = 'a' * (AppConstants.maxDescriptionLength + 1);
expect(ValidationUtils.getValidationErrorMessage(longDescription, ValidationType.description), 'Описание не может содержать более ${AppConstants.maxDescriptionLength} символов');
});
test('returns null for valid text', () {
expect(ValidationUtils.getValidationErrorMessage('Valid text', ValidationType.text), isNull);
});
test('returns error for invalid text', () {
final longText = 'a' * (AppConstants.maxTextLength + 1);
expect(ValidationUtils.getValidationErrorMessage(longText, ValidationType.text), 'Текст не может содержать более ${AppConstants.maxTextLength} символов');
});
});
group('ValidationType enum', () {
test('has all expected values', () {
expect(ValidationType.values.length, 8);
expect(ValidationType.values, contains(ValidationType.email));
expect(ValidationType.values, contains(ValidationType.phone));
expect(ValidationType.values, contains(ValidationType.url));
expect(ValidationType.values, contains(ValidationType.password));
expect(ValidationType.values, contains(ValidationType.username));
expect(ValidationType.values, contains(ValidationType.title));
expect(ValidationType.values, contains(ValidationType.description));
expect(ValidationType.values, contains(ValidationType.text));
});
test('can access by name', () {
expect(ValidationType.values.byName('email'), ValidationType.email);
expect(ValidationType.values.byName('phone'), ValidationType.phone);
expect(ValidationType.values.byName('url'), ValidationType.url);
});
});
group('Edge cases', () {
test('isValidEmail handles special characters', () {
expect(ValidationUtils.isValidEmail('test+tag@example.com'), isTrue);
expect(ValidationUtils.isValidEmail('test.email@example.co.uk'), isTrue);
});
test('isValidPhone handles various formats', () {
expect(ValidationUtils.isValidPhone('+1234567890'), isTrue);
expect(ValidationUtils.isValidPhone('1234567890'), isTrue);
expect(ValidationUtils.isValidPhone('+123'), isTrue);
});
test('isValidUrl handles various protocols', () {
expect(ValidationUtils.isValidUrl('https://example.com'), isTrue);
expect(ValidationUtils.isValidUrl('http://example.com'), isTrue);
expect(ValidationUtils.isValidUrl('https://www.example.com/path?query=value'), isTrue);
});
test('isValidPassword handles edge cases', () {
expect(ValidationUtils.isValidPassword('Password1'), isTrue);
expect(ValidationUtils.isValidPassword('PASSWORD123'), isTrue);
expect(ValidationUtils.isValidPassword('password1'), isTrue);
expect(ValidationUtils.isValidPassword('1234567a'), isTrue);
});
test('isValidUsername handles edge cases', () {
expect(ValidationUtils.isValidUsername('a' * 3), isTrue);
expect(ValidationUtils.isValidUsername('a' * 20), isTrue);
expect(ValidationUtils.isValidUsername('user_123'), isTrue);
expect(ValidationUtils.isValidUsername('123user'), isTrue);
});
});
});
}

View File

@@ -1,11 +1,15 @@
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/token_storage.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/features/chat/models/websocket_info_model.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'ws_manager_test.mocks.dart';
@@ -17,6 +21,8 @@ void main() {
});
setUp(() async {
SharedPreferences.setMockInitialValues({});
TokenStorage.clearCache();
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
@@ -25,7 +31,6 @@ void main() {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
await manager.initialize();
expect(manager, isNotNull);
@@ -86,5 +91,165 @@ void main() {
expect(manager.client, isNull);
expect(manager.userId, isNull);
});
test('initPrivateDirectChatChannel - создает канал для прямого чата', () async {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
const chatId = 'chat-123';
const token = 'test-token';
await TokenStorage.saveTokenInStorage(token);
// initPrivateDirectChatChannel требует, чтобы client был инициализирован
// Но так как мы не можем легко мокировать PusherChannelsClient,
// этот тест проверяет, что метод не выбрасывает исключение
// когда client равен null
expect(() => manager.initPrivateDirectChatChannel(chatId), returnsNormally);
});
test('initialize - обрабатывает ошибку при отсутствии токена', () async {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
// Убеждаемся, что токена нет
await TokenStorage.removeTokenInStorage();
// initialize должен обработать ошибку gracefully
await manager.initialize();
expect(manager.client, isNull);
});
test('initialize - обрабатывает ошибку при ошибке getWebsocket', () async {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
const token = 'test-token';
await TokenStorage.saveTokenInStorage(token);
when(wsService.getWebsocket()).thenThrow(Exception('Network error'));
// initialize должен обработать ошибку gracefully
await manager.initialize();
expect(manager.client, isNull);
});
test('initialize - устанавливает userId из WebsocketInfoModel', () async {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
const token = 'test-token';
const userId = 42;
await TokenStorage.saveTokenInStorage(token);
when(
wsService.getWebsocket(),
).thenAnswer((_) async => const WebsocketInfoModel(key: 'test-key', host: 'test-host', port: '443', cluster: 'test-cluster', userId: userId));
// initialize должен установить userId, но так как мы не можем
// полностью мокировать PusherChannelsClient, этот тест проверяет,
// что метод не выбрасывает исключение
await manager.initialize();
// userId может быть установлен только если client успешно создан
// В данном случае, так как мы не можем полностью мокировать client,
// мы просто проверяем, что метод не выбрасывает исключение
expect(manager, isNotNull);
});
test('clear - отписывается от всех каналов', () async {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
manager.userId = 1;
// clear должен отписаться от всех каналов
await manager.clear();
expect(manager.client, isNull);
expect(manager.userId, isNull);
});
test('initialize - не инициализирует повторно если client уже существует', () async {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
const token = 'test-token';
await TokenStorage.saveTokenInStorage(token);
when(
wsService.getWebsocket(),
).thenAnswer((_) async => const WebsocketInfoModel(key: 'test-key', host: 'test-host', port: '443', cluster: 'test-cluster', userId: 1));
// Первый вызов initialize
await manager.initialize();
// Второй вызов initialize не должен вызывать getWebsocket снова
// если client уже инициализирован
final initialClient = manager.client;
await manager.initialize();
// client должен остаться тем же (или null, если инициализация не удалась)
expect(manager.client, initialClient);
});
test('subscribeDirectChatChannel - подписывается на канал когда он существует', () {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
// Когда канал null, метод не должен выбрасывать исключение
manager.subscribeDirectChatChannel();
expect(manager, isNotNull);
});
test('unsubscribeDirectChatChannel - отписывается от канала когда он существует', () {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
// Когда канал null, метод не должен выбрасывать исключение
manager.unsubscribeDirectChatChannel();
expect(manager, isNotNull);
});
test('newMessagesStream - возвращает null когда канал не инициализирован', () {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
expect(manager.newMessagesStream, isNull);
});
test('newNotificationsStream - возвращает null когда канал не инициализирован', () {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
expect(manager.newNotificationsStream, isNull);
});
test('directChatStream - возвращает null когда канал не инициализирован', () {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
expect(manager.directChatStream, isNull);
});
test('clientTypingStream - возвращает null когда канал не инициализирован', () {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
expect(manager.clientTypingStream, isNull);
});
test('initialize - обрабатывает случай когда TokenStorage.haveTokenInStorage возвращает false', () async {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
// Убеждаемся, что токена нет
await TokenStorage.removeTokenInStorage();
// initialize должен обработать отсутствие токена gracefully
await manager.initialize();
expect(manager.client, isNull);
});
});
}

View File

@@ -26,6 +26,26 @@ void main() {
await tester.pump();
expect(find.byType(AddItemButton), findsOneWidget);
expect(find.byIcon(Icons.add_rounded), findsOneWidget);
});
testWidgets('calls onTap when provided', (tester) async {
bool tapped = false;
await tester.pumpWidget(
wrap(
AddItemButton(
onTap: () {
tapped = true;
},
),
),
);
await tester.pump();
await tester.tap(find.byType(AddItemButton));
await tester.pump();
expect(tapped, true);
});
});
}

View File

@@ -27,5 +27,43 @@ void main() {
expect(find.byType(AnimatedOptionButton), findsOneWidget);
expect(find.text('Option 2'), findsOneWidget);
});
testWidgets('renders animated option button with custom text style', (tester) async {
await tester.pumpWidget(
wrap(
AnimatedOptionButton(
text: 'Option 3',
isSelected: false,
onTap: () {},
textStyle: const TextStyle(fontSize: 18),
),
),
);
await tester.pump();
expect(find.byType(AnimatedOptionButton), findsOneWidget);
expect(find.text('Option 3'), findsOneWidget);
});
testWidgets('calls onTap when button is tapped', (tester) async {
bool tapped = false;
await tester.pumpWidget(
wrap(
AnimatedOptionButton(
text: 'Option 4',
isSelected: false,
onTap: () {
tapped = true;
},
),
),
);
await tester.pump();
await tester.tap(find.byType(AnimatedOptionButton));
await tester.pump();
expect(tapped, true);
});
});
}

View File

@@ -24,6 +24,69 @@ void main() {
await tester.pump();
expect(find.byType(BookmarkButton), findsOneWidget);
expect(find.byIcon(Icons.bookmark), findsOneWidget);
});
testWidgets('renders bookmark button with custom shape', (tester) async {
await tester.pumpWidget(
wrap(
BookmarkButton(
onTap: () {},
color: Colors.white,
isFavorite: false,
width: 40,
height: 40,
size: 20,
shape: BoxShape.circle,
),
),
);
await tester.pump();
expect(find.byType(BookmarkButton), findsOneWidget);
});
testWidgets('renders bookmark button with borderRadius', (tester) async {
await tester.pumpWidget(
wrap(
BookmarkButton(
onTap: () {},
color: Colors.white,
isFavorite: false,
width: 40,
height: 40,
size: 20,
borderRadius: BorderRadius.circular(8),
),
),
);
await tester.pump();
expect(find.byType(BookmarkButton), findsOneWidget);
});
testWidgets('calls onTap when button is tapped', (tester) async {
bool tapped = false;
await tester.pumpWidget(
wrap(
BookmarkButton(
onTap: () {
tapped = true;
},
color: Colors.white,
isFavorite: false,
width: 40,
height: 40,
size: 20,
),
),
);
await tester.pump();
await tester.tap(find.byType(BookmarkButton));
await tester.pump();
expect(tapped, true);
});
});
}

View File

@@ -45,5 +45,44 @@ void main() {
expect(find.byType(BottomSheetActionWidget), findsOneWidget);
expect(find.text('Test Action'), findsOneWidget);
});
testWidgets('renders bottom sheet action widget with custom color', (tester) async {
await tester.pumpWidget(
wrap(
BottomSheetActionWidget(
text: 'Test Action',
iconPath: 'assets/icons/ok.svg',
onTap: () {},
color: Colors.red,
),
),
);
await tester.pump();
expect(find.byType(BottomSheetActionWidget), findsOneWidget);
expect(find.text('Test Action'), findsOneWidget);
});
testWidgets('calls onTap when widget is tapped', (tester) async {
bool tapped = false;
await tester.pumpWidget(
wrap(
BottomSheetActionWidget(
text: 'Test Action',
iconPath: 'assets/icons/ok.svg',
onTap: () {
tapped = true;
},
needToPop: false,
),
),
);
await tester.pump();
await tester.tap(find.byType(BottomSheetActionWidget));
await tester.pump();
expect(tapped, true);
});
});
}

View File

@@ -17,6 +17,16 @@ void main() {
await tester.pump();
expect(find.byType(BottomSheetHandlerBar), findsOneWidget);
expect(find.byType(Container), findsWidgets);
});
testWidgets('renders handler bar with correct dimensions', (tester) async {
await tester.pumpWidget(wrap(const BottomSheetHandlerBar()));
await tester.pump();
final container = tester.widget<Container>(find.byType(Container).first);
final boxDecoration = container.decoration as BoxDecoration?;
expect(boxDecoration?.borderRadius, isNotNull);
});
});
}

View File

@@ -123,6 +123,64 @@ void main() {
expect(find.text('Test Content'), findsNothing);
});
testWidgets('shows bottom sheet with enableDrag false', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Builder(
builder: (context) {
return Scaffold(
body: ElevatedButton(
onPressed: () {
showBottomSheetWithBlur(
context: context,
child: const Text('Test Content'),
enableDrag: false,
);
},
child: const Text('Show Bottom Sheet'),
),
);
},
),
),
);
await tester.tap(find.text('Show Bottom Sheet'));
await tester.pumpAndSettle();
expect(find.text('Test Content'), findsOneWidget);
});
testWidgets('shows bottom sheet with userRootNavigator true', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Builder(
builder: (context) {
return Scaffold(
body: ElevatedButton(
onPressed: () {
showBottomSheetWithBlur(
context: context,
child: const Text('Test Content'),
userRootNavigator: true,
);
},
child: const Text('Show Bottom Sheet'),
),
);
},
),
),
);
await tester.tap(find.text('Show Bottom Sheet'));
await tester.pumpAndSettle();
expect(find.text('Test Content'), findsOneWidget);
});
});
}

View File

@@ -27,5 +27,33 @@ void main() {
expect(find.byType(CpfcContainer), findsOneWidget);
});
testWidgets('displays calories value', (tester) async {
await tester.pumpWidget(wrap(const CpfcContainer(calories: 2000.0, proteins: 150.0, fats: 80.0, carbohydrates: 250.0)));
await tester.pump();
expect(find.text('2000.0'), findsOneWidget);
});
testWidgets('displays proteins value', (tester) async {
await tester.pumpWidget(wrap(const CpfcContainer(calories: 2000.0, proteins: 150.0, fats: 80.0, carbohydrates: 250.0)));
await tester.pump();
expect(find.text('150.0'), findsOneWidget);
});
testWidgets('displays fats value', (tester) async {
await tester.pumpWidget(wrap(const CpfcContainer(calories: 2000.0, proteins: 150.0, fats: 80.0, carbohydrates: 250.0)));
await tester.pump();
expect(find.text('80.0'), findsOneWidget);
});
testWidgets('displays carbohydrates value', (tester) async {
await tester.pumpWidget(wrap(const CpfcContainer(calories: 2000.0, proteins: 150.0, fats: 80.0, carbohydrates: 250.0)));
await tester.pump();
expect(find.text('250.0'), findsOneWidget);
});
});
}

View File

@@ -17,6 +17,15 @@ void main() {
await tester.pump();
expect(find.byType(CustomDivider), findsOneWidget);
expect(find.byType(Divider), findsOneWidget);
});
testWidgets('renders divider with correct properties', (tester) async {
await tester.pumpWidget(wrap(const CustomDivider()));
await tester.pump();
final divider = tester.widget<Divider>(find.byType(Divider));
expect(divider.height, 1);
});
});
}

View File

@@ -29,5 +29,34 @@ void main() {
expect(find.byType(CustomDropDownButton<String>), findsOneWidget);
});
testWidgets('renders custom drop down button with hasError', (tester) async {
const items = [DropdownMenuItem(value: 'option1', child: Text('Option 1')), DropdownMenuItem(value: 'option2', child: Text('Option 2'))];
await tester.pumpWidget(wrap(const CustomDropDownButton<String>(items: items, hint: Text('Select option'), hasError: true, onChanged: null)));
await tester.pump();
expect(find.byType(CustomDropDownButton<String>), findsOneWidget);
});
testWidgets('renders custom drop down button with isExpanded', (tester) async {
const items = [DropdownMenuItem(value: 'option1', child: Text('Option 1')), DropdownMenuItem(value: 'option2', child: Text('Option 2'))];
await tester.pumpWidget(wrap(const CustomDropDownButton<String>(items: items, hint: Text('Select option'), isExpanded: true, onChanged: null)));
await tester.pump();
expect(find.byType(CustomDropDownButton<String>), findsOneWidget);
});
testWidgets('calls onChanged when value is selected', (tester) async {
const items = [DropdownMenuItem(value: 'option1', child: Text('Option 1')), DropdownMenuItem(value: 'option2', child: Text('Option 2'))];
await tester.pumpWidget(wrap(CustomDropDownButton<String>(items: items, hint: const Text('Select option'), onChanged: (value) {})));
await tester.pump();
// Note: Testing dropdown selection in Flutter tests is complex
// This test verifies the widget renders correctly
expect(find.byType(CustomDropDownButton<String>), findsOneWidget);
});
});
}

View File

@@ -32,5 +32,24 @@ void main() {
expect(find.byType(CustomDropDown), findsOneWidget);
});
testWidgets('renders custom drop down with alignment', (tester) async {
const items = [SelectionPopupModel(id: 1, title: 'Option 1'), SelectionPopupModel(id: 2, title: 'Option 2')];
await tester.pumpWidget(wrap(const CustomDropDown(items: items, hintText: 'Select option', alignment: Alignment.centerLeft)));
await tester.pump();
expect(find.byType(CustomDropDown), findsOneWidget);
});
testWidgets('renders custom drop down with prefix', (tester) async {
const items = [SelectionPopupModel(id: 1, title: 'Option 1'), SelectionPopupModel(id: 2, title: 'Option 2')];
await tester.pumpWidget(wrap(const CustomDropDown(items: items, hintText: 'Select option', prefix: Icon(Icons.search))));
await tester.pump();
expect(find.byType(CustomDropDown), findsOneWidget);
expect(find.byIcon(Icons.search), findsOneWidget);
});
});
}

View File

@@ -34,5 +34,40 @@ void main() {
expect(find.byType(CustomOutlinedButton), findsOneWidget);
});
testWidgets('renders custom outlined button with right icon', (tester) async {
await tester.pumpWidget(wrap(const CustomOutlinedButton(text: 'Test Button', rightIcon: Icon(Icons.arrow_forward))));
await tester.pump();
expect(find.byType(CustomOutlinedButton), findsOneWidget);
expect(find.byIcon(Icons.arrow_forward), findsOneWidget);
});
testWidgets('renders custom outlined button with custom alignment', (tester) async {
await tester.pumpWidget(wrap(const CustomOutlinedButton(text: 'Test Button', alignment: Alignment.centerLeft)));
await tester.pump();
expect(find.byType(CustomOutlinedButton), findsOneWidget);
});
testWidgets('calls onPressed when button is tapped', (tester) async {
bool pressed = false;
await tester.pumpWidget(
wrap(
CustomOutlinedButton(
text: 'Test Button',
onPressed: () {
pressed = true;
},
),
),
);
await tester.pump();
await tester.tap(find.text('Test Button'));
await tester.pump();
expect(pressed, true);
});
});
}

View File

@@ -86,5 +86,32 @@ void main() {
expect(find.byType(CustomPopup), findsOneWidget);
expect(find.byIcon(Icons.more_horiz_rounded), findsOneWidget);
});
testWidgets('renders custom popup for author with recipe', (tester) async {
await tester.pumpWidget(
wrap(const CustomPopup(isAuthor: true, path: 'test/path', userSlug: 'test-user', complainedContentType: ComplainedContent.recipe, slug: 'test-recipe')),
);
await tester.pump();
expect(find.byType(CustomPopup), findsOneWidget);
expect(find.byIcon(Icons.more_horiz_rounded), findsOneWidget);
});
testWidgets('renders custom popup for author with diet', (tester) async {
await tester.pumpWidget(
wrap(const CustomPopup(isAuthor: true, path: 'test/path', userSlug: 'test-user', complainedContentType: ComplainedContent.diet, id: 1)),
);
await tester.pump();
expect(find.byType(CustomPopup), findsOneWidget);
expect(find.byIcon(Icons.more_horiz_rounded), findsOneWidget);
});
testWidgets('renders custom popup for author with plan', (tester) async {
await tester.pumpWidget(
wrap(const CustomPopup(isAuthor: true, path: 'test/path', userSlug: 'test-user', complainedContentType: ComplainedContent.plan, slug: 'test-plan')),
);
await tester.pump();
expect(find.byType(CustomPopup), findsOneWidget);
expect(find.byIcon(Icons.more_horiz_rounded), findsOneWidget);
});
});
}

View File

@@ -43,5 +43,55 @@ void main() {
expect(find.byType(CustomTextFormField), findsOneWidget);
});
testWidgets('renders custom text form field with prefix', (tester) async {
await tester.pumpWidget(wrap(const CustomTextFormField(hintText: 'Enter text', prefix: Icon(Icons.search))));
await tester.pump();
expect(find.byType(CustomTextFormField), findsOneWidget);
expect(find.byIcon(Icons.search), findsOneWidget);
});
testWidgets('renders custom text form field with suffix', (tester) async {
await tester.pumpWidget(wrap(const CustomTextFormField(hintText: 'Enter text', suffix: Icon(Icons.clear))));
await tester.pump();
expect(find.byType(CustomTextFormField), findsOneWidget);
expect(find.byIcon(Icons.clear), findsOneWidget);
});
testWidgets('renders custom text form field with hasError', (tester) async {
await tester.pumpWidget(wrap(const CustomTextFormField(hintText: 'Enter text', hasError: true)));
await tester.pump();
expect(find.byType(CustomTextFormField), findsOneWidget);
});
testWidgets('renders custom text form field with readOnly', (tester) async {
await tester.pumpWidget(wrap(const CustomTextFormField(hintText: 'Enter text', readOnly: true)));
await tester.pump();
expect(find.byType(CustomTextFormField), findsOneWidget);
});
testWidgets('calls onChanged when text is entered', (tester) async {
String? changedValue;
await tester.pumpWidget(
wrap(
CustomTextFormField(
hintText: 'Enter text',
onChanged: (value) {
changedValue = value;
},
),
),
);
await tester.pump();
await tester.enterText(find.byType(CustomTextFormField), 'test');
await tester.pump();
expect(changedValue, 'test');
});
});
}

View File

@@ -27,5 +27,33 @@ void main() {
expect(find.byType(DataErrorWidget), findsOneWidget);
});
testWidgets('displays error message and reload button', (tester) async {
await tester.pumpWidget(wrap(DataErrorWidget(onReload: () async {})));
await tester.pump();
expect(find.byType(DataErrorWidget), findsOneWidget);
expect(find.textContaining('Что-то пошло не так'), findsWidgets);
});
testWidgets('calls onReload when button is tapped', (tester) async {
bool reloadCalled = false;
await tester.pumpWidget(
wrap(
DataErrorWidget(
onReload: () async {
reloadCalled = true;
},
),
),
);
await tester.pump();
final button = find.textContaining('Попробовать');
await tester.tap(button);
await tester.pump();
expect(reloadCalled, true);
});
});
}

View File

@@ -31,5 +31,42 @@ void main() {
expect(find.byIcon(Icons.star), findsOneWidget);
expect(find.text('Star'), findsOneWidget);
});
testWidgets('renders feed icon button with custom colors', (tester) async {
await tester.pumpWidget(
wrap(
FeedIconButton(
icon: Icons.favorite,
text: 'Like',
onTap: () {},
iconColor: Colors.red,
backgroundColor: Colors.pink,
textColor: Colors.red,
),
),
);
await tester.pump();
expect(find.byType(FeedIconButton), findsOneWidget);
});
testWidgets('calls onTap when button is tapped', (tester) async {
bool tapped = false;
await tester.pumpWidget(
wrap(
FeedIconButton(
icon: Icons.favorite,
onTap: () {
tapped = true;
},
),
),
);
await tester.pump();
await tester.tap(find.byType(FeedIconButton));
await tester.pump();
expect(tapped, true);
});
});
}

View File

@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/enums/product_type.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/info_row.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
@@ -97,5 +98,39 @@ void main() {
expect(find.byType(InfoRow), findsOneWidget);
});
testWidgets('renders info row for plan', (tester) async {
const plan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: '/test.jpg',
duration: 14,
state: 'active',
type: PlanType(slim: true, fit: false, wellness: false, behavior: false),
details: 'Test plan description',
packetsCount: 1,
author: Author(
authorSlug: 'test-author',
authorName: 'Test Author',
authorProfileIcon: '/test.jpg',
authorIsMe: false,
isEntitySubscribedByMe: false,
authorPlaceInRating: null,
),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
wasLikedByMe: false,
isFavorite: false,
minPriceOfOptions: 1000,
maxPriceOfOptions: 2000,
);
await tester.pumpWidget(wrap(const InfoRow(type: ProductType.plan, plan: plan)));
await tester.pump();
expect(find.byType(InfoRow), findsOneWidget);
});
});
}

View File

@@ -40,5 +40,21 @@ void main() {
expect(find.byType(ItemWidget), findsOneWidget);
expect(find.text('Test Item'), findsOneWidget);
});
testWidgets('renders item widget with custom icon size', (tester) async {
await tester.pumpWidget(wrap(const ItemWidget(text: 'Test Item', iconSize: 30)));
await tester.pumpAndSettle();
expect(find.byType(ItemWidget), findsOneWidget);
expect(find.text('Test Item'), findsOneWidget);
});
testWidgets('renders item widget reversed with custom icon size', (tester) async {
await tester.pumpWidget(wrap(const ItemWidget(text: 'Test Item', reversed: true, iconSize: 25)));
await tester.pumpAndSettle();
expect(find.byType(ItemWidget), findsOneWidget);
expect(find.text('Test Item'), findsOneWidget);
});
});
}

View File

@@ -17,6 +17,17 @@ void main() {
await tester.pump();
expect(find.byType(Loader), findsOneWidget);
expect(find.byType(Center), findsOneWidget);
expect(find.byType(SizedBox), findsWidgets);
});
testWidgets('renders loader with correct dimensions', (tester) async {
await tester.pumpWidget(wrap(const Loader()));
await tester.pump();
final sizedBox = tester.widget<SizedBox>(find.byType(SizedBox).first);
expect(sizedBox.height, 42);
expect(sizedBox.width, 42);
});
});
}

View File

@@ -33,5 +33,19 @@ void main() {
expect(find.byType(MainBackgroundBouble), findsOneWidget);
});
testWidgets('renders main background bouble with custom width', (tester) async {
await tester.pumpWidget(wrap(const MainBackgroundBouble(width: 200.0, child: Text('Test'))));
await tester.pump();
expect(find.byType(MainBackgroundBouble), findsOneWidget);
});
testWidgets('renders main background bouble with custom padding', (tester) async {
await tester.pumpWidget(wrap(const MainBackgroundBouble(padding: EdgeInsets.all(16), child: Text('Test'))));
await tester.pump();
expect(find.byType(MainBackgroundBouble), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,43 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('MarkdownHtmlWidget smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
testWidgets('renders markdown html widget', (tester) async {
await tester.pumpWidget(wrap(const MarkdownHtmlWidget(markdownText: '# Test')));
await tester.pump();
expect(find.byType(MarkdownHtmlWidget), findsOneWidget);
});
testWidgets('renders markdown html widget with custom image width', (tester) async {
await tester.pumpWidget(wrap(const MarkdownHtmlWidget(markdownText: '# Test', imageWidth: 200.0)));
await tester.pump();
expect(find.byType(MarkdownHtmlWidget), findsOneWidget);
});
testWidgets('renders markdown html widget with custom text color', (tester) async {
await tester.pumpWidget(wrap(const MarkdownHtmlWidget(markdownText: '# Test', textColor: Colors.red)));
await tester.pump();
expect(find.byType(MarkdownHtmlWidget), findsOneWidget);
});
testWidgets('renders markdown html widget with custom font size', (tester) async {
await tester.pumpWidget(wrap(const MarkdownHtmlWidget(markdownText: '# Test', fontSize: 18.0)));
await tester.pump();
expect(find.byType(MarkdownHtmlWidget), findsOneWidget);
});
});
}

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/marketing_bottom_sheet.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
@@ -66,6 +67,42 @@ void main() {
expect(find.byType(MarketingBottomSheet), findsOneWidget);
});
testWidgets('displays marketing bottom sheet with congratulations text', (tester) async {
await tester.pumpWidget(
createTestWidget(
const MarketingBottomSheet(),
),
);
await tester.pumpAndSettle();
expect(find.byType(MarketingBottomSheet), findsOneWidget);
expect(find.textContaining('Поздравляем'), findsWidgets);
});
testWidgets('displays marketing bottom sheet with invite button', (tester) async {
await tester.pumpWidget(
createTestWidget(
const MarketingBottomSheet(),
),
);
await tester.pumpAndSettle();
expect(find.byType(MarketingBottomSheet), findsOneWidget);
expect(find.byType(CustomOutlinedButton), findsOneWidget);
});
testWidgets('displays marketing bottom sheet with logo', (tester) async {
await tester.pumpWidget(
createTestWidget(
const MarketingBottomSheet(),
),
);
await tester.pumpAndSettle();
expect(find.byType(MarketingBottomSheet), findsOneWidget);
expect(find.byType(ClipRRect), findsWidgets);
});
});
}

View File

@@ -1,93 +1,133 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/option_widget.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('OptionWidget', () {
Widget createTestWidget(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Scaffold(body: child),
);
}
testWidgets('returns SizedBox.shrink when option is null', (tester) async {
await tester.pumpWidget(createTestWidget(const OptionWidget(option: null, duration: 7)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget);
expect(find.byType(SizedBox), findsWidgets);
});
testWidgets('displays option widget with option', (tester) async {
const option = PacketOptions(items: ['Item 1', 'Item 2'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget);
expect(find.text('Short'), findsOneWidget);
expect(find.text('Description'), findsOneWidget);
});
testWidgets('displays option widget with personal diet', (tester) async {
const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: true);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget);
});
testWidgets('hides duration when disableDuration is true', (tester) async {
const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7, disableDuration: true)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget);
});
});
}
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/option_widget.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('OptionWidget', () {
Widget createTestWidget(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Scaffold(body: child),
);
}
testWidgets('returns SizedBox.shrink when option is null', (tester) async {
await tester.pumpWidget(createTestWidget(const OptionWidget(option: null, duration: 7)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget);
expect(find.byType(SizedBox), findsWidgets);
});
testWidgets('displays option widget with option', (tester) async {
const option = PacketOptions(items: ['Item 1', 'Item 2'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget);
expect(find.text('Short'), findsOneWidget);
expect(find.text('Description'), findsOneWidget);
});
testWidgets('displays option widget with personal diet', (tester) async {
const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: true);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget);
});
testWidgets('hides duration when disableDuration is true', (tester) async {
const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7, disableDuration: true)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget);
});
testWidgets('displays option items', (tester) async {
const option = PacketOptions(items: ['Item 1', 'Item 2', 'Item 3'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 2'), findsOneWidget);
expect(find.text('Item 3'), findsOneWidget);
});
testWidgets('displays option widget with empty items list', (tester) async {
const option = PacketOptions(items: [], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget);
expect(find.text('Short'), findsOneWidget);
});
testWidgets('displays option widget with different duration', (tester) async {
const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 14)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget);
});
testWidgets('displays option widget with personal diet and duration disabled', (tester) async {
const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: true);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7, disableDuration: true)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget);
});
});
}

View File

@@ -152,5 +152,40 @@ void main() {
// Dialog should be closed
expect(find.text('Упс! Произошла ошибка'), findsNothing);
});
testWidgets('displays error dialog with needToOpenSettings', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Builder(
builder: (context) {
return Scaffold(
body: ElevatedButton(
onPressed: () {
showErrorDialog(
context,
needToOpenSettings: true,
);
},
child: const Text('Show Error'),
),
);
},
),
),
);
await tester.pump();
await tester.tap(find.text('Show Error'));
await tester.pumpAndSettle();
expect(find.text('Упс! Произошла ошибка'), findsOneWidget);
});
});
}

View File

@@ -3,6 +3,8 @@ import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/success_published_diet_bottom_sheet.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
@@ -59,5 +61,51 @@ void main() {
expect(find.byType(SuccessPublishedDietOrPlanBottomSheet), findsOneWidget);
});
testWidgets('displays title and published text', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedDietOrPlanBottomSheet(title: 'Test Diet', isPlan: false)));
await tester.pump();
expect(find.text('Test Diet'), findsOneWidget);
expect(find.textContaining('опубликованна'), findsWidgets);
});
testWidgets('displays plan published text when isPlan is true', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedDietOrPlanBottomSheet(title: 'Test Plan', isPlan: true)));
await tester.pump();
expect(find.text('Test Plan'), findsOneWidget);
expect(find.textContaining('опубликован'), findsWidgets);
});
testWidgets('displays bottom sheet handler bar', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedDietOrPlanBottomSheet(title: 'Test Diet', isPlan: false)));
await tester.pump();
expect(find.byType(BottomSheetHandlerBar), findsOneWidget);
});
testWidgets('displays ok button', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedDietOrPlanBottomSheet(title: 'Test Diet', isPlan: false)));
await tester.pump();
expect(find.byType(CustomOutlinedButton), findsOneWidget);
});
testWidgets('displays different titles for diet', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedDietOrPlanBottomSheet(title: 'Another Diet', isPlan: false)));
await tester.pump();
expect(find.text('Another Diet'), findsOneWidget);
expect(find.textContaining('опубликованна'), findsWidgets);
});
testWidgets('displays different titles for plan', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedDietOrPlanBottomSheet(title: 'Another Plan', isPlan: true)));
await tester.pump();
expect(find.text('Another Plan'), findsOneWidget);
expect(find.textContaining('опубликован'), findsWidgets);
});
});
}

View File

@@ -3,6 +3,8 @@ import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/success_published_recipe_bottom_sheet.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
@@ -52,5 +54,41 @@ void main() {
expect(find.byType(SuccessPublishedRecipeBottomSheet), findsOneWidget);
});
testWidgets('displays recipe title', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedRecipeBottomSheet(title: 'My Recipe')));
await tester.pump();
expect(find.textContaining('My Recipe'), findsWidgets);
});
testWidgets('displays recipe publishing text', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedRecipeBottomSheet(title: 'Test Recipe')));
await tester.pump();
expect(find.byType(SuccessPublishedRecipeBottomSheet), findsOneWidget);
expect(find.textContaining('Публикация рецепта'), findsWidgets);
});
testWidgets('displays bottom sheet handler bar', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedRecipeBottomSheet(title: 'Test Recipe')));
await tester.pump();
expect(find.byType(BottomSheetHandlerBar), findsOneWidget);
});
testWidgets('displays ok button', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedRecipeBottomSheet(title: 'Test Recipe')));
await tester.pump();
expect(find.byType(CustomOutlinedButton), findsOneWidget);
});
testWidgets('displays different recipe titles', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedRecipeBottomSheet(title: 'Another Recipe')));
await tester.pump();
expect(find.textContaining('Another Recipe'), findsWidgets);
});
});
}

View File

@@ -0,0 +1,232 @@
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('OAuthVerificationConfirmRequest', () {
test('creates instance with required fields', () {
const request = OAuthVerificationConfirmRequest(
accessToken: 'access_token_123',
refreshToken: 'refresh_token_123',
clientId: 'client_id_123',
provider: 'vk',
id: 'user_id_123',
code: 'code_123',
verificationType: 'email',
firstName: 'John',
lastName: 'Doe',
dateOfBirth: '1990-01-01',
gender: 'male',
weight: 70,
height: 180,
);
expect(request.accessToken, 'access_token_123');
expect(request.refreshToken, 'refresh_token_123');
expect(request.clientId, 'client_id_123');
expect(request.provider, 'vk');
expect(request.id, 'user_id_123');
expect(request.code, 'code_123');
expect(request.verificationType, 'email');
expect(request.firstName, 'John');
expect(request.lastName, 'Doe');
expect(request.dateOfBirth, '1990-01-01');
expect(request.gender, 'male');
expect(request.weight, 70);
expect(request.height, 180);
});
test('creates instance with optional email', () {
const request = OAuthVerificationConfirmRequest(
accessToken: 'access_token_123',
refreshToken: 'refresh_token_123',
clientId: 'client_id_123',
provider: 'vk',
id: 'user_id_123',
email: 'test@example.com',
code: 'code_123',
verificationType: 'email',
firstName: 'John',
lastName: 'Doe',
dateOfBirth: '1990-01-01',
gender: 'male',
weight: 70,
height: 180,
);
expect(request.email, 'test@example.com');
});
test('creates instance with optional phone', () {
const request = OAuthVerificationConfirmRequest(
accessToken: 'access_token_123',
refreshToken: 'refresh_token_123',
clientId: 'client_id_123',
provider: 'vk',
id: 'user_id_123',
phone: '+79991234567',
code: 'code_123',
verificationType: 'phone',
firstName: 'John',
lastName: 'Doe',
dateOfBirth: '1990-01-01',
gender: 'male',
weight: 70,
height: 180,
);
expect(request.phone, '+79991234567');
});
test('fromJson creates instance from JSON', () {
final json = {
'access_token': 'access_token_123',
'refresh_token': 'refresh_token_123',
'client_id': 'client_id_123',
'provider': 'vk',
'id': 'user_id_123',
'code': 'code_123',
'verification_type': 'email',
'first_name': 'John',
'last_name': 'Doe',
'date_of_birth': '1990-01-01',
'gender': 'male',
'weight': 70,
'height': 180,
};
final request = OAuthVerificationConfirmRequest.fromJson(json);
expect(request.accessToken, 'access_token_123');
expect(request.refreshToken, 'refresh_token_123');
expect(request.clientId, 'client_id_123');
expect(request.provider, 'vk');
expect(request.id, 'user_id_123');
expect(request.code, 'code_123');
expect(request.verificationType, 'email');
expect(request.firstName, 'John');
expect(request.lastName, 'Doe');
expect(request.dateOfBirth, '1990-01-01');
expect(request.gender, 'male');
expect(request.weight, 70);
expect(request.height, 180);
});
test('toJson converts instance to JSON', () {
const request = OAuthVerificationConfirmRequest(
accessToken: 'access_token_123',
refreshToken: 'refresh_token_123',
clientId: 'client_id_123',
provider: 'vk',
id: 'user_id_123',
email: 'test@example.com',
code: 'code_123',
verificationType: 'email',
firstName: 'John',
lastName: 'Doe',
dateOfBirth: '1990-01-01',
gender: 'male',
weight: 70,
height: 180,
);
final json = request.toJson();
expect(json['access_token'], 'access_token_123');
expect(json['refresh_token'], 'refresh_token_123');
expect(json['client_id'], 'client_id_123');
expect(json['provider'], 'vk');
expect(json['id'], 'user_id_123');
expect(json['email'], 'test@example.com');
expect(json['code'], 'code_123');
expect(json['verification_type'], 'email');
expect(json['first_name'], 'John');
expect(json['last_name'], 'Doe');
expect(json['date_of_birth'], '1990-01-01');
expect(json['gender'], 'male');
expect(json['weight'], 70);
expect(json['height'], 180);
});
test('copyWith creates new instance with updated values', () {
const original = OAuthVerificationConfirmRequest(
accessToken: 'access_token_123',
refreshToken: 'refresh_token_123',
clientId: 'client_id_123',
provider: 'vk',
id: 'user_id_123',
code: 'code_123',
verificationType: 'email',
firstName: 'John',
lastName: 'Doe',
dateOfBirth: '1990-01-01',
gender: 'male',
weight: 70,
height: 180,
);
final updated = original.copyWith(firstName: 'Jane', weight: 65);
expect(updated.firstName, 'Jane');
expect(updated.weight, 65);
expect(original.firstName, 'John');
expect(original.weight, 70);
});
test('copyWith keeps original values when not updated', () {
const original = OAuthVerificationConfirmRequest(
accessToken: 'access_token_123',
refreshToken: 'refresh_token_123',
clientId: 'client_id_123',
provider: 'vk',
id: 'user_id_123',
code: 'code_123',
verificationType: 'email',
firstName: 'John',
lastName: 'Doe',
dateOfBirth: '1990-01-01',
gender: 'male',
weight: 70,
height: 180,
);
final updated = original.copyWith();
expect(updated.accessToken, original.accessToken);
expect(updated.firstName, original.firstName);
});
test('handles different providers', () {
const vkRequest = OAuthVerificationConfirmRequest(
accessToken: 'token',
refreshToken: 'refresh',
clientId: 'client',
provider: 'vk',
id: 'id',
code: 'code',
verificationType: 'email',
firstName: 'John',
lastName: 'Doe',
dateOfBirth: '1990-01-01',
gender: 'male',
weight: 70,
height: 180,
);
const googleRequest = OAuthVerificationConfirmRequest(
accessToken: 'token',
refreshToken: 'refresh',
clientId: 'client',
provider: 'google',
id: 'id',
code: 'code',
verificationType: 'email',
firstName: 'John',
lastName: 'Doe',
dateOfBirth: '1990-01-01',
gender: 'male',
weight: 70,
height: 180,
);
expect(vkRequest.provider, 'vk');
expect(googleRequest.provider, 'google');
});
});
}

View File

@@ -0,0 +1,152 @@
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('OAuthVerificationConfirmResponse', () {
test('creates instance with required fields', () {
const response = OAuthVerificationConfirmResponse(
success: true,
message: 'Success',
token: 'token_123',
user: {'id': 1, 'name': 'John'},
isNewUser: false,
);
expect(response.success, true);
expect(response.message, 'Success');
expect(response.token, 'token_123');
expect(response.user, {'id': 1, 'name': 'John'});
expect(response.isNewUser, false);
});
test('creates instance with isNewUser true', () {
const response = OAuthVerificationConfirmResponse(
success: true,
message: 'New user created',
token: 'token_123',
user: {'id': 1, 'name': 'John'},
isNewUser: true,
);
expect(response.isNewUser, true);
});
test('fromJson creates instance from JSON', () {
final json = {
'success': true,
'message': 'Success',
'token': 'token_123',
'user': {'id': 1, 'name': 'John'},
'is_new_user': false,
};
final response = OAuthVerificationConfirmResponse.fromJson(json);
expect(response.success, true);
expect(response.message, 'Success');
expect(response.token, 'token_123');
expect(response.user, {'id': 1, 'name': 'John'});
expect(response.isNewUser, false);
});
test('toJson converts instance to JSON', () {
const response = OAuthVerificationConfirmResponse(
success: true,
message: 'Success',
token: 'token_123',
user: {'id': 1, 'name': 'John'},
isNewUser: false,
);
final json = response.toJson();
expect(json['success'], true);
expect(json['message'], 'Success');
expect(json['token'], 'token_123');
expect(json['user'], {'id': 1, 'name': 'John'});
expect(json['is_new_user'], false);
});
test('equality works correctly', () {
const response1 = OAuthVerificationConfirmResponse(
success: true,
message: 'Success',
token: 'token_123',
user: {'id': 1},
isNewUser: false,
);
const response2 = OAuthVerificationConfirmResponse(
success: true,
message: 'Success',
token: 'token_123',
user: {'id': 1},
isNewUser: false,
);
const response3 = OAuthVerificationConfirmResponse(
success: false,
message: 'Error',
token: 'token_123',
user: {'id': 1},
isNewUser: false,
);
expect(response1, equals(response2));
expect(response1, isNot(equals(response3)));
});
test('copyWith creates new instance with updated values', () {
const original = OAuthVerificationConfirmResponse(
success: true,
message: 'Success',
token: 'token_123',
user: {'id': 1, 'name': 'John'},
isNewUser: false,
);
final updated = original.copyWith(success: false, message: 'Error', isNewUser: true);
expect(updated.success, false);
expect(updated.message, 'Error');
expect(updated.isNewUser, true);
expect(original.success, true);
expect(original.isNewUser, false);
});
test('copyWith keeps original values when not updated', () {
const original = OAuthVerificationConfirmResponse(
success: true,
message: 'Success',
token: 'token_123',
user: {'id': 1, 'name': 'John'},
isNewUser: false,
);
final updated = original.copyWith();
expect(updated.success, original.success);
expect(updated.message, original.message);
expect(updated.token, original.token);
expect(updated.user, original.user);
expect(updated.isNewUser, original.isNewUser);
});
test('handles different user data', () {
const response1 = OAuthVerificationConfirmResponse(
success: true,
message: 'Success',
token: 'token_123',
user: {'id': 1, 'name': 'John'},
isNewUser: false,
);
const response2 = OAuthVerificationConfirmResponse(
success: true,
message: 'Success',
token: 'token_123',
user: {'id': 2, 'name': 'Jane', 'email': 'jane@example.com'},
isNewUser: true,
);
expect(response1.user['id'], 1);
expect(response2.user['id'], 2);
expect(response2.user['email'], 'jane@example.com');
});
});
}

View File

@@ -0,0 +1,76 @@
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('OAuthVerificationRequestResponse', () {
test('creates instance with required fields', () {
const response = OAuthVerificationRequestResponse(
email: 'test@example.com',
status: 'pending',
);
expect(response.email, 'test@example.com');
expect(response.status, 'pending');
});
test('creates instance with different status values', () {
const pending = OAuthVerificationRequestResponse(
email: 'test@example.com',
status: 'pending',
);
const completed = OAuthVerificationRequestResponse(
email: 'test@example.com',
status: 'completed',
);
const failed = OAuthVerificationRequestResponse(
email: 'test@example.com',
status: 'failed',
);
expect(pending.status, 'pending');
expect(completed.status, 'completed');
expect(failed.status, 'failed');
});
test('equality works correctly', () {
const response1 = OAuthVerificationRequestResponse(
email: 'test@example.com',
status: 'pending',
);
const response2 = OAuthVerificationRequestResponse(
email: 'test@example.com',
status: 'pending',
);
const response3 = OAuthVerificationRequestResponse(
email: 'other@example.com',
status: 'pending',
);
expect(response1, equals(response2));
expect(response1, isNot(equals(response3)));
});
test('copyWith creates new instance with updated values', () {
const original = OAuthVerificationRequestResponse(
email: 'test@example.com',
status: 'pending',
);
final updated = original.copyWith(status: 'completed');
expect(updated.email, 'test@example.com');
expect(updated.status, 'completed');
expect(original.status, 'pending');
});
test('copyWith keeps original values when not updated', () {
const original = OAuthVerificationRequestResponse(
email: 'test@example.com',
status: 'pending',
);
final updated = original.copyWith();
expect(updated.email, original.email);
expect(updated.status, original.status);
});
});
}

View File

@@ -0,0 +1,204 @@
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'auth_cubit_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<AuthRepository>(),
MockSpec<ProfileRepository>(),
MockSpec<FirebaseApi>(),
MockSpec<Prefs>(),
])
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('AuthCubit', () {
AuthCubit? cubit;
late MockAuthRepository mockAuthRepository;
late MockProfileRepository mockProfileRepository;
late MockFirebaseApi mockFirebaseApi;
late MockPrefs mockPrefs;
setUpAll(() {
// Mock Fluttertoast plugin
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('PonnamKarthik/fluttertoast'),
(MethodCall methodCall) async {
return null;
},
);
// Mock SharedPreferences for TokenStorage
SharedPreferences.setMockInitialValues({});
});
setUp(() async {
mockAuthRepository = MockAuthRepository();
mockProfileRepository = MockProfileRepository();
mockFirebaseApi = MockFirebaseApi();
mockPrefs = MockPrefs();
// Clear TokenStorage cache
TokenStorage.clearCache();
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
});
tearDownAll(() {
// Clear mock handlers
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('PonnamKarthik/fluttertoast'),
null,
);
});
tearDown(() async {
if (cubit != null && !cubit!.isClosed) {
await cubit!.close();
cubit = null;
}
});
test('начальное состояние - AuthState', () {
cubit = AuthCubit(
authRepository: mockAuthRepository,
profileRepository: mockProfileRepository,
firebaseApi: mockFirebaseApi,
prefs: mockPrefs,
);
expect(cubit!.state, isA<AuthState>());
});
test('getTokenByCredentials - успешная аутентификация', () async {
cubit = AuthCubit(
authRepository: mockAuthRepository,
profileRepository: mockProfileRepository,
firebaseApi: mockFirebaseApi,
prefs: mockPrefs,
);
const email = 'test@example.com';
const password = 'password123';
const token = 'test_token_123';
when(mockAuthRepository.getTokenByCredentials(email, password))
.thenAnswer((_) async => const TokenModel(token: token));
// Убеждаемся, что SharedPreferences готов к использованию
final prefs = await SharedPreferences.getInstance();
// Очищаем токен перед тестом
await prefs.remove('token');
final result = await cubit!.getTokenByCredentials(email, password);
// Даем время для завершения всех асинхронных операций, включая сохранение токена
await Future.delayed(const Duration(milliseconds: 200));
expect(result, true);
expect(cubit!.state, isA<AuthStateLoaded>());
expect((cubit!.state as AuthStateLoaded).token, token);
});
test('getTokenByCredentials - обработка ошибки', () async {
cubit = AuthCubit(
authRepository: mockAuthRepository,
profileRepository: mockProfileRepository,
firebaseApi: mockFirebaseApi,
prefs: mockPrefs,
);
const email = 'test@example.com';
const password = 'wrong_password';
when(mockAuthRepository.getTokenByCredentials(email, password))
.thenThrow(Exception('Invalid credentials'));
final result = await cubit!.getTokenByCredentials(email, password);
// Даем время для завершения всех асинхронных операций, включая showErrorToast
await Future.delayed(const Duration(milliseconds: 200));
expect(result, false);
expect(cubit!.state, isA<AuthStateError>());
expect((cubit!.state as AuthStateError).errorMessage, contains('Invalid credentials'));
});
test('registerAnonymousPushToken - вызывает firebaseApi когда токен еще не отправлен', () async {
cubit = AuthCubit(
authRepository: mockAuthRepository,
profileRepository: mockProfileRepository,
firebaseApi: mockFirebaseApi,
prefs: mockPrefs,
);
when(mockPrefs.getBool(any)).thenReturn(false);
when(mockFirebaseApi.registerAnonymousPushToken())
.thenAnswer((_) async => Future.value());
when(mockFirebaseApi.getToken()).thenAnswer((_) async => 'test_token');
await cubit!.registerAnonymousPushToken();
verify(mockFirebaseApi.registerAnonymousPushToken()).called(1);
verify(mockPrefs.setBool(any, true)).called(1);
});
test('registerAnonymousPushToken - не вызывает firebaseApi когда токен уже отправлен', () async {
cubit = AuthCubit(
authRepository: mockAuthRepository,
profileRepository: mockProfileRepository,
firebaseApi: mockFirebaseApi,
prefs: mockPrefs,
);
when(mockPrefs.getBool(any)).thenReturn(true);
await cubit!.registerAnonymousPushToken();
verifyNever(mockFirebaseApi.registerAnonymousPushToken());
});
test('registerPushToken - вызывает firebaseApi когда есть токен в хранилище', () async {
cubit = AuthCubit(
authRepository: mockAuthRepository,
profileRepository: mockProfileRepository,
firebaseApi: mockFirebaseApi,
prefs: mockPrefs,
);
when(mockFirebaseApi.registerPushToken())
.thenAnswer((_) async => Future.value());
await cubit!.registerPushToken();
verify(mockFirebaseApi.registerPushToken()).called(1);
});
test('registerPushToken - не вызывает firebaseApi когда нет токена в хранилище', () async {
cubit = AuthCubit(
authRepository: mockAuthRepository,
profileRepository: mockProfileRepository,
firebaseApi: mockFirebaseApi,
prefs: mockPrefs,
);
// TokenStorage.haveTokenInStorage() вернет false
// registerPushToken проверяет это внутри и не вызывает firebaseApi
await cubit!.registerPushToken();
// В тестах TokenStorage может быть не инициализирован, поэтому метод может не вызваться
// Проверяем, что метод завершился без ошибок
expect(cubit!.state, isA<AuthState>());
});
});
}

View File

@@ -0,0 +1,83 @@
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('AuthState', () {
test('creates initial AuthState', () {
const state = AuthState();
expect(state, isA<AuthState>());
});
test('creates loading AuthState', () {
const state = AuthState.loading();
expect(state, isA<AuthState>());
expect(state, isA<AuthStateLoading>());
});
test('creates loaded AuthState with token', () {
const state = AuthState.loaded(token: 'test_token_123');
expect(state, isA<AuthState>());
expect(state, isA<AuthStateLoaded>());
expect((state as AuthStateLoaded).token, 'test_token_123');
});
test('creates error AuthState with error message', () {
const state = AuthState.error(errorMessage: 'Test error');
expect(state, isA<AuthState>());
expect(state, isA<AuthStateError>());
expect((state as AuthStateError).errorMessage, 'Test error');
});
test('different state types are not equal', () {
const loading = AuthState.loading();
const loaded = AuthState.loaded(token: 'token');
const error = AuthState.error(errorMessage: 'Error');
expect(loading == loaded, isFalse);
expect(loading == error, isFalse);
expect(loaded == error, isFalse);
});
test('same state types with same data are equal', () {
const state1 = AuthState.loaded(token: 'token');
const state2 = AuthState.loaded(token: 'token');
expect(state1 == state2, isTrue);
});
test('same state types with different data are not equal', () {
const state1 = AuthState.loaded(token: 'token1');
const state2 = AuthState.loaded(token: 'token2');
expect(state1 == state2, isFalse);
});
test('error states with different messages are not equal', () {
const state1 = AuthState.error(errorMessage: 'Error 1');
const state2 = AuthState.error(errorMessage: 'Error 2');
expect(state1 == state2, isFalse);
});
test('error states with same messages are equal', () {
const state1 = AuthState.error(errorMessage: 'Error');
const state2 = AuthState.error(errorMessage: 'Error');
expect(state1 == state2, isTrue);
});
test('initial AuthState instances are equal', () {
const state1 = AuthState();
const state2 = AuthState();
expect(state1 == state2, isTrue);
});
test('loading AuthState instances are equal', () {
const state1 = AuthState.loading();
const state2 = AuthState.loading();
expect(state1 == state2, isTrue);
});
});
}

View File

@@ -0,0 +1,43 @@
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/auth/presentation/pages/scan_qr_code_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ScanQRCodeScreen 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(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: child,
);
}
testWidgets('renders scan QR code screen', (tester) async {
await tester.pumpWidget(wrap(const ScanQRCodeScreen()));
await tester.pumpAndSettle();
expect(find.byType(ScanQRCodeScreen), findsOneWidget);
expect(find.text('Сканирование QR-кода'), findsOneWidget);
});
testWidgets('displays QR scanner UI', (tester) async {
await tester.pumpWidget(wrap(const ScanQRCodeScreen()));
await tester.pumpAndSettle();
expect(find.byType(ScanQRCodeScreen), findsOneWidget);
});
});
}

View File

@@ -17,6 +17,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:network_image_mock/network_image_mock.dart';
import 'blog_post_screen_smoke_test.mocks.dart';
@@ -105,5 +106,58 @@ void main() {
expect(find.byType(BlogPostScreen), findsOneWidget);
});
testWidgets('renders error state', (tester) async {
await mockNetworkImagesFor(() async {
final blogPostCubit = MockBlogPostCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
final state = BlogPostState(isLoading: false, blogPostModel: null, error: 'Test error');
when(blogPostCubit.state).thenReturn(state);
when(blogPostCubit.stream).thenAnswer((_) => Stream.value(state));
when(blogPostCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewPost(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(blogPostCubit, commentsCubit);
await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, const BlogPostScreen(id: 1)));
await tester.pump();
expect(find.byType(BlogPostScreen), findsOneWidget);
});
});
testWidgets('renders loaded state', (tester) async {
await mockNetworkImagesFor(() async {
final blogPostCubit = MockBlogPostCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
final state = BlogPostState(isLoading: false, blogPostModel: null, error: null);
when(blogPostCubit.state).thenReturn(state);
when(blogPostCubit.stream).thenAnswer((_) => Stream.value(state));
when(blogPostCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewPost(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(blogPostCubit, commentsCubit);
await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, const BlogPostScreen(id: 1)));
await tester.pump();
expect(find.byType(BlogPostScreen), findsOneWidget);
});
});
});
}

View File

@@ -317,6 +317,231 @@ void main() {
expect(loadedState, isNotNull);
});
test('getMyProfile - обработка неправильного формата даты рождения', () async {
cubit = ProfileAvatarCubit(profileRepository: mockRepository);
const testProfile = ProfileModel(
id: 1,
slug: 'test-user',
name: 'Test User',
email: 'test@example.com',
phoneConfirmationState: 'confirmed',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: 'https://example.com/icon.jpg',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
// Неправильный формат даты (не 3 части)
const testProfileDetails = ProfileDetailsModel(
firstName: 'Test',
lastName: 'User',
birthdate: 'invalid-date',
gender: 'male',
country: 'Russia',
city: 'Moscow',
lang: 'ru',
weight: 70.0,
height: 175,
chestCircumference: 100,
waistCircumference: 80,
hipCircumference: 90,
bloodPressureUp: 120,
bloodPressureDown: 80,
hasHypo: false,
hasHyper: false,
aboutAlergy: 'No allergies',
aboutTakingPils: 'No pills',
aboutSportHobbies: 'Running',
favoriteFoods: 'Fruits',
unfavoriteFoods: 'Fast food',
additionalInfo: 'Additional info',
isMoodFillable: true,
isBedtimeFillable: true,
activityCoefficient: 1.5,
);
when(mockRepository.getMyProfile())
.thenAnswer((_) async => testProfile);
when(mockRepository.getMyProfileDetails())
.thenAnswer((_) async => testProfileDetails);
ProfileAvatarState? loadedState;
final subscription = cubit!.stream.listen((state) {
if (state is ProfileAvatarStateLoaded) {
loadedState = state;
}
});
try {
await cubit!.getMyProfile();
await Future.delayed(const Duration(milliseconds: 100));
} catch (e) {
// Игнорируем ошибки плагинов
} finally {
await subscription.cancel();
}
// Профиль должен быть загружен, даже если дата в неправильном формате
expect(loadedState, isNotNull);
});
test('getMyProfile - обработка даты рождения с невалидными числами', () async {
cubit = ProfileAvatarCubit(profileRepository: mockRepository);
const testProfile = ProfileModel(
id: 1,
slug: 'test-user',
name: 'Test User',
email: 'test@example.com',
phoneConfirmationState: 'confirmed',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: 'https://example.com/icon.jpg',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
// Дата с невалидными числами (не может быть распарсена)
const testProfileDetails = ProfileDetailsModel(
firstName: 'Test',
lastName: 'User',
birthdate: 'abc-def-ghi',
gender: 'male',
country: 'Russia',
city: 'Moscow',
lang: 'ru',
weight: 70.0,
height: 175,
chestCircumference: 100,
waistCircumference: 80,
hipCircumference: 90,
bloodPressureUp: 120,
bloodPressureDown: 80,
hasHypo: false,
hasHyper: false,
aboutAlergy: 'No allergies',
aboutTakingPils: 'No pills',
aboutSportHobbies: 'Running',
favoriteFoods: 'Fruits',
unfavoriteFoods: 'Fast food',
additionalInfo: 'Additional info',
isMoodFillable: true,
isBedtimeFillable: true,
activityCoefficient: 1.5,
);
when(mockRepository.getMyProfile())
.thenAnswer((_) async => testProfile);
when(mockRepository.getMyProfileDetails())
.thenAnswer((_) async => testProfileDetails);
ProfileAvatarState? loadedState;
final subscription = cubit!.stream.listen((state) {
if (state is ProfileAvatarStateLoaded) {
loadedState = state;
}
});
try {
await cubit!.getMyProfile();
await Future.delayed(const Duration(milliseconds: 100));
} catch (e) {
// Игнорируем ошибки плагинов
} finally {
await subscription.cancel();
}
// Профиль должен быть загружен, даже если дата невалидна
expect(loadedState, isNotNull);
});
test('getMyProfile - обработка даты рождения с неполным форматом', () async {
cubit = ProfileAvatarCubit(profileRepository: mockRepository);
const testProfile = ProfileModel(
id: 1,
slug: 'test-user',
name: 'Test User',
email: 'test@example.com',
phoneConfirmationState: 'confirmed',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: 'https://example.com/icon.jpg',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
// Дата с неполным форматом (только 2 части вместо 3)
const testProfileDetails = ProfileDetailsModel(
firstName: 'Test',
lastName: 'User',
birthdate: '1990-01',
gender: 'male',
country: 'Russia',
city: 'Moscow',
lang: 'ru',
weight: 70.0,
height: 175,
chestCircumference: 100,
waistCircumference: 80,
hipCircumference: 90,
bloodPressureUp: 120,
bloodPressureDown: 80,
hasHypo: false,
hasHyper: false,
aboutAlergy: 'No allergies',
aboutTakingPils: 'No pills',
aboutSportHobbies: 'Running',
favoriteFoods: 'Fruits',
unfavoriteFoods: 'Fast food',
additionalInfo: 'Additional info',
isMoodFillable: true,
isBedtimeFillable: true,
activityCoefficient: 1.5,
);
when(mockRepository.getMyProfile())
.thenAnswer((_) async => testProfile);
when(mockRepository.getMyProfileDetails())
.thenAnswer((_) async => testProfileDetails);
ProfileAvatarState? loadedState;
final subscription = cubit!.stream.listen((state) {
if (state is ProfileAvatarStateLoaded) {
loadedState = state;
}
});
try {
await cubit!.getMyProfile();
await Future.delayed(const Duration(milliseconds: 100));
} catch (e) {
// Игнорируем ошибки плагинов
} finally {
await subscription.cancel();
}
// Профиль должен быть загружен, даже если дата в неполном формате
expect(loadedState, isNotNull);
});
});
}

View File

@@ -0,0 +1,169 @@
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ProfileAvatarState', () {
test('creates initial ProfileAvatarState', () {
const state = ProfileAvatarState();
expect(state, isA<ProfileAvatarState>());
});
test('creates loading ProfileAvatarState', () {
const state = ProfileAvatarState.loading();
expect(state, isA<ProfileAvatarState>());
expect(state, isA<ProfileAvatarStateLoading>());
});
test('creates loaded ProfileAvatarState with profile', () {
const profile = ProfileModel(
id: 1,
slug: 'test-user',
name: 'Test User',
email: 'test@example.com',
phoneConfirmationState: 'confirmed',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: 'https://example.com/icon.jpg',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
const state = ProfileAvatarState.loaded(profile: profile);
expect(state, isA<ProfileAvatarState>());
expect(state, isA<ProfileAvatarStateLoaded>());
expect((state as ProfileAvatarStateLoaded).profile, profile);
});
test('creates error ProfileAvatarState with error message', () {
const state = ProfileAvatarState.error(errorMessage: 'Test error');
expect(state, isA<ProfileAvatarState>());
expect(state, isA<ProfileAvatarStateError>());
expect((state as ProfileAvatarStateError).errorMessage, 'Test error');
});
test('different state types are not equal', () {
const profile = ProfileModel(
id: 1,
slug: 'test-user',
name: 'Test User',
email: 'test@example.com',
phoneConfirmationState: 'confirmed',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: 'https://example.com/icon.jpg',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
const loading = ProfileAvatarState.loading();
const loaded = ProfileAvatarState.loaded(profile: profile);
const error = ProfileAvatarState.error(errorMessage: 'Error');
expect(loading == loaded, isFalse);
expect(loading == error, isFalse);
expect(loaded == error, isFalse);
});
test('same state types with same data are equal', () {
const profile = ProfileModel(
id: 1,
slug: 'test-user',
name: 'Test User',
email: 'test@example.com',
phoneConfirmationState: 'confirmed',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: 'https://example.com/icon.jpg',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
const state1 = ProfileAvatarState.loaded(profile: profile);
const state2 = ProfileAvatarState.loaded(profile: profile);
expect(state1 == state2, isTrue);
});
test('same state types with different data are not equal', () {
const profile1 = ProfileModel(
id: 1,
slug: 'test-user',
name: 'Test User',
email: 'test@example.com',
phoneConfirmationState: 'confirmed',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: 'https://example.com/icon.jpg',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
const profile2 = ProfileModel(
id: 2,
slug: 'other-user',
name: 'Other User',
email: 'other@example.com',
phoneConfirmationState: 'confirmed',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: 'https://example.com/icon2.jpg',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
const state1 = ProfileAvatarState.loaded(profile: profile1);
const state2 = ProfileAvatarState.loaded(profile: profile2);
expect(state1 == state2, isFalse);
});
test('error states with different messages are not equal', () {
const state1 = ProfileAvatarState.error(errorMessage: 'Error 1');
const state2 = ProfileAvatarState.error(errorMessage: 'Error 2');
expect(state1 == state2, isFalse);
});
test('error states with same messages are equal', () {
const state1 = ProfileAvatarState.error(errorMessage: 'Error');
const state2 = ProfileAvatarState.error(errorMessage: 'Error');
expect(state1 == state2, isTrue);
});
test('initial ProfileAvatarState instances are equal', () {
const state1 = ProfileAvatarState();
const state2 = ProfileAvatarState();
expect(state1 == state2, isTrue);
});
test('loading ProfileAvatarState instances are equal', () {
const state1 = ProfileAvatarState.loading();
const state2 = ProfileAvatarState.loading();
expect(state1 == state2, isTrue);
});
});
}

View File

@@ -0,0 +1,102 @@
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/models/chat_room_model.dart';
import 'package:eatvio_app/features/chat/models/chat_room_settings_model.dart';
import 'package:eatvio_app/features/chat/models/chat_user_model.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ChatRoomState', () {
test('creates initial ChatRoomState', () {
const state = ChatRoomState();
expect(state, isA<ChatRoomState>());
});
test('creates loading ChatRoomState', () {
const state = ChatRoomState.loading();
expect(state, isA<ChatRoomState>());
expect(state, isA<ChatRoomStateLoading>());
});
test('creates loaded ChatRoomState with default values', () {
const chatRoomSettings = ChatRoomSettingsModel(name: 'Test Room', state: 'active');
const sender = ChatUserModel(id: 1, name: 'Sender', slug: 'sender');
const receiver = ChatUserModel(id: 2, name: 'Receiver', slug: 'receiver');
const users = Users(sender: sender, receiver: receiver);
const chatRoom = ChatRoomModel(id: 'room-1', chatRoomSettings: chatRoomSettings, users: users, chosenPlanId: null);
const state = ChatRoomState.loaded(chatRoom: chatRoom, chatMessages: [], hasCard: false, isTyping: false, isLoading: false);
expect(state, isA<ChatRoomState>());
expect(state, isA<ChatRoomStateLoaded>());
expect((state as ChatRoomStateLoaded).chatRoom, chatRoom);
expect(state.chatMessages, isEmpty);
expect(state.hasCard, isFalse);
expect(state.isTyping, isFalse);
expect(state.isLoading, isFalse);
});
test('creates loaded ChatRoomState with messages', () {
const chatRoomSettings = ChatRoomSettingsModel(name: 'Test Room', state: 'active');
const sender = ChatUserModel(id: 1, name: 'Sender', slug: 'sender');
const receiver = ChatUserModel(id: 2, name: 'Receiver', slug: 'receiver');
const users = Users(sender: sender, receiver: receiver);
const chatRoom = ChatRoomModel(id: 'room-1', chatRoomSettings: chatRoomSettings, users: users, chosenPlanId: null);
final message = ChatMessageModel(
id: 1,
data: 'Test message',
mediaData: null,
authorId: 1,
updatedAt: DateTime.now(),
isRead: false,
isCurrentUserAuthor: true,
roomId: 'room-1',
);
final state = ChatRoomState.loaded(chatRoom: chatRoom, chatMessages: [message], hasCard: true, isTyping: true, isLoading: true);
expect(state, isA<ChatRoomStateLoaded>());
expect((state as ChatRoomStateLoaded).chatMessages.length, 1);
expect(state.hasCard, isTrue);
expect(state.isTyping, isTrue);
expect(state.isLoading, isTrue);
});
test('creates error ChatRoomState', () {
const state = ChatRoomState.error(errorMessage: 'Test error');
expect(state, isA<ChatRoomState>());
expect(state, isA<ChatRoomStateError>());
expect((state as ChatRoomStateError).errorMessage, 'Test error');
});
test('different state types are not equal', () {
const chatRoomSettings = ChatRoomSettingsModel(name: 'Test Room', state: 'active');
const sender = ChatUserModel(id: 1, name: 'Sender', slug: 'sender');
const receiver = ChatUserModel(id: 2, name: 'Receiver', slug: 'receiver');
const users = Users(sender: sender, receiver: receiver);
const chatRoom = ChatRoomModel(id: 'room-1', chatRoomSettings: chatRoomSettings, users: users, chosenPlanId: null);
const loading = ChatRoomState.loading();
const loaded = ChatRoomState.loaded(chatRoom: chatRoom, chatMessages: [], hasCard: false, isTyping: false, isLoading: false);
const error = ChatRoomState.error(errorMessage: 'Error');
expect(loading == loaded, isFalse);
expect(loading == error, isFalse);
expect(loaded == error, isFalse);
});
});
}

View File

@@ -10,6 +10,8 @@ import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:eatvio_app/features/chat/models/payment_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
@@ -588,5 +590,774 @@ void main() {
expect(chatRoomCubit.state, isA<ChatRoomStateError>());
});
test('checkCards - устанавливает hasCard если есть карта с isDefault', () async {
const uuid = 'room-uuid';
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => testChatRoom);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
when(mockChatRepository.getChosenPlan(id: anyNamed('id'))).thenAnswer((_) async => throw Exception('No plan'));
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
// Мокаем карту с isDefault = true
final cardWithDefault = CardModel(id: 1, cardNumber: '1234', isDefault: true);
when(mockCardsRepository.getCards()).thenAnswer((_) async => [cardWithDefault]);
await chatRoomCubit.checkCards();
await Future.delayed(const Duration(milliseconds: 100));
final state = chatRoomCubit.state as ChatRoomStateLoaded;
expect(state.hasCard, true);
});
test('checkCards - не устанавливает hasCard если нет карты с isDefault', () async {
const uuid = 'room-uuid';
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => testChatRoom);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
when(mockChatRepository.getChosenPlan(id: anyNamed('id'))).thenAnswer((_) async => throw Exception('No plan'));
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
// Мокаем карту с isDefault = false
final cardWithoutDefault = CardModel(id: 1, cardNumber: '1234', isDefault: false);
when(mockCardsRepository.getCards()).thenAnswer((_) async => [cardWithoutDefault]);
await chatRoomCubit.checkCards();
await Future.delayed(const Duration(milliseconds: 100));
final state = chatRoomCubit.state as ChatRoomStateLoaded;
expect(state.hasCard, false);
});
test('getPaymentLink - возвращает ссылку на оплату когда есть chosenPlan', () async {
const uuid = 'room-uuid';
const planId = 123;
final chatRoomWithPlan = testChatRoom.copyWith(chosenPlanId: planId);
const testPlan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://example.com/image.jpg',
duration: 7,
state: 'active',
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test details',
packetsCount: 3,
author: Author(authorSlug: 'author', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
isFavorite: false,
minPriceOfOptions: 1000,
maxPriceOfOptions: 2000,
wasLikedByMe: false,
);
final chosenPlan = ChosenPlanModel(
id: planId,
optionType: 'options1',
startDate: DateTime.now(),
chosenPlanState: ChosenPlanStateEnum.statePendingPayment,
chatId: 'chat-1',
consultantId: 1,
plan: testPlan,
);
const paymentUrl = 'https://example.com/payment';
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
when(mockChatRepository.getPaymentLink(id: planId)).thenAnswer((_) async => PaymentModel(paymentUrl: paymentUrl));
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
final result = await chatRoomCubit.getPaymentLink();
expect(result, paymentUrl);
verify(mockChatRepository.getPaymentLink(id: planId)).called(1);
});
test('getPaymentLink - обрабатывает ошибку', () async {
const uuid = 'room-uuid';
const planId = 123;
final chatRoomWithPlan = testChatRoom.copyWith(chosenPlanId: planId);
const testPlan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://example.com/image.jpg',
duration: 7,
state: 'active',
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test details',
packetsCount: 3,
author: Author(authorSlug: 'author', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
isFavorite: false,
minPriceOfOptions: 1000,
maxPriceOfOptions: 2000,
wasLikedByMe: false,
);
final chosenPlan = ChosenPlanModel(
id: planId,
optionType: 'options1',
startDate: DateTime.now(),
chosenPlanState: ChosenPlanStateEnum.statePendingPayment,
chatId: 'chat-1',
consultantId: 1,
plan: testPlan,
);
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
when(mockChatRepository.getPaymentLink(id: planId)).thenThrow(Exception('Payment error'));
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
final result = await chatRoomCubit.getPaymentLink();
expect(result, null);
});
test('complainAboutConsultation - отправляет жалобу на консультацию', () async {
const uuid = 'room-uuid';
const planId = 123;
final chatRoomWithPlan = testChatRoom.copyWith(chosenPlanId: planId);
const testPlan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://example.com/image.jpg',
duration: 7,
state: 'active',
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test details',
packetsCount: 3,
author: Author(authorSlug: 'author', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
isFavorite: false,
minPriceOfOptions: 1000,
maxPriceOfOptions: 2000,
wasLikedByMe: false,
);
final chosenPlan = ChosenPlanModel(
id: planId,
optionType: 'options1',
startDate: DateTime.now(),
chosenPlanState: ChosenPlanStateEnum.stateStarted,
chatId: 'chat-1',
consultantId: 1,
plan: testPlan,
);
const description = 'Test complaint';
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
when(mockChatRepository.complainAboutConsultation(id: planId, body: anyNamed('body'))).thenAnswer((_) async => Future.value());
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
final result = await chatRoomCubit.complainAboutConsultation(description: description);
expect(result, true);
verify(mockChatRepository.complainAboutConsultation(id: planId, body: anyNamed('body'))).called(1);
});
test('complainAboutConsultation - возвращает false если нет planId', () async {
const uuid = 'room-uuid';
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => testChatRoom);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: anyNamed('id'))).thenAnswer((_) async => throw Exception('No plan'));
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
final result = await chatRoomCubit.complainAboutConsultation(description: 'Test');
expect(result, false);
});
test('complainAboutConsultation - обрабатывает ошибку', () async {
const uuid = 'room-uuid';
const planId = 123;
final chatRoomWithPlan = testChatRoom.copyWith(chosenPlanId: planId);
const testPlan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://example.com/image.jpg',
duration: 7,
state: 'active',
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test details',
packetsCount: 3,
author: Author(authorSlug: 'author', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
isFavorite: false,
minPriceOfOptions: 1000,
maxPriceOfOptions: 2000,
wasLikedByMe: false,
);
final chosenPlan = ChosenPlanModel(
id: planId,
optionType: 'options1',
startDate: DateTime.now(),
chosenPlanState: ChosenPlanStateEnum.stateStarted,
chatId: 'chat-1',
consultantId: 1,
plan: testPlan,
);
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
when(mockChatRepository.complainAboutConsultation(id: planId, body: anyNamed('body'))).thenThrow(Exception('Error'));
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
final result = await chatRoomCubit.complainAboutConsultation(description: 'Test');
expect(result, false);
});
test('finishChosenPlan - завершает выбранный план', () async {
const uuid = 'room-uuid';
const planId = 123;
final chatRoomWithPlan = testChatRoom.copyWith(chosenPlanId: planId);
const testPlan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://example.com/image.jpg',
duration: 7,
state: 'active',
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test details',
packetsCount: 3,
author: Author(authorSlug: 'author', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
isFavorite: false,
minPriceOfOptions: 1000,
maxPriceOfOptions: 2000,
wasLikedByMe: false,
);
final chosenPlan = ChosenPlanModel(
id: planId,
optionType: 'options1',
startDate: DateTime.now(),
chosenPlanState: ChosenPlanStateEnum.stateStarted,
chatId: 'chat-1',
consultantId: 1,
plan: testPlan,
);
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
when(mockChatRepository.finishChosenPlan(id: planId)).thenAnswer((_) async => Future.value());
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
final result = await chatRoomCubit.finishChosenPlan();
expect(result, true);
verify(mockChatRepository.finishChosenPlan(id: planId)).called(1);
});
test('finishChosenPlan - возвращает false если нет planId', () async {
const uuid = 'room-uuid';
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => testChatRoom);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: anyNamed('id'))).thenAnswer((_) async => throw Exception('No plan'));
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
final result = await chatRoomCubit.finishChosenPlan();
expect(result, false);
});
test('finishChosenPlan - обрабатывает ошибку', () async {
const uuid = 'room-uuid';
const planId = 123;
final chatRoomWithPlan = testChatRoom.copyWith(chosenPlanId: planId);
const testPlan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://example.com/image.jpg',
duration: 7,
state: 'active',
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test details',
packetsCount: 3,
author: Author(authorSlug: 'author', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
isFavorite: false,
minPriceOfOptions: 1000,
maxPriceOfOptions: 2000,
wasLikedByMe: false,
);
final chosenPlan = ChosenPlanModel(
id: planId,
optionType: 'options1',
startDate: DateTime.now(),
chosenPlanState: ChosenPlanStateEnum.stateStarted,
chatId: 'chat-1',
consultantId: 1,
plan: testPlan,
);
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
when(mockChatRepository.finishChosenPlan(id: planId)).thenThrow(Exception('Error'));
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
final result = await chatRoomCubit.finishChosenPlan();
expect(result, false);
});
test('loadNextPage - обрабатывает ошибку', () async {
const uuid = 'room-uuid';
final initialMessages = ChatMessagesModel(
messagesList: [
ChatMessageModel(
id: 1,
data: 'Initial message',
mediaData: null,
authorId: 1,
updatedAt: DateTime.now(),
isRead: false,
isCurrentUserAuthor: true,
roomId: 'chat-id',
),
],
meta: const ChatMetaModel(lastPage: 2),
);
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => testChatRoom);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => initialMessages);
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
when(mockChatRepository.getChosenPlan(id: anyNamed('id'))).thenAnswer((_) async => throw Exception('No plan'));
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
when(mockChatRepository.getChatMessages(uuid, 2)).thenThrow(Exception('Error'));
await chatRoomCubit.loadNextPage(uuid);
await Future.delayed(const Duration(milliseconds: 100));
expect(chatRoomCubit.state, isA<ChatRoomStateError>());
});
test('loadNextPage - не загружает если currentPage >= lastPage', () async {
const uuid = 'room-uuid';
final initialMessages = ChatMessagesModel(
messagesList: [
ChatMessageModel(
id: 1,
data: 'Initial message',
mediaData: null,
authorId: 1,
updatedAt: DateTime.now(),
isRead: false,
isCurrentUserAuthor: true,
roomId: 'chat-id',
),
],
meta: const ChatMetaModel(lastPage: 1),
);
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => testChatRoom);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => initialMessages);
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
when(mockChatRepository.getChosenPlan(id: anyNamed('id'))).thenAnswer((_) async => throw Exception('No plan'));
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
await chatRoomCubit.loadNextPage(uuid);
await Future.delayed(const Duration(milliseconds: 100));
// Не должно вызывать getChatMessages для страницы 2
verifyNever(mockChatRepository.getChatMessages(uuid, 2));
});
test('resendMessage - не делает ничего если сообщение не найдено', () async {
const uuid = 'room-uuid';
const messageUuid = 'non-existent-uuid';
final initialMessages = ChatMessagesModel(
messagesList: [
ChatMessageModel(
id: 1,
data: 'Test message',
mediaData: null,
authorId: 1,
updatedAt: DateTime.now(),
isRead: false,
isCurrentUserAuthor: true,
roomId: 'chat-id',
),
],
meta: const ChatMetaModel(lastPage: 1),
);
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => testChatRoom);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => initialMessages);
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
when(mockChatRepository.getChosenPlan(id: anyNamed('id'))).thenAnswer((_) async => throw Exception('No plan'));
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
await chatRoomCubit.resendMessage(uuid: messageUuid, userSlug: 'user-1');
await Future.delayed(const Duration(milliseconds: 100));
verifyNever(mockChatRepository.sendMessage(data: anyNamed('data'), files: anyNamed('files'), slug: anyNamed('slug')));
});
test('initChat - вызывает checkCards если consultantId == sender.id', () async {
const uuid = 'room-uuid';
const planId = 123;
final chatRoomWithPlan = testChatRoom.copyWith(chosenPlanId: planId);
const testPlan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://example.com/image.jpg',
duration: 7,
state: 'active',
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test details',
packetsCount: 3,
author: Author(authorSlug: 'author', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
isFavorite: false,
minPriceOfOptions: 1000,
maxPriceOfOptions: 2000,
wasLikedByMe: false,
);
final chosenPlan = ChosenPlanModel(
id: planId,
optionType: 'options1',
startDate: DateTime.now(),
chosenPlanState: ChosenPlanStateEnum.stateStarted,
chatId: 'chat-1',
consultantId: 1, // совпадает с sender.id
plan: testPlan,
);
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
when(mockCardsRepository.getCards()).thenAnswer((_) async => []);
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
verify(mockCardsRepository.getCards()).called(1);
});
test('initChat - не вызывает checkCards если consultantId != sender.id', () async {
const uuid = 'room-uuid';
const planId = 123;
final chatRoomWithPlan = testChatRoom.copyWith(chosenPlanId: planId);
const testPlan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://example.com/image.jpg',
duration: 7,
state: 'active',
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test details',
packetsCount: 3,
author: Author(authorSlug: 'author', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
isFavorite: false,
minPriceOfOptions: 1000,
maxPriceOfOptions: 2000,
wasLikedByMe: false,
);
final chosenPlan = ChosenPlanModel(
id: planId,
optionType: 'options1',
startDate: DateTime.now(),
chosenPlanState: ChosenPlanStateEnum.stateStarted,
chatId: 'chat-1',
consultantId: 2, // не совпадает с sender.id (1)
plan: testPlan,
);
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
verifyNever(mockCardsRepository.getCards());
});
test('choseStartPlanDate - обрабатывает ошибку', () async {
const uuid = 'room-uuid';
const planId = 123;
final chatRoomWithPlan = testChatRoom.copyWith(chosenPlanId: planId);
const testPlan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://example.com/image.jpg',
duration: 7,
state: 'active',
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test details',
packetsCount: 3,
author: Author(authorSlug: 'author', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
isFavorite: false,
minPriceOfOptions: 1000,
maxPriceOfOptions: 2000,
wasLikedByMe: false,
);
final chosenPlan = ChosenPlanModel(
id: planId,
optionType: 'options1',
startDate: DateTime(2024, 1, 1),
chosenPlanState: ChosenPlanStateEnum.statePendingPayment,
chatId: 'chat-1',
consultantId: 1,
plan: testPlan,
);
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
when(mockChatRepository.choseStartPlanDate(id: planId, date: anyNamed('date'))).thenThrow(Exception('Error'));
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
final result = await chatRoomCubit.choseStartPlanDate(DateTime(2024, 1, 15));
expect(result, false);
});
test('checkCards - обрабатывает ошибку', () async {
const uuid = 'room-uuid';
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => testChatRoom);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
when(mockChatRepository.getChosenPlan(id: anyNamed('id'))).thenAnswer((_) async => throw Exception('No plan'));
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
when(mockCardsRepository.getCards()).thenThrow(Exception('Error'));
await chatRoomCubit.checkCards();
await Future.delayed(const Duration(milliseconds: 100));
// Не должно быть исключений, ошибка должна быть обработана
expect(chatRoomCubit.state, isA<ChatRoomStateLoaded>());
});
test('deleteMessage - не делает ничего если state не loaded', () {
const uuid = 'message-uuid';
chatRoomCubit.deleteMessage(uuid);
// Не должно быть исключений
expect(chatRoomCubit.state, isA<ChatRoomStateLoading>());
});
test('resendMessage - не делает ничего если state не loaded', () async {
const uuid = 'message-uuid';
await chatRoomCubit.resendMessage(uuid: uuid, userSlug: 'user-1');
verifyNever(mockChatRepository.sendMessage(data: anyNamed('data'), files: anyNamed('files'), slug: anyNamed('slug')));
});
test('loadNextPage - обновляет currentPage после успешной загрузки', () async {
const uuid = 'room-uuid';
final initialMessages = ChatMessagesModel(
messagesList: [
ChatMessageModel(
id: 1,
data: 'Initial message',
mediaData: null,
authorId: 1,
updatedAt: DateTime.now(),
isRead: false,
isCurrentUserAuthor: true,
roomId: 'chat-id',
),
],
meta: const ChatMetaModel(lastPage: 2),
);
final nextPageMessages = ChatMessagesModel(
messagesList: [
ChatMessageModel(
id: 2,
data: 'Next page message',
mediaData: null,
authorId: 1,
updatedAt: DateTime.now(),
isRead: false,
isCurrentUserAuthor: true,
roomId: 'chat-id',
),
],
meta: const ChatMetaModel(lastPage: 2),
);
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => testChatRoom);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => initialMessages);
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
when(mockChatRepository.getChosenPlan(id: anyNamed('id'))).thenAnswer((_) async => throw Exception('No plan'));
await chatRoomCubit.initChat(uuid);
await Future.delayed(const Duration(milliseconds: 200));
expect(chatRoomCubit.currentPage, 1);
when(mockChatRepository.getChatMessages(uuid, 2)).thenAnswer((_) async => nextPageMessages);
await chatRoomCubit.loadNextPage(uuid);
await Future.delayed(const Duration(milliseconds: 200));
expect(chatRoomCubit.currentPage, 2);
});
test('loadNextPage - не загружает если state не loaded', () async {
const uuid = 'room-uuid';
// loadNextPage проверяет lastPage, который не инициализирован до initChat
// Метод выбросит исключение, но мы просто проверяем, что getChatMessages не вызывается
try {
await chatRoomCubit.loadNextPage(uuid);
} catch (e) {
// Ожидаем исключение, так как lastPage не инициализирован
expect(e, isNotNull);
}
verifyNever(mockChatRepository.getChatMessages(any, any));
});
});
}

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:eatvio_app/core/enums/base_state.dart';
import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
@@ -303,5 +304,175 @@ void main() {
expect(chatsCubit.state.state, BaseState.error);
expect(chatsCubit.state.errorMessage, isNotNull);
});
test('_setNewLastMessage - обновляет последнее сообщение в чате', () async {
const chatId = 'chat-id';
const chatRoom = ChatRoom(
id: chatId,
lastMessage: null,
users: Users(
fromChatUser: ChatUser(id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: ''),
toChatUser: ChatUser(id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: ''),
),
unreadMessagesCount: 0,
chosenPlan: null,
);
const pagination = ChatRoomPaginationModel(
data: [chatRoom],
unreadChatsCount: 0,
meta: Meta(currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 1),
);
chatsCubit.emit(ChatsState(state: BaseState.success, chatRooms: pagination.data));
final newMessage = ChatMessageModel(
id: 1,
data: 'Test message',
mediaData: null,
urlsMediaInfo: null,
authorId: 1,
updatedAt: DateTime.now(),
isRead: false,
isCurrentUserAuthor: true,
roomId: chatId,
type: 'text',
);
// Используем рефлексию для вызова приватного метода через тестовый доступ
// Или проверяем через публичный API, если есть
// В данном случае, _setNewLastMessage вызывается через newUserMessagesStream
final streamController = StreamController<ChatMessageModel>();
when(mockChatRepository.newUserMessagesStream).thenAnswer((_) => streamController.stream);
// Пересоздаем cubit чтобы подписаться на stream
await chatsCubit.close();
chatsCubit = ChatsCubit(chatRepository: mockChatRepository, wsManager: mockWsManager, profileRepository: mockProfileRepository);
chatsCubit.emit(ChatsState(state: BaseState.success, chatRooms: pagination.data));
streamController.add(newMessage);
await Future.delayed(const Duration(milliseconds: 100));
final updatedState = chatsCubit.state;
final updatedRoom = updatedState.chatRooms.firstWhere((r) => r.id == chatId);
expect(updatedRoom.lastMessage, isNotNull);
expect(updatedRoom.lastMessage?.id, 1);
await streamController.close();
});
test('_setNewLastMessage - обновляет последнее сообщение через newUserMessagesStream', () async {
const chatId = 'chat-id';
const chatRoom = ChatRoom(
id: chatId,
lastMessage: null,
users: Users(
fromChatUser: ChatUser(id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: ''),
toChatUser: ChatUser(id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: ''),
),
unreadMessagesCount: 0,
chosenPlan: null,
);
const pagination = ChatRoomPaginationModel(
data: [chatRoom],
unreadChatsCount: 0,
meta: Meta(currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 1),
);
final newMessage = ChatMessageModel(
id: 1,
data: 'Test message',
mediaData: null,
urlsMediaInfo: null,
authorId: 1,
updatedAt: DateTime.now(),
isRead: false,
isCurrentUserAuthor: true,
roomId: chatId,
type: 'text',
);
final streamController = StreamController<ChatMessageModel>();
when(mockChatRepository.newUserMessagesStream).thenAnswer((_) => streamController.stream);
// Пересоздаем cubit чтобы подписаться на stream
await chatsCubit.close();
chatsCubit = ChatsCubit(chatRepository: mockChatRepository, wsManager: mockWsManager, profileRepository: mockProfileRepository);
chatsCubit.emit(ChatsState(state: BaseState.success, chatRooms: pagination.data));
streamController.add(newMessage);
await Future.delayed(const Duration(milliseconds: 100));
final updatedState = chatsCubit.state;
final updatedRoom = updatedState.chatRooms.firstWhere((r) => r.id == chatId);
expect(updatedRoom.lastMessage, isNotNull);
expect(updatedRoom.lastMessage?.id, 1);
await streamController.close();
});
test('reduceUnreadMessagesCount - обрабатывает чат который не существует', () {
const chatId = 'non-existent-chat';
const count = 2;
const chatRoom = ChatRoom(
id: 'other-chat-id',
lastMessage: null,
users: Users(
fromChatUser: ChatUser(id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: ''),
toChatUser: ChatUser(id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: ''),
),
unreadMessagesCount: 5,
chosenPlan: null,
);
const pagination = ChatRoomPaginationModel(
data: [chatRoom],
unreadChatsCount: 1,
meta: Meta(currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 1),
);
chatsCubit.emit(ChatsState(state: BaseState.success, chatRooms: pagination.data, chatsWithUnreadMessagesCount: 1));
chatsCubit.reduceUnreadMessagesCount(chatId: chatId, count: count);
final updatedState = chatsCubit.state;
// Чат не должен измениться
expect(updatedState.chatRooms.length, 1);
expect(updatedState.chatRooms.first.unreadMessagesCount, 5);
expect(updatedState.chatsWithUnreadMessagesCount, 1);
});
test('reduceUnreadMessagesCount - обрабатывает уменьшение больше чем unreadMessagesCount', () {
const chatId = 'chat-id';
const count = 10; // Больше чем unreadMessagesCount
const chatRoom = ChatRoom(
id: chatId,
lastMessage: null,
users: Users(
fromChatUser: ChatUser(id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: ''),
toChatUser: ChatUser(id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: ''),
),
unreadMessagesCount: 5,
chosenPlan: null,
);
const pagination = ChatRoomPaginationModel(
data: [chatRoom],
unreadChatsCount: 1,
meta: Meta(currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 1),
);
chatsCubit.emit(ChatsState(state: BaseState.success, chatRooms: pagination.data, chatsWithUnreadMessagesCount: 1));
chatsCubit.reduceUnreadMessagesCount(chatId: chatId, count: count);
final updatedState = chatsCubit.state;
final updatedRoom = updatedState.chatRooms.firstWhere((r) => r.id == chatId);
expect(updatedRoom.unreadMessagesCount, 0);
expect(updatedState.chatsWithUnreadMessagesCount, 0);
});
});
}

View File

@@ -0,0 +1,128 @@
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/features/complaining/models/complain_model.dart';
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ComplainingState', () {
test('creates ComplainingState with default values', () {
final state = ComplainingState();
expect(state.isLoading, false);
expect(state.wasSentSuccessfully, null);
expect(state.category, null);
expect(state.contentType, null);
expect(state.contentId, null);
expect(state.contentSlug, null);
expect(state.complainInfo, isA<ComplainModel>());
});
test('creates ComplainingState with custom values', () {
final state = ComplainingState(
isLoading: true,
wasSentSuccessfully: true,
category: ComplainType.spam,
contentType: ComplainedContent.recipe,
contentId: 1,
contentSlug: 'test-slug',
complainInfo: const ComplainModel(
description: 'Test description',
tags: ['tag1', 'tag2'],
),
);
expect(state.isLoading, true);
expect(state.wasSentSuccessfully, true);
expect(state.category, ComplainType.spam);
expect(state.contentType, ComplainedContent.recipe);
expect(state.contentId, 1);
expect(state.contentSlug, 'test-slug');
expect(state.complainInfo.description, 'Test description');
expect(state.complainInfo.tags, ['tag1', 'tag2']);
});
test('copyWith creates new instance with updated values', () {
final original = ComplainingState();
final updated = original.copyWith(
isLoading: true,
category: ComplainType.spam,
contentType: ComplainedContent.plan,
);
expect(updated.isLoading, true);
expect(updated.category, ComplainType.spam);
expect(updated.contentType, ComplainedContent.plan);
expect(original.isLoading, false);
});
test('copyWith keeps original values when not updated', () {
final original = ComplainingState(
isLoading: true,
category: ComplainType.spam,
);
final updated = original.copyWith();
expect(updated.isLoading, original.isLoading);
expect(updated.category, original.category);
});
test('copyWith.complainInfo updates complainInfo', () {
final original = ComplainingState();
final updated = original.copyWith.complainInfo(
description: 'New description',
tags: ['new_tag'],
);
expect(updated.complainInfo.description, 'New description');
expect(updated.complainInfo.tags, ['new_tag']);
expect(original.complainInfo.description, isNull);
});
test('handles different ComplainType values', () {
final state1 = ComplainingState(category: ComplainType.spam);
final state2 = ComplainingState(category: ComplainType.lawViolation);
final state3 = ComplainingState(category: ComplainType.deception);
final state4 = ComplainingState(category: ComplainType.violenceDndHostility);
final state5 = ComplainingState(category: ComplainType.explicitImage);
expect(state1.category, ComplainType.spam);
expect(state2.category, ComplainType.lawViolation);
expect(state3.category, ComplainType.deception);
expect(state4.category, ComplainType.violenceDndHostility);
expect(state5.category, ComplainType.explicitImage);
});
test('handles different ComplainedContent values', () {
final state1 = ComplainingState(contentType: ComplainedContent.diet);
final state2 = ComplainingState(contentType: ComplainedContent.plan);
final state3 = ComplainingState(contentType: ComplainedContent.recipe);
expect(state1.contentType, ComplainedContent.diet);
expect(state2.contentType, ComplainedContent.plan);
expect(state3.contentType, ComplainedContent.recipe);
});
test('equality works correctly', () {
final state1 = ComplainingState(
isLoading: true,
category: ComplainType.spam,
contentType: ComplainedContent.recipe,
contentId: 1,
);
final state2 = ComplainingState(
isLoading: true,
category: ComplainType.spam,
contentType: ComplainedContent.recipe,
contentId: 1,
);
final state3 = ComplainingState(
isLoading: false,
category: ComplainType.spam,
contentType: ComplainedContent.recipe,
contentId: 1,
);
expect(state1 == state2, isTrue);
expect(state1 == state3, isFalse);
});
});
}

View File

@@ -0,0 +1,89 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/enums/base_state.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart';
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'consultations_screen_smoke_test.mocks.dart';
@GenerateNiceMocks([MockSpec<ConsultationsCubit>()])
void main() {
group('ConsultationsScreen 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(Widget child, ConsultationsCubit cubit) {
return MaterialApp.router(
theme: ThemeHelper().themeData(),
routerConfig: GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => Scaffold(
body: BlocProvider<ConsultationsCubit>.value(value: cubit, child: child),
),
),
],
),
);
}
testWidgets('renders consultations screen', (tester) async {
final cubit = MockConsultationsCubit();
final state = ConsultationsState(state: BaseState.initial, consultations: []);
when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state));
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(wrap(const ConsultationsContent(), cubit));
await tester.pump();
expect(find.byType(ConsultationsContent), findsOneWidget);
});
testWidgets('displays consultations title', (tester) async {
final cubit = MockConsultationsCubit();
final state = ConsultationsState(state: BaseState.success, consultations: []);
when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state));
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(wrap(const ConsultationsContent(), cubit));
await tester.pump();
expect(find.text('Консультации'), findsOneWidget);
});
testWidgets('displays archive button when loaded', (tester) async {
final cubit = MockConsultationsCubit();
final state = ConsultationsState(state: BaseState.success, consultations: []);
when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state));
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(wrap(const ConsultationsContent(), cubit));
await tester.pump();
expect(find.text('Архив консультаций'), findsOneWidget);
});
});
}

View File

@@ -1,9 +1,13 @@
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/features/create_diet/presentation/cubit/create_diet_cubit.dart';
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
@@ -19,7 +23,13 @@ void main() {
late MockProfileRepository profileRepository;
late MockGlossaryRepository glossaryRepository;
setUp(() {
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'));
dietRepository = MockCreateDietRepository();
profileRepository = MockProfileRepository();
glossaryRepository = MockGlossaryRepository();
@@ -220,5 +230,328 @@ void main() {
cubit.pasteCopiedDay(0);
expect(cubit.state.dietData.dailySchedule?[0][0], isEmpty);
});
test('getDietForPreview returns null when form is invalid', () async {
final result = await cubit.getDietForPreview();
expect(result, null);
});
test('createDiet returns validation error when form is invalid', () async {
final result = await cubit.createDiet();
expect(result, 'validation');
expect(cubit.state.isDietPublishing, false);
});
test('updateDiet returns error when form is invalid', () async {
final result = await cubit.updateDiet(1);
expect(result, 'error');
});
test('updateDiet handles error when image compression fails', () async {
cubit.setName('Test Diet');
cubit.setDetails('Test Description');
cubit.setDuration(0);
cubit.setDietState(DietState.active);
const portion = DailySchedule(
name: 'Apple',
portion: Portion.grams,
quantity: 100,
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
productNutritionalValueId: '1',
);
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
// Set image with non-existent file to trigger error in compression
final image = XFile('non_existent_image.jpg');
cubit.setImage(image);
final result = await cubit.updateDiet(1);
// Should return 'error' due to image compression failure
expect(result, 'error');
});
test('createDiet validates name length', () async {
cubit.setName('Te'); // Less than 3 characters
cubit.setDetails('Test Description');
cubit.setDuration(0);
cubit.setDietState(DietState.active);
const portion = DailySchedule(
name: 'Apple',
portion: Portion.grams,
quantity: 100,
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
productNutritionalValueId: '1',
);
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
final image = XFile('test_image.jpg');
cubit.setImage(image);
final result = await cubit.createDiet();
expect(result, 'validation');
expect(cubit.state.dietNameError, 'Длинна названия не может быть меньше 3 символов');
});
test('createDiet validates details length', () async {
cubit.setName('Test Diet');
cubit.setDetails('Te'); // Less than 3 characters
cubit.setDuration(0);
cubit.setDietState(DietState.active);
const portion = DailySchedule(
name: 'Apple',
portion: Portion.grams,
quantity: 100,
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
productNutritionalValueId: '1',
);
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
final image = XFile('test_image.jpg');
cubit.setImage(image);
final result = await cubit.createDiet();
expect(result, 'validation');
expect(cubit.state.dietDescriptionError, 'Длинна описания не может быть меньше 3 символов');
});
test('addPortion validates day when adding portion', () {
cubit.setDuration(0);
const portion = DailySchedule(
name: 'Apple',
portion: Portion.grams,
quantity: 100,
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
productNutritionalValueId: '1',
);
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
expect(cubit.state.dailyScheduleErrors[0], '');
});
test('removePortion removes portion from daily schedule', () {
cubit.setDuration(0);
const portion = DailySchedule(
name: 'Apple',
portion: Portion.grams,
quantity: 100,
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
productNutritionalValueId: '1',
);
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
expect(cubit.state.dietData.dailySchedule?[0][0].length, 1);
cubit.removePortion(day: 0, meal: 0, portionIndex: 0);
expect(cubit.state.dietData.dailySchedule?[0][0].length, 0);
});
test('setInitialData returns early when diet is null', () async {
await cubit.setInitialData(null);
expect(cubit.state.isDietBeingEdited, false);
expect(cubit.dietId, isNull);
});
test('setInitialData initializes data for editing diet', () async {
const testDiet = Diet(
id: 1,
slug: 'test-diet',
name: 'Test Diet',
image: 'test-image.jpg',
duration: 7,
state: DietState.active,
type: DietType(light: true, veg: false, carb: false, meat: false),
details: 'Test details',
dailySchedule: {
1: {
1: {1: DailySchedule(name: 'Apple', portion: Portion.grams, quantity: 100, dailyNutrition: null, productNutritionalValueId: null)},
},
},
author: Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: '', authorIsMe: false),
totalLikes: 0,
totalViews: 0,
totalComments: 0,
wasLikedByMe: false,
isFavorite: false,
dailyNutrition: [],
);
final dailyScheduleWithNutrition = {
1: {
1: {1: const DailySchedule(name: 'Apple', portion: Portion.grams, quantity: 100, dailyNutrition: null, productNutritionalValueId: null)},
},
};
when(dietRepository.getDietPortionsNutrition(1)).thenAnswer((_) async => dailyScheduleWithNutrition);
await cubit.setInitialData(testDiet);
expect(cubit.dietId, 1);
expect(cubit.state.isDietBeingEdited, true);
expect(cubit.state.dietData.name, 'Test Diet');
expect(cubit.state.dietData.details, 'Test details');
expect(cubit.state.dietData.duration, 7);
expect(cubit.state.dietData.type['light'], true);
expect(cubit.state.dietData.type['veg'], false);
verify(dietRepository.getDietPortionsNutrition(1)).called(1);
});
test('createDiet validates empty name', () async {
cubit.setName('');
cubit.setDetails('Test Description');
cubit.setDuration(0);
cubit.setDietState(DietState.active);
const portion = DailySchedule(
name: 'Apple',
portion: Portion.grams,
quantity: 100,
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
productNutritionalValueId: '1',
);
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
final image = XFile('test_image.jpg');
cubit.setImage(image);
final result = await cubit.createDiet();
expect(result, 'validation');
expect(cubit.state.dietNameError, 'Поле не заполнено');
});
test('createDiet validates empty details', () async {
cubit.setName('Test Diet');
cubit.setDetails('');
cubit.setDuration(0);
cubit.setDietState(DietState.active);
const portion = DailySchedule(
name: 'Apple',
portion: Portion.grams,
quantity: 100,
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
productNutritionalValueId: '1',
);
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
final image = XFile('test_image.jpg');
cubit.setImage(image);
final result = await cubit.createDiet();
expect(result, 'validation');
expect(cubit.state.dietDescriptionError, 'Поле не заполнено');
});
test('createDiet validates missing duration', () async {
cubit.setName('Test Diet');
cubit.setDetails('Test Description');
cubit.setDietState(DietState.active);
const portion = DailySchedule(
name: 'Apple',
portion: Portion.grams,
quantity: 100,
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
productNutritionalValueId: '1',
);
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
final image = XFile('test_image.jpg');
cubit.setImage(image);
final result = await cubit.createDiet();
expect(result, 'validation');
expect(cubit.state.dietDurationError, 'Срок диеты не выбран');
});
test('createDiet validates missing state', () async {
cubit.setName('Test Diet');
cubit.setDetails('Test Description');
cubit.setDuration(0);
const portion = DailySchedule(
name: 'Apple',
portion: Portion.grams,
quantity: 100,
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
productNutritionalValueId: '1',
);
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
final image = XFile('test_image.jpg');
cubit.setImage(image);
final result = await cubit.createDiet();
expect(result, 'validation');
expect(cubit.state.dietStateError, 'Статус диеты не выбран');
});
test('createDiet validates missing image', () async {
cubit.setName('Test Diet');
cubit.setDetails('Test Description');
cubit.setDuration(0);
cubit.setDietState(DietState.active);
const portion = DailySchedule(
name: 'Apple',
portion: Portion.grams,
quantity: 100,
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
productNutritionalValueId: '1',
);
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
final result = await cubit.createDiet();
expect(result, 'validation');
expect(cubit.state.dietImageError, 'Не добавлена обложка');
});
test('createDiet validates empty daily schedule', () async {
cubit.setName('Test Diet');
cubit.setDetails('Test Description');
cubit.setDuration(0);
cubit.setDietState(DietState.active);
final image = XFile('test_image.jpg');
cubit.setImage(image);
final result = await cubit.createDiet();
expect(result, 'validation');
expect(cubit.state.dailyScheduleErrors[0], 'Добавьте продукты');
});
test('pasteCopiedDay validates day when pasting', () {
cubit.setDuration(0);
const portion = DailySchedule(
name: 'Apple',
portion: Portion.grams,
quantity: 100,
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
productNutritionalValueId: '1',
);
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
cubit.copyDay(0);
cubit.pasteCopiedDay(1);
expect(cubit.state.dietData.dailySchedule?[1][0].length, 1);
expect(cubit.state.dailyScheduleErrors[1], '');
});
test('_isDayValid returns false when day is empty', () {
cubit.setDuration(0);
final isValid = cubit.state.dietData.dailySchedule?[0].every((meal) => meal.isEmpty) ?? false;
expect(isValid, true);
});
test('_isDayValid returns true when day has portions', () {
cubit.setDuration(0);
const portion = DailySchedule(
name: 'Apple',
portion: Portion.grams,
quantity: 100,
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
productNutritionalValueId: '1',
);
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
final isValid = cubit.state.dietData.dailySchedule?[0].every((meal) => meal.isEmpty) ?? true;
expect(isValid, false);
});
});
}

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
});
});
});

Some files were not shown because too many files have changed in this diff Show More