Merge pull request 'main' (#836) from main into test-flight

Reviewed-on: eatvio/eatvio_app#836
This commit is contained in:
mso7nueh
2025-12-25 14:20:16 +00:00
266 changed files with 23325 additions and 9567 deletions

View File

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

View File

@@ -1,3 +1,4 @@
import 'dart:developer' as developer;
import 'package:eatvio_app/core/routing/auth_routes.dart';
import 'package:eatvio_app/core/routing/content_routes.dart';
import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
@@ -13,6 +14,9 @@ import 'package:go_router/go_router.dart';
class AppRouter {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// Домены для deep links
static const List<String> _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru'];
static GoRouter appRouter = GoRouter(
initialLocation: RouteConstants.updateScreen,
navigatorKey: navigatorKey,
@@ -27,51 +31,128 @@ class AppRouter {
);
/// Обработка редиректов для deep links
/// Возвращает null для обычной навигации, чтобы не мешать внутренним переходам
static String? _handleRedirect(BuildContext context, GoRouterState state) {
final segments = state.uri.pathSegments;
if (segments.length == 4) {
final type = segments[1];
switch (type) {
case 'diary':
FlutterNativeSplash.remove();
return '/diary-screen';
}
final uri = state.uri;
final host = uri.host.toLowerCase();
final segments = uri.pathSegments;
// Проверяем, является ли это deep link по домену
final isDeepLink = _deepLinkHosts.contains(host);
// Если это не deep link, не мешаем обычной навигации
if (!isDeepLink) {
return null;
}
if (segments.length == 3) {
// Логирование для отладки
developer.log(
'Deep link detected: host=$host, path=${uri.path}, segments=$segments',
name: 'AppRouter',
);
// Проверка на пустой путь или некорректный формат
if (segments.isEmpty) {
developer.log('Empty segments in deep link, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
// Проверка языка (первый сегмент должен быть ru или en)
if (segments[0] != 'ru' && segments[0] != 'en') {
developer.log(
'Invalid language segment: ${segments[0]}, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
// Обработка deep links с учетом языка
// Формат: /{lang}/{type}/{identifier?}
if (segments.length == 4) {
// Формат: /{lang}/diary/{...}
final type = segments[1];
if (type == 'diary') {
developer.log('Deep link: diary, redirecting to diary-screen', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
}
developer.log(
'Unknown deep link type for 4 segments: $type, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
} else if (segments.length == 3) {
// Формат: /{lang}/{type}/{identifier}
final type = segments[1];
final contentIdentifier = segments[2];
switch (type) {
case 'recipe':
developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-recipes-screen?slug=$contentIdentifier';
case 'blog_post':
developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/blog-post-screen?id=$contentIdentifier';
case 'plan':
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-plan-screen?slug=$contentIdentifier';
case 'diet':
developer.log('Deep link: diet/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-diet-screen?slug=$contentIdentifier';
case 'chat':
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/chat-select-screen';
return RouteConstants.chatSelectScreen;
case 'profile':
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/profile-screen?slug=$contentIdentifier';
}
}
if (segments.length == 2) {
final type = segments[1];
switch (type) {
case 'chat':
case 'diary':
developer.log('Deep link: diary/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/chat-select-screen';
return RouteConstants.diaryScreen;
default:
developer.log(
'Unknown deep link type: $type, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
} else if (segments.length == 2) {
// Формат: /{lang}/{type} - только для chat
final type = segments[1];
if (type == 'chat') {
developer.log('Deep link: chat', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
} else {
developer.log(
'Unknown deep link type for 2 segments: $type, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
} else {
// Неизвестный формат deep link
developer.log(
'Unknown deep link format: ${segments.length} segments, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
return null;
}
static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) {

View File

@@ -76,7 +76,7 @@ class CubitRegistrations {
/// Регистрация Cubit'ов как фабрик
static void _registerFactoryCubits() {
// Auth related cubits
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
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()));

View File

@@ -17,6 +17,7 @@ import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/barcode/repositories/barcode_repository.dart';
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
@@ -55,6 +56,7 @@ class RepositoryRegistrations {
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
sl.registerLazySingleton<BarcodeRepository>(() => BarcodeRepository(barcodeService: sl(), dio: sl()));
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));

View File

@@ -19,6 +19,7 @@ import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/diets/services/diet_service.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart';
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
@@ -56,6 +57,7 @@ class ServiceRegistrations {
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
sl.registerLazySingleton<BarcodeService>(() => BarcodeService(sl()));
sl.registerLazySingleton<DietService>(() => DietService(sl()));
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));

View File

@@ -1,11 +1,9 @@
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/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,23 +57,14 @@ class OtherRoutes {
redirect: (context, state) async {
try {
final prefs = sl<Prefs>();
final firebaseApi = sl<FirebaseApi>();
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
await firebaseApi.registerPushToken();
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
if (!welcomeScreenWasShowed) {
return null;
} else if (await TokenStorage.haveTokenInStorage()) {
return '/feed-screen';
} else {
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
if (!hasAnonymousTokenBeenSent) {
await firebaseApi.registerAnonymousPushToken();
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
}
return '/auth-screen';
}
} catch (e) {
return '/feed-screen';
return '/auth-screen';
}
},
),
@@ -247,7 +236,7 @@ class OtherRoutes {
GoRoute(
path: PersonalDietSuggestionScreen.path,
name: PersonalDietSuggestionScreen.name,
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int,),
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int),
),
];
}

View File

@@ -1,19 +1,33 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_verification_confirm_response.freezed.dart';
part 'oauth_verification_confirm_response.g.dart';
@freezed
abstract class OAuthVerificationConfirmResponse with _$OAuthVerificationConfirmResponse {
const factory OAuthVerificationConfirmResponse({
required bool success,
required String message,
required String token,
required Map<String, dynamic> user,
@JsonKey(name: 'is_new_user') required bool isNewUser,
}) = _OAuthVerificationConfirmResponse;
factory OAuthVerificationConfirmResponse.fromJson(Map<String, dynamic> json) => _$OAuthVerificationConfirmResponseFromJson(json);
}
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_verification_confirm_response.freezed.dart';
part 'oauth_verification_confirm_response.g.dart';
@freezed
abstract class OAuthVerificationConfirmResponse with _$OAuthVerificationConfirmResponse {
const factory OAuthVerificationConfirmResponse({
required bool success,
required String message,
required String token,
required Map<String, dynamic> user,
@JsonKey(name: 'is_new_user') required bool isNewUser,
}) = _OAuthVerificationConfirmResponse;
factory OAuthVerificationConfirmResponse.fromJson(Map<String, dynamic> json) => _$OAuthVerificationConfirmResponseFromJson(json);
}

View File

@@ -1,13 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_verification_request.freezed.dart';
part 'oauth_verification_request.g.dart';
@freezed
abstract class OAuthVerificationRequest with _$OAuthVerificationRequest {
const factory OAuthVerificationRequest({String? email, String? phone}) = _OAuthVerificationRequest;
factory OAuthVerificationRequest.fromJson(Map<String, dynamic> json) => _$OAuthVerificationRequestFromJson(json);
}

View File

@@ -1,13 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_verification_request_response.freezed.dart';
part 'oauth_verification_request_response.g.dart';
@freezed
abstract class OAuthVerificationRequestResponse with _$OAuthVerificationRequestResponse {
const factory OAuthVerificationRequestResponse({required String email, required String status}) = _OAuthVerificationRequestResponse;
factory OAuthVerificationRequestResponse.fromJson(Map<String, dynamic> json) => _$OAuthVerificationRequestResponseFromJson(json);
}
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_verification_request_response.freezed.dart';
part 'oauth_verification_request_response.g.dart';
@freezed
abstract class OAuthVerificationRequestResponse with _$OAuthVerificationRequestResponse {
const factory OAuthVerificationRequestResponse({required String email, required String status}) = _OAuthVerificationRequestResponse;
factory OAuthVerificationRequestResponse.fromJson(Map<String, dynamic> json) => _$OAuthVerificationRequestResponseFromJson(json);
}

View File

@@ -1,15 +1,26 @@
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';
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;
final ProfileRepository profileRepository;
final FirebaseApi firebaseApi;
final Prefs prefs;
AuthCubit({required this.authRepository}) : super(const AuthState());
AuthCubit({required this.authRepository, required this.prefs, required this.profileRepository, required this.firebaseApi}) : super(const AuthState());
Future<bool> getTokenByCredentials(String email, String password) async {
emit(const AuthState.loading());
@@ -25,4 +36,32 @@ class AuthCubit extends Cubit<AuthState> {
return false;
}
}
Future<void> initializeUser() async {
await registerAnonymousPushToken();
if (await TokenStorage.haveTokenInStorage()) {
final profile = await profileRepository.getMyProfile();
if (profile.emailVerifiedAt != null) {
await registerPushToken();
AppRouter.navigatorKey.currentContext?.go(RouteConstants.feedScreen);
return;
}
AppRouter.navigatorKey.currentContext?.go(RouteConstants.verifyEmailScreen);
} else {
await registerAnonymousPushToken();
FlutterNativeSplash.remove();
}
}
Future<void> registerAnonymousPushToken() async {
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
if (!hasAnonymousTokenBeenSent) {
await firebaseApi.registerAnonymousPushToken();
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
}
}
Future<void> registerPushToken() async {
await firebaseApi.registerPushToken();
}
}

View File

@@ -126,8 +126,10 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
try {
emit(const OAuthState.loading());
log('OAuthCubit: Requesting verification code for email: $email, phone: $phone');
final response = await authRepository.requestOAuthVerification(email: email, phone: phone);
log('OAuthCubit: Verification code requested successfully, masked email: ${response.email}');
final response = await authRepository.requestOAuthVerification(email: null, phone: phone);
if (response != null) {
log('OAuthCubit: Verification code requested successfully, masked email: ${response.email}');
}
// Сбрасываем состояние после успешного запроса кода
emit(const OAuthState());
return response;

View File

@@ -28,7 +28,6 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -232,9 +231,8 @@ class _AuthScreenState extends State<AuthScreen> {
final bool useMobileLayout = shortestSide < 600;
if (!useMobileLayout) isFocused = true;
final s = S.of(context);
FlutterNativeSplash.remove();
return BlocProvider<AuthCubit>(
create: (BuildContext context) => authCubit,
create: (BuildContext context) => authCubit..initializeUser(),
child: BlocBuilder<AuthCubit, AuthState>(
builder: (BuildContext context, AuthState state) {
return Scaffold(

View File

@@ -338,7 +338,13 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
children: [
const Text('Вес'),
const SizedBox(height: 4),
CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}, hasError: weightError),
CustomTextFormField(
controller: weightController,
hintText: 'Введите вес',
keyboardType: TextInputType.number,
onChanged: (newValue) {},
hasError: weightError,
),
if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')],
],
),
@@ -351,7 +357,13 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
children: [
const Text('Рост'),
const SizedBox(height: 4),
CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}, hasError: heightError),
CustomTextFormField(
controller: heightController,
hintText: 'Введите рост',
keyboardType: TextInputType.number,
onChanged: (newValue) {},
hasError: heightError,
),
if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')],
],
),
@@ -611,10 +623,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
// Показываем диалог для ввода кода
final displayText = response.email;
_showVerificationCodeDialog(context, type, displayText);
} else {
if (context.mounted) {
showErrorToast('Не удалось отправить код верификации');
}
} else if ((emailController.text.isEmpty || phoneController.text.isEmpty) && context.mounted) {
showErrorToast('Пожалуйста, заполните все поля');
} else if (context.mounted) {
showErrorToast('Не удалось отправить код верификации');
}
} catch (e) {
if (context.mounted) {

View File

@@ -4,7 +4,6 @@ import 'package:eatvio_app/features/auth/models/oauth_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_request.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
@@ -78,11 +77,18 @@ class AuthRepository {
}
}
Future<OAuthVerificationRequestResponse> requestOAuthVerification({String? email, String? phone}) async {
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
try {
return await authService.requestOAuthVerification(
body: OAuthVerificationRequest(email: email, phone: phone),
);
Map<String, dynamic>? body;
if (email != null) {
body = {'email': email};
} else if (phone != null) {
body = {'phone': phone};
}
if (body != null) {
return await authService.requestOAuthVerification(body: body);
}
return null;
} catch (e) {
rethrow;
}

View File

@@ -4,7 +4,6 @@ import 'package:eatvio_app/features/auth/models/oauth_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_request.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:retrofit/retrofit.dart';
@@ -25,7 +24,7 @@ abstract class AuthService {
Future<TokenModel> signInViaSocialMedia({@Body() required OAuthLoginRequest body});
@POST('/api/oauth/verification/request')
Future<OAuthVerificationRequestResponse> requestOAuthVerification({@Body() required OAuthVerificationRequest body});
Future<OAuthVerificationRequestResponse> requestOAuthVerification({@Body() required Map<String, dynamic> body});
@POST('/api/oauth/verification/confirm')
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({@Body() required OAuthVerificationConfirmRequest body});

View File

@@ -0,0 +1,29 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'barcode_product_model.freezed.dart';
part 'barcode_product_model.g.dart';
@freezed
abstract class BarcodeProductModel with _$BarcodeProductModel {
const factory BarcodeProductModel({
// ignore: non_constant_identifier_names
required String GTIN,
@JsonKey(name: 'Товарный знак (ru)') String? brandName,
@JsonKey(name: 'Описание (ru)') String? description,
@JsonKey(name: 'Количество/вес') String? quantity,
@JsonKey(name: 'Рынок сбыта') String? market,
@JsonKey(name: 'Калорийность') double? calories,
@JsonKey(name: 'Белки') double? proteins,
@JsonKey(name: 'Жиры') double? fats,
@JsonKey(name: 'Углеводы') double? carbohydrates,
}) = _BarcodeProductModel;
factory BarcodeProductModel.fromJson(Map<String, Object?> json) => _$BarcodeProductModelFromJson(json);
}
@freezed
abstract class BarcodeTaskStatusModel with _$BarcodeTaskStatusModel {
const factory BarcodeTaskStatusModel({String? status, @JsonKey(name: 'task_id') String? taskId, BarcodeProductModel? data}) = _BarcodeTaskStatusModel;
factory BarcodeTaskStatusModel.fromJson(Map<String, Object?> json) => _$BarcodeTaskStatusModelFromJson(json);
}

View File

@@ -0,0 +1,108 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
class BarcodeRepository {
final BarcodeService barcodeService;
final Dio dio;
BarcodeRepository({
required this.barcodeService,
required this.dio,
});
/// Получить информацию о продукте по штрихкоду
/// Возвращает либо данные продукта, либо статус задачи обработки
Future<BarcodeResult> getProductByBarcode(String barcode) async {
try {
final response = await dio.get('/api/gsone/barcode/$barcode');
if (response.statusCode == 200) {
// Данные найдены в БД
final product = BarcodeProductModel.fromJson(response.data as Map<String, dynamic>);
return BarcodeResult.success(product);
} else if (response.statusCode == 202) {
// Запущена задача обработки
final taskStatus = BarcodeTaskStatusModel.fromJson(response.data as Map<String, dynamic>);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
throw Exception('Unexpected status code: ${response.statusCode}');
} on DioException catch (e) {
if (e.response?.statusCode == 202) {
// Если получен статус 202, значит запущена задача обработки
final taskStatus = BarcodeTaskStatusModel.fromJson(e.response!.data as Map<String, dynamic>);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
rethrow;
} catch (e) {
rethrow;
}
}
/// Получить статус задачи обработки
Future<BarcodeTaskStatus> getTaskStatus(String taskId) async {
try {
final statusModel = await barcodeService.getTaskStatus(taskId: taskId);
if (statusModel.status == 'completed' && statusModel.data != null) {
return BarcodeTaskStatus.completed(statusModel.data!);
} else if (statusModel.status == 'failed') {
return BarcodeTaskStatus.failed();
} else {
return BarcodeTaskStatus.processing();
}
} catch (e) {
rethrow;
}
}
}
/// Результат запроса информации о продукте по штрихкоду
class BarcodeResult {
final BarcodeResultType type;
final BarcodeProductModel? product;
final String? taskId;
BarcodeResult._({required this.type, this.product, this.taskId});
factory BarcodeResult.success(BarcodeProductModel product) {
return BarcodeResult._(type: BarcodeResultType.success, product: product);
}
factory BarcodeResult.processing(String taskId) {
return BarcodeResult._(type: BarcodeResultType.processing, taskId: taskId);
}
}
enum BarcodeResultType {
success,
processing,
}
/// Статус задачи обработки
class BarcodeTaskStatus {
final BarcodeTaskStatusType type;
final BarcodeProductModel? product;
BarcodeTaskStatus._({required this.type, this.product});
factory BarcodeTaskStatus.completed(BarcodeProductModel product) {
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.completed, product: product);
}
factory BarcodeTaskStatus.processing() {
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.processing);
}
factory BarcodeTaskStatus.failed() {
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.failed);
}
}
enum BarcodeTaskStatusType {
completed,
processing,
failed,
}

View File

@@ -0,0 +1,21 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
import 'package:retrofit/retrofit.dart';
part 'barcode_service.g.dart';
@RestApi()
abstract class BarcodeService {
factory BarcodeService(Dio dio, {String baseUrl}) = _BarcodeService;
@GET('/api/gsone/barcode/{barcode}')
Future<BarcodeProductModel> getProductByBarcode({@Path('barcode') required String barcode});
@GET('/api/gsone/barcode/status/{task_id}')
Future<BarcodeTaskStatusModel> getTaskStatus({@Path('task_id') required String taskId});
}

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
@@ -79,7 +78,10 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
return Scaffold(
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
padding: EdgeInsets.only(
top: 12,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
@@ -195,7 +197,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 80),
padding: EdgeInsets.only(bottom: 160 + MediaQuery.of(context).padding.bottom), // поле комментария (52-200) + padding (12+12+56) + safe area
physics: const BouncingScrollPhysics(),
child: Column(
children: [

View File

@@ -4,17 +4,36 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_message_model.freezed.dart';
part 'chat_message_model.g.dart';
int _parseAuthorId(dynamic json) {
if (json == null) return 0;
if (json is num) return json.toInt();
return 0;
}
DateTime _parseUpdatedAt(dynamic json) {
if (json == null) return DateTime.now();
if (json is String) return parseDate(json);
return DateTime.now();
}
String _parseData(dynamic json) {
if (json == null) return '';
return json.toString();
}
@freezed
abstract class ChatMessageModel with _$ChatMessageModel {
const factory ChatMessageModel({
required int id,
required String data,
@JsonKey(name: 'data', fromJson: _parseData) required String data,
@JsonKey(name: 'media_data') required List<dynamic>? mediaData,
@JsonKey(name: 'author_id') required int authorId,
@JsonKey(name: 'updated_at', fromJson: parseDate) required DateTime updatedAt,
@JsonKey(name: 'urls_media_info') List<Map<String, dynamic>>? urlsMediaInfo,
@JsonKey(name: 'author_id', fromJson: _parseAuthorId) required int authorId,
@JsonKey(name: 'updated_at', fromJson: _parseUpdatedAt) required DateTime updatedAt,
@JsonKey(name: 'read_by_to_user', defaultValue: false) required bool isRead,
@JsonKey(name: 'is_author_message_auth_user') required bool? isCurrentUserAuthor,
@JsonKey(name: 'room_id') required String? roomId,
@JsonKey(name: 'type') String? type,
@Default(MessageState.sent) @JsonKey(includeToJson: false, includeFromJson: false) MessageState messageState,
@Default(null) @JsonKey(includeToJson: false, includeFromJson: false) String? uuid,
}) = _ChatMessageModel;

View File

@@ -164,9 +164,15 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
messagesSubscription = wsManager.directChatStream?.listen((event) {
AudioPlayer().play((AssetSource('audio/recive_message.wav')));
final message = ChatMessageModel.fromJson(jsonDecode(event.data));
List<ChatMessageModel> messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages];
final currentState = state as ChatRoomStateLoaded;
// Сообщения от текущего пользователя не приходят через WebSocket,
// они обрабатываются через ответ API в sendMessage
// Здесь обрабатываем только входящие сообщения от других пользователей
List<ChatMessageModel> messagesList = [message, ...currentState.chatMessages];
chatRepository.emitNewMessageToChats(message);
emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading));
emit(currentState.copyWith(chatMessages: messagesList, isLoading: isLoading));
EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl<ChatsCubit>().getChatRooms());
});
@@ -185,6 +191,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
final messageUuid = uuid.v4();
final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null;
AudioPlayer().play((AssetSource('audio/send_message.wav')));
// Создаем временное сообщение с состоянием loading
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: [
@@ -206,12 +214,25 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
);
try {
await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
// Отправляем сообщение и получаем ответ от сервера с правильными данными
final serverMessage = await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
// Заменяем локальное сообщение данными с сервера (с правильными URL файлов)
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == messageUuid) {
return elem.copyWith(messageState: MessageState.sent);
// Сохраняем UUID, состояние и обновляем данные из ответа сервера
// Используем значения из локального сообщения для полей, которые могут отсутствовать в ответе
return serverMessage.copyWith(
uuid: messageUuid,
messageState: MessageState.sent,
isCurrentUserAuthor: true,
isRead: elem.isRead,
roomId: elem.roomId ?? serverMessage.roomId,
authorId: serverMessage.authorId,
updatedAt: serverMessage.updatedAt,
);
}
return elem;
}).toList(),
@@ -320,6 +341,18 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (message != null) {
try {
AudioPlayer().play((AssetSource('audio/send_message.wav')));
// Определяем, есть ли файлы для повторной отправки
List<File>? filesToResend;
if (message.mediaData != null && message.mediaData!.isNotEmpty) {
// Если mediaData содержит File объекты, используем их
// Если это строки (URL), то файлы уже на сервере и повторная отправка не нужна
final firstItem = message.mediaData!.first;
if (firstItem is File) {
filesToResend = message.mediaData!.cast<File>();
}
}
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: [
@@ -341,12 +374,27 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
);
try {
await chatRepository.sendMessage(data: message.data, files: message.mediaData as List<File>?, slug: userSlug);
// Отправляем сообщение и получаем ответ от сервера
final serverMessage = await chatRepository.sendMessage(
data: message.data,
files: filesToResend,
slug: userSlug,
);
// Заменяем локальное сообщение данными с сервера
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == uuid) {
return elem.copyWith(messageState: MessageState.sent);
return serverMessage.copyWith(
uuid: uuid,
messageState: MessageState.sent,
isCurrentUserAuthor: true,
isRead: elem.isRead,
roomId: elem.roomId ?? serverMessage.roomId,
authorId: serverMessage.authorId,
updatedAt: serverMessage.updatedAt,
);
}
return elem;
}).toList(),

View File

@@ -12,6 +12,7 @@ import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cub
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/plan_order_message_widget.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
@@ -221,14 +222,36 @@ class _ChatScreenState extends State<ChatScreen> {
state.chatMessages[index].updatedAt.day != state.chatMessages[index + 1].updatedAt.day) ||
index + 1 == state.chatMessages.length) {
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
final message = state.chatMessages[index];
final isPlanOrderMessage = message.type == 'plan_order';
PacketOptions? messageOption;
if (isPlanOrderMessage && chosenPlan != null) {
if (chosenPlan.optionType == 'options1') {
messageOption = chosenPlan.plan.packet1Options;
} else if (chosenPlan.optionType == 'options2') {
messageOption = chosenPlan.plan.packet2Options;
} else if (chosenPlan.optionType == 'options3') {
messageOption = chosenPlan.plan.packet3Options;
}
}
return Column(
children: [
_messageDate(state.chatMessages[index].updatedAt),
_messageDate(message.updatedAt),
if (isPlanOrderMessage && chosenPlan != null && messageOption != null)
PlanOrderMessageWidget(
planName: chosenPlan.plan.name,
planSlug: chosenPlan.plan.slug,
price: messageOption.price,
sendTime: message.updatedAt,
isCurrentUser: isCurrentUserAuthor,
),
VisibilityDetector(
key: Key(state.chatMessages[index].id.toString()),
key: Key(message.id.toString()),
onVisibilityChanged: (VisibilityInfo info) {
if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
unreadMessagesIdsCopy.add((id: state.chatMessages[index].id, index: index));
if (!message.isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
unreadMessagesIdsCopy.add((id: message.id, index: index));
EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async {
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIdsCopy);
if (!result) return;
@@ -242,44 +265,76 @@ class _ChatScreenState extends State<ChatScreen> {
}
},
child: ChatBubbleWidget(
isRead: state.chatMessages[index].isRead,
sendTime: state.chatMessages[index].updatedAt,
text: state.chatMessages[index].data.trim(),
isRead: message.isRead,
sendTime: message.updatedAt,
text: message.data.trim(),
isCurrentUser: isCurrentUserAuthor,
mediaData: state.chatMessages[index].mediaData ?? [],
messageState: state.chatMessages[index].messageState,
messageUuid: state.chatMessages[index].uuid ?? '',
mediaData: message.mediaData ?? [],
messageState: message.messageState,
messageUuid: message.uuid ?? '',
userSlug: state.chatRoom.users.receiver.slug,
messageType: message.type,
isConsultant: amIConsultant,
urlsMediaInfo: message.urlsMediaInfo,
),
),
],
);
}
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
return VisibilityDetector(
key: Key(state.chatMessages[index].id.toString()),
onVisibilityChanged: (VisibilityInfo info) {
if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
unreadMessagesIds.add((id: state.chatMessages[index].id, index: index));
EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async {
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds);
if (!result) return;
if (!context.mounted) return;
context.read<ChatsCubit>().reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length);
unreadMessagesIds.clear();
});
}
},
child: ChatBubbleWidget(
isRead: state.chatMessages[index].isRead,
sendTime: state.chatMessages[index].updatedAt,
text: state.chatMessages[index].data.trim(),
isCurrentUser: isCurrentUserAuthor,
mediaData: state.chatMessages[index].mediaData ?? [],
messageState: state.chatMessages[index].messageState,
messageUuid: state.chatMessages[index].uuid ?? '',
userSlug: state.chatRoom.users.receiver.slug,
),
final message = state.chatMessages[index];
final isPlanOrderMessage = message.type == 'plan_order';
PacketOptions? messageOption;
if (isPlanOrderMessage && chosenPlan != null) {
if (chosenPlan.optionType == 'options1') {
messageOption = chosenPlan.plan.packet1Options;
} else if (chosenPlan.optionType == 'options2') {
messageOption = chosenPlan.plan.packet2Options;
} else if (chosenPlan.optionType == 'options3') {
messageOption = chosenPlan.plan.packet3Options;
}
}
return Column(
children: [
if (isPlanOrderMessage && chosenPlan != null && messageOption != null)
PlanOrderMessageWidget(
planName: chosenPlan.plan.name,
planSlug: chosenPlan.plan.slug,
price: messageOption.price,
sendTime: message.updatedAt,
isCurrentUser: isCurrentUserAuthor,
),
VisibilityDetector(
key: Key(message.id.toString()),
onVisibilityChanged: (VisibilityInfo info) {
if (!message.isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
unreadMessagesIds.add((id: message.id, index: index));
EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async {
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds);
if (!result) return;
if (!context.mounted) return;
context.read<ChatsCubit>().reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length);
unreadMessagesIds.clear();
});
}
},
child: ChatBubbleWidget(
isRead: message.isRead,
sendTime: message.updatedAt,
text: message.data.trim(),
isCurrentUser: isCurrentUserAuthor,
mediaData: message.mediaData ?? [],
messageState: message.messageState,
messageUuid: message.uuid ?? '',
userSlug: state.chatRoom.users.receiver.slug,
messageType: message.type,
isConsultant: amIConsultant,
urlsMediaInfo: message.urlsMediaInfo,
),
),
],
);
},
itemCount: state.chatMessages.length + (state.isLoading ? 1 : 0),
@@ -407,34 +462,30 @@ class _ChatScreenState extends State<ChatScreen> {
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 40,
child: TextFormField(
expands: true,
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
maxLines: 3,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),

View File

@@ -79,34 +79,30 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 40,
child: TextFormField(
expands: true,
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
maxLines: 3,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/message_action_bottom_sheet.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
@@ -24,7 +25,10 @@ class ChatBubbleWidget extends StatelessWidget {
required this.isRead,
required this.messageState,
required this.messageUuid,
required this.userSlug
required this.userSlug,
this.messageType,
this.isConsultant,
this.urlsMediaInfo,
});
final DateTime sendTime;
@@ -35,6 +39,9 @@ class ChatBubbleWidget extends StatelessWidget {
final MessageState messageState;
final String messageUuid;
final String userSlug;
final String? messageType;
final bool? isConsultant;
final List<Map<String, dynamic>>? urlsMediaInfo;
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
@@ -102,21 +109,104 @@ class ChatBubbleWidget extends StatelessWidget {
slivers: [
SliverList.separated(
itemBuilder: (BuildContext context, int index) {
if (!mediaData[index].toString().contains('.png') &&
!mediaData[index].toString().contains('.jpg') &&
!mediaData[index].toString().contains('.jpeg')) {
final mediaItem = mediaData[index];
final isImage = mediaItem is File
? (mediaItem.path.toLowerCase().endsWith('.png') ||
mediaItem.path.toLowerCase().endsWith('.jpg') ||
mediaItem.path.toLowerCase().endsWith('.jpeg') ||
mediaItem.path.toLowerCase().endsWith('.gif') ||
mediaItem.path.toLowerCase().endsWith('.webp'))
: (mediaItem.toString().toLowerCase().contains('.png') ||
mediaItem.toString().toLowerCase().contains('.jpg') ||
mediaItem.toString().toLowerCase().contains('.jpeg') ||
mediaItem.toString().toLowerCase().contains('.gif') ||
mediaItem.toString().toLowerCase().contains('.webp'));
if (!isImage) {
// Получаем имя файла
String fileName;
if (mediaItem is File) {
fileName = mediaItem.path.split('/').last;
} else {
final urlString = mediaItem.toString();
// Получаем расширение файла из URL
String? fileExtension;
final urlParts = urlString.split('/');
if (urlParts.isNotEmpty) {
final lastPart = urlParts.last.split('?').first;
final dotIndex = lastPart.lastIndexOf('.');
if (dotIndex != -1 && dotIndex < lastPart.length - 1) {
fileExtension = lastPart.substring(dotIndex);
}
}
// Пытаемся найти имя файла в urls_media_info
String? fileNameFromInfo;
final mediaInfo = urlsMediaInfo;
if (mediaInfo != null && mediaInfo.isNotEmpty) {
for (final mediaInfoMap in mediaInfo) {
// Каждый элемент urls_media_info - это Map, где ключ - это URL
if (mediaInfoMap.isNotEmpty) {
for (final entry in mediaInfoMap.entries) {
final infoUrl = entry.key.toString();
// Сравниваем полные URL или только имя файла
if (infoUrl == urlString ||
urlString.contains(infoUrl) ||
infoUrl.contains(urlString) ||
urlString.split('/').last == infoUrl.split('/').last) {
final fileInfo = entry.value;
if (fileInfo is Map<String, dynamic> && fileInfo.containsKey('name')) {
final nameValue = fileInfo['name'];
if (nameValue != null) {
fileNameFromInfo = nameValue.toString();
break;
}
}
}
}
if (fileNameFromInfo != null && fileNameFromInfo.isNotEmpty) break;
}
}
}
if (fileNameFromInfo != null && fileNameFromInfo.isNotEmpty) {
// Добавляем расширение к названию файла, если его там нет
if (fileExtension != null && !fileNameFromInfo.endsWith(fileExtension)) {
fileName = '$fileNameFromInfo$fileExtension';
} else {
fileName = fileNameFromInfo;
}
} else {
// Fallback к старой логике, если информации нет
fileName = urlString.contains('/') ? urlString.split('/').last.split('?').first : urlString.replaceAll('chat_files/', '');
}
}
return ListTile(
contentPadding: const EdgeInsets.all(4),
horizontalTitleGap: 8,
onTap: () async {
if (!await launchUrl(Uri.parse('${mediaData[index]}'))) {
throw Exception('Could not launch ${mediaData[index].toString()}');
if (mediaItem is File) {
// Для локальных файлов используем file:// URI
final fileUri = Uri.file(mediaItem.path);
if (await canLaunchUrl(fileUri)) {
await launchUrl(fileUri, mode: LaunchMode.externalApplication);
}
} else {
// Для URL файлов используем обычный launchUrl
final urlString = mediaItem.toString();
final uri = urlString.startsWith('http://') || urlString.startsWith('https://')
? Uri.parse(urlString)
: Uri.parse('https://$urlString');
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
},
leading: const Icon(Icons.file_copy_outlined, color: Colors.black),
trailing: const Icon(Icons.download_outlined, color: Colors.black),
title: Text(
mediaData[index].toString().replaceAll('chat_files/', ''),
fileName,
maxLines: 2,
style: const TextStyle(color: Colors.black, fontSize: 12, overflow: TextOverflow.ellipsis),
),
@@ -124,16 +214,16 @@ class ChatBubbleWidget extends StatelessWidget {
}
return GestureDetector(
onTap: () {
context.pushTransparentRoute(
PhotoViewScreen(imageUrls: mediaData, index: index, isLocal: mediaData[index] is File ? true : false),
);
// Определяем, все ли изображения локальные
final allLocal = mediaData.every((item) => item is File);
context.pushTransparentRoute(PhotoViewScreen(imageUrls: mediaData, index: index, isLocal: allLocal));
},
child: Hero(
tag: '${mediaData[index]}',
transitionOnUserGestures: true,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: mediaData[index] is File ? Image.file(mediaData[index]) : CachedNetworkImage(imageUrl: '${mediaData[index]}'),
child: mediaItem is File ? Image.file(mediaItem) : CachedNetworkImage(imageUrl: '$mediaItem'),
),
),
);
@@ -144,8 +234,8 @@ class ChatBubbleWidget extends StatelessWidget {
],
),
),
if (mediaData.isNotEmpty && text.isNotEmpty) const SizedBox(height: 8),
if (text.isNotEmpty) Text(text, style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16)),
if (mediaData.isNotEmpty && _getDisplayText().isNotEmpty) const SizedBox(height: 8),
if (_getDisplayText().isNotEmpty) _buildLinkableText(_getDisplayText()),
],
),
),
@@ -163,4 +253,77 @@ class ChatBubbleWidget extends StatelessWidget {
),
);
}
String _getDisplayText() {
if (messageType == 'plan_order') {
if (isConsultant == true) {
// Текст для консультанта
return '1. Уточните у подопечного его цели и пожелания\n'
'2. Согласуйте и выберите дату старта плана\n'
'3. Ожидайте уведомления об успешной оплате\n'
'4. После оплаты предоставьте необходимые данные, согласно вашему плану питания';
} else {
// Текст для пользователя
return 'Укажите вашу цель в работе с консультантом\n'
'Согласуйте удобную дату старта плана, после чего вам будет направлена ссылка на оплату.';
}
}
return text;
}
Widget _buildLinkableText(String text) {
final urlRegex = RegExp(
r'(?:(?:https?|ftp):\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)',
caseSensitive: false,
);
final textStyle = TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16);
final linkStyle = textStyle.copyWith(color: appTheme.blueA400, decoration: TextDecoration.underline);
final matches = urlRegex.allMatches(text);
if (matches.isEmpty) {
return Text(text, style: textStyle);
}
final spans = <TextSpan>[];
int lastEnd = 0;
for (final match in matches) {
// Добавляем текст до ссылки
if (match.start > lastEnd) {
spans.add(TextSpan(text: text.substring(lastEnd, match.start), style: textStyle));
}
// Добавляем кликабельную ссылку
final url = match.group(0)!;
final fullUrl = url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://') ? url : 'https://$url';
spans.add(
TextSpan(
text: url,
style: linkStyle,
recognizer: TapGestureRecognizer()
..onTap = () async {
try {
final uri = Uri.parse(fullUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} catch (e) {
// Игнорируем ошибки открытия ссылок
}
},
),
);
lastEnd = match.end;
}
// Добавляем оставшийся текст после последней ссылки
if (lastEnd < text.length) {
spans.add(TextSpan(text: text.substring(lastEnd), style: textStyle));
}
return RichText(text: TextSpan(children: spans));
}
}

View File

@@ -0,0 +1,127 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class PlanOrderMessageWidget extends StatelessWidget {
const PlanOrderMessageWidget({
super.key,
required this.planName,
required this.planSlug,
required this.price,
required this.sendTime,
required this.isCurrentUser,
});
final String planName;
final String planSlug;
final dynamic price;
final DateTime sendTime;
final bool isCurrentUser;
String _formatPrice(dynamic price) {
if (price == null) return '0₽';
if (price is num) {
return '${price.toStringAsFixed(0)}₽';
}
if (price is String) {
final numPrice = num.tryParse(price);
if (numPrice != null) {
return '${numPrice.toStringAsFixed(0)}₽';
}
return price.endsWith('₽') ? price : '$price₽';
}
return '0₽';
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final formattedPrice = _formatPrice(price);
final textStyle = TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16);
final linkStyle = textStyle.copyWith(
color: appTheme.blueA400,
decoration: TextDecoration.underline,
);
return SizedBox(
width: size.width,
child: Padding(
padding: EdgeInsets.fromLTRB(isCurrentUser ? 64 : 16, 10, isCurrentUser ? 16 : 64, 4),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: isCurrentUser ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
if (isCurrentUser)
Padding(
padding: const EdgeInsets.only(right: 4, bottom: 4),
child: Text(
sendTime.format('HH:mm', 'ru'),
style: const TextStyle(color: Color(0xFF7B8794), height: 1, fontSize: 10),
),
),
Container(
constraints: BoxConstraints(maxWidth: size.width * 0.7),
decoration: BoxDecoration(
color: isCurrentUser ? appTheme.gray50 : appTheme.gray60004,
borderRadius: BorderRadius.horizontal(
left: isCurrentUser ? const Radius.circular(32) : const Radius.circular(16),
right: isCurrentUser ? const Radius.circular(16) : const Radius.circular(24),
),
),
child: Padding(
padding: EdgeInsets.only(
top: 8,
bottom: 8,
left: isCurrentUser ? 16 : 8,
right: isCurrentUser ? 8 : 16,
),
child: RichText(
text: TextSpan(
style: textStyle,
children: [
const TextSpan(text: 'Я хочу заказать '),
TextSpan(
text: planName,
style: linkStyle,
recognizer: TapGestureRecognizer()
..onTap = () {
GoRouter.of(context).pushNamed(
'aboutPlanScreen',
extra: {
'slug': planSlug,
'index': null,
'navigateToComments': false,
},
);
},
),
TextSpan(text: ' по цене $formattedPrice'),
],
),
),
),
),
if (!isCurrentUser)
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 4),
child: Text(
sendTime.format('HH:mm', 'ru'),
style: const TextStyle(
color: Color(0xFF7B8794),
height: 1,
fontSize: 10,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
);
}
}

View File

@@ -28,7 +28,7 @@ class ChatRepository {
_newUserMessagesController.add(message);
}
Future<void> sendMessage({
Future<ChatMessageModel> sendMessage({
required String slug,
String? data,
List<File>? files,
@@ -48,9 +48,13 @@ class ChatRepository {
}
}
await chatService.sendMessage(
// Если data пустая строка или null, не передаем её вообще
// Сервер требует либо непустую строку, либо отсутствие поля
final dataToSend = (data == null || data.trim().isEmpty) ? null : data;
return await chatService.sendMessage(
slug: slug,
data: data,
data: dataToSend,
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
socketId: (await SocketIdStorage.getSocketIdFromStorage())!,
);

View File

@@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
import 'package:eatvio_app/features/chat/models/chat_room_model.dart';
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
@@ -26,7 +27,7 @@ abstract class ChatService {
@POST('/api/v2/chat/message/send/{slug}')
@MultiPart()
Future<void> sendMessage({
Future<ChatMessageModel> sendMessage({
@Header('X-Socket-ID') required String socketId,
@Path() required String slug,
@Part() String? data,

View File

@@ -10,8 +10,12 @@ import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/dev_tools/test_cases.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/features/diary/presentation/pages/barcode_scanner_screen.dart';
import 'package:eatvio_app/features/barcode/repositories/barcode_repository.dart';
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:async';
class DevToolsScreen extends StatelessWidget {
const DevToolsScreen({super.key});
@@ -296,6 +300,20 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
}
},
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: 'Сканирование QR/штрихкода',
buttonStyle: const ButtonStyle(),
onPressed: () async {
if (!context.mounted) return;
final barcode = await Navigator.of(context).push<String>(
MaterialPageRoute(builder: (context) => const BarcodeScannerScreen()),
);
if (barcode != null && barcode.isNotEmpty && context.mounted) {
await _handleBarcodeScanned(barcode, context);
}
},
),
const SizedBox(height: 300),
],
),
@@ -303,6 +321,195 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
),
);
}
Future<void> _handleBarcodeScanned(String barcode, BuildContext context) async {
try {
final barcodeRepository = sl<BarcodeRepository>();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Row(
children: [
CircularProgressIndicator(),
SizedBox(width: 16),
Text('Загрузка данных...'),
],
),
),
);
final result = await barcodeRepository.getProductByBarcode(barcode);
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор загрузки
if (result.type == BarcodeResultType.success && result.product != null) {
// Данные получены сразу
_showProductDialog(context, result.product!, barcode);
} else if (result.type == BarcodeResultType.processing && result.taskId != null) {
// Запущена задача обработки, нужно опрашивать статус
await _pollTaskStatus(context, result.taskId!, barcode);
}
} catch (e) {
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор загрузки если открыт
showErrorToast('Ошибка: $e');
_showBarcodeDialog(context, barcode, null);
}
}
Future<void> _pollTaskStatus(BuildContext context, String taskId, String barcode) async {
final barcodeRepository = sl<BarcodeRepository>();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
const Text('Обработка штрихкода...'),
const SizedBox(height: 8),
Text('Это может занять некоторое время', style: TextStyle(fontSize: 12, color: Colors.grey[600])),
],
),
),
);
final completer = Completer<void>();
int attempts = 0;
const maxAttempts = 30; // Максимум 30 попыток (примерно 5 минут при интервале 10 секунд)
// ignore: unused_local_variable
final timer = Timer.periodic(const Duration(seconds: 10), (timer) async {
attempts++;
try {
final status = await barcodeRepository.getTaskStatus(taskId);
if (status.type == BarcodeTaskStatusType.completed && status.product != null) {
timer.cancel();
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор загрузки
_showProductDialog(context, status.product!, barcode);
completer.complete();
} else if (status.type == BarcodeTaskStatusType.failed) {
timer.cancel();
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор загрузки
showErrorToast('Не удалось получить данные о продукте');
_showBarcodeDialog(context, barcode, null);
completer.complete();
} else if (attempts >= maxAttempts) {
timer.cancel();
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор загрузки
showErrorToast('Превышено время ожидания');
_showBarcodeDialog(context, barcode, null);
completer.complete();
}
} catch (e) {
timer.cancel();
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор загрузки
showErrorToast('Ошибка при проверке статуса: $e');
_showBarcodeDialog(context, barcode, null);
completer.complete();
}
});
await completer.future;
}
void _showProductDialog(BuildContext context, BarcodeProductModel product, String barcode) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Информация о продукте'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildInfoRow('Штрихкод:', barcode),
if (product.brandName != null) _buildInfoRow('Бренд:', product.brandName!),
if (product.description != null) _buildInfoRow('Описание:', product.description!),
if (product.quantity != null) _buildInfoRow('Количество:', product.quantity!),
if (product.market != null) _buildInfoRow('Рынок сбыта:', product.market!),
if (product.calories != null) _buildInfoRow('Калорийность:', '${product.calories} ккал'),
if (product.proteins != null) _buildInfoRow('Белки:', '${product.proteins} г'),
if (product.fats != null) _buildInfoRow('Жиры:', '${product.fats} г'),
if (product.carbohydrates != null) _buildInfoRow('Углеводы:', '${product.carbohydrates} г'),
],
),
),
actions: [
TextButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: barcode));
showInfoToast('Штрихкод скопирован в буфер обмена');
},
child: const Text('Скопировать код'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Закрыть'),
),
],
),
);
}
void _showBarcodeDialog(BuildContext context, String barcode, BarcodeProductModel? product) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Отсканированный код'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(barcode),
if (product == null) ...[
const SizedBox(height: 16),
const Text('Данные о продукте не найдены', style: TextStyle(color: Colors.grey)),
],
],
),
actions: [
TextButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: barcode));
Navigator.of(context).pop();
showInfoToast('Код скопирован в буфер обмена');
},
child: const Text('Скопировать'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Закрыть'),
),
],
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: RichText(
text: TextSpan(
style: const TextStyle(color: Colors.black, fontSize: 14),
children: [
TextSpan(text: '$label ', style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: value),
],
),
),
);
}
}
enum TestState { success, fail, notRun, loading }

View File

@@ -25,61 +25,63 @@ class _BarcodeScannerScreenState extends State<BarcodeScannerScreen> {
title: const Text('Сканирование штрихкода'),
centerTitle: true,
),
body: Column(
children: [
Expanded(
child: MobileScanner(
controller: _controller,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
if (barcodes.isEmpty) return;
final rawValue = barcodes.first.rawValue;
if (rawValue == null || rawValue == _detectedBarcode) return;
setState(() {
_detectedBarcode = rawValue;
});
},
body: SafeArea(
child: Column(
children: [
Expanded(
child: MobileScanner(
controller: _controller,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
if (barcodes.isEmpty) return;
final rawValue = barcodes.first.rawValue;
if (rawValue == null || rawValue == _detectedBarcode) return;
setState(() {
_detectedBarcode = rawValue;
});
},
),
),
),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0xFFF6F6F6),
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Найденный штрихкод',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Text(
_detectedBarcode ?? 'Наведите камеру на штрихкод',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: _detectedBarcode == null ? Colors.grey : Colors.black,
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0xFFF6F6F6),
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Найденный штрихкод',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
),
const SizedBox(height: 16),
if (_detectedBarcode != null)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop(_detectedBarcode);
},
child: const Text('Использовать штрихкод'),
const SizedBox(height: 8),
Text(
_detectedBarcode ?? 'Наведите камеру на штрихкод',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: _detectedBarcode == null ? Colors.grey : Colors.black,
),
),
],
const SizedBox(height: 16),
if (_detectedBarcode != null)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop(_detectedBarcode);
},
child: const Text('Использовать штрихкод'),
),
),
],
),
),
),
],
],
),
),
);
}

View File

@@ -5,11 +5,9 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/pages/barcode_scanner_screen.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class AddProductModal extends StatefulWidget {
final PortionModel? portion;
@@ -27,7 +25,7 @@ class _AddProductModalState extends State<AddProductModal> {
final quantityController = TextEditingController();
GlossaryModel? selectedGlossary;
List<GlossaryModel> glossariesList = [];
late final String? _previousProductName;
String? _previousProductName;
@override
void initState() {
super.initState();
@@ -64,25 +62,9 @@ class _AddProductModalState extends State<AddProductModal> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Добавление блюда',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
),
IconButton(
onPressed: () async {
final barcode = await Navigator.of(context).push<String>(MaterialPageRoute(builder: (context) => const BarcodeScannerScreen()));
if (barcode != null && barcode.isNotEmpty) {
productNameController.text = barcode;
setState(() {});
}
},
splashRadius: 20,
icon: const Icon(Symbols.barcode_scanner, color: Color(0xFF323F4B)),
),
],
const Text(
'Добавление блюда',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
),
const SizedBox(height: 16),
Row(

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
@@ -70,9 +68,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
void _scrollToBottom() {
_jumped = true;
scrollController.jumpTo(
scrollController.position.maxScrollExtent,
);
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
@override
@@ -97,11 +93,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
if (dietState is DietStateLoaded) {
if (_isFirstLaunch) {
context.read<FeedCubit>().viewDiet(dietState.diet.id);
context.read<CommentsCubit>().getComments(
dietState.diet.id.toString(),
CommentSourceType.diet,
true,
);
context.read<CommentsCubit>().getComments(dietState.diet.id.toString(), CommentSourceType.diet, true);
_isFirstLaunch = false;
}
Future.delayed(const Duration(milliseconds: 500), () {
@@ -113,14 +105,13 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
padding: EdgeInsets.only(
top: 12,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(
color: Colors.black.withValues(alpha: 0.1),
),
),
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -136,9 +127,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: dietState.isStarted ? appTheme.transparentRed : appTheme.greenA700,
onPressed: () {
final cubit = context.read<AboutDietCubit>();
@@ -146,10 +135,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
if (dietState.isStarted) {
showModalBottomSheet(
context: context,
builder: (context) => CancelDietBottomSheet(
aboutDietCubit: cubit,
dietName: dietState.diet.name,
),
builder: (context) => CancelDietBottomSheet(aboutDietCubit: cubit, dietName: dietState.diet.name),
);
} else if (dietState.hasStartedDiet) {
showModalBottomSheet(
@@ -162,9 +148,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
} else {
showModalBottomSheet(
context: context,
builder: (context) => StartDietBottomSheet(
aboutDietCubit: cubit,
),
builder: (context) => StartDietBottomSheet(aboutDietCubit: cubit),
);
}
},
@@ -209,17 +193,10 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
FocusManager.instance.primaryFocus?.unfocus();
},
child: Container(
decoration: BoxDecoration(
color: appTheme.blueA400,
shape: BoxShape.circle,
),
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(
Icons.close_rounded,
size: 18,
color: Colors.white,
),
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
@@ -235,22 +212,12 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.black,
height: 1.2,
),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false, // убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), // вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.2,
color: Color(0xFFAEAEB2),
),
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
@@ -312,14 +279,14 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
dietState.diet.name,
maxLines: 2,
@@ -342,7 +309,9 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
),
body: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.only(bottom: 160),
padding: EdgeInsets.only(
bottom: 220 + MediaQuery.of(context).padding.bottom,
), // кнопка (52) + отступ (8) + поле комментария (52-200) + padding (12+12+56) + safe area
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -371,10 +340,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
children: [
SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12),
const SizedBox(width: 8),
Text(
dietState.diet.totalViews.toString(),
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
Text(dietState.diet.totalViews.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
const SizedBox(width: 12),
@@ -392,10 +358,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
colorFilter: dietState.diet.wasLikedByMe ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null,
),
const SizedBox(width: 8),
Text(
dietState.diet.totalLikes.toString(),
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
Text(dietState.diet.totalLikes.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
),
@@ -405,10 +368,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
children: [
SvgPicture.asset('assets/icons/comment2.svg', width: 13, height: 12),
const SizedBox(width: 8),
Text(
dietState.diet.totalComments.toString(),
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
Text(dietState.diet.totalComments.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
],
@@ -419,26 +379,15 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
s.about_diet,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
child: Text(s.about_diet, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
InfoRow(
diet: dietState.diet,
type: ProductType.diet,
),
ExpandableText(
text: dietState.diet.details,
),
InfoRow(diet: dietState.diet, type: ProductType.diet),
ExpandableText(text: dietState.diet.details),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AuthorCard(
author: dietState.diet.author,
title: s.diet_author,
),
child: AuthorCard(author: dietState.diet.author, title: s.diet_author),
),
const SizedBox(height: 16),
],
@@ -451,9 +400,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
child: Center(
child: Text(
'${s.diet_plan}(${dietState.diet.duration.toString()} ${getDayForm(dietState.diet.duration, s)})',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
),
@@ -463,13 +410,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) {
return _dayOfPlan(
dietState.diet.dailySchedule[index + 1] ?? {},
index,
s,
dietState.selectedIndexes,
dietState.diet,
);
return _dayOfPlan(dietState.diet.dailySchedule[index + 1] ?? {}, index, s, dietState.selectedIndexes, dietState.diet);
},
itemCount: dietState.diet.dailySchedule.length,
),
@@ -482,9 +423,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
alignment: Alignment.centerLeft,
child: Text(
'${dietState.diet.totalComments} ${getCommentForm(dietState.diet.totalComments, s)}',
style: TextStyles.textMMedium.copyWith(
color: appTheme.gray500,
),
style: TextStyles.textMMedium.copyWith(color: appTheme.gray500),
),
),
const SizedBox(height: 16),
@@ -509,14 +448,14 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
'',
maxLines: 2,
@@ -569,18 +508,14 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
pressElevation: 0,
showCheckmark: false,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
label: Text((index + 1).toString()),
labelStyle: TextStyles.textXSSemibold.copyWith(
color: index == selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600,
),
labelStyle: TextStyles.textXSSemibold.copyWith(color: index == selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600),
selected: selectedIndexes[dayNumber] == index,
backgroundColor: appTheme.gray100,
onSelected: (bool selected) {
dietsCubit.setSelectedIndex(dayNumber: dayNumber, index: index);
dietsCubit.setSelectedIndex(dayNumber: dayNumber, index: index);
},
);
}
@@ -588,9 +523,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MainBackgroundBouble(
padding: const EdgeInsets.only(
top: 16,
),
padding: const EdgeInsets.only(top: 16),
child: Column(
children: [
Padding(
@@ -598,31 +531,12 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${s.day} ${dayNumber + 1}',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: CustomDivider(),
),
Text(
s.meal,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
),
),
Text('${s.day} ${dayNumber + 1}', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const Padding(padding: EdgeInsets.symmetric(vertical: 12), child: CustomDivider()),
Text(s.meal, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
choiceChipWidget(0),
choiceChipWidget(1),
choiceChipWidget(2),
choiceChipWidget(3),
choiceChipWidget(4),
],
children: [choiceChipWidget(0), choiceChipWidget(1), choiceChipWidget(2), choiceChipWidget(3), choiceChipWidget(4)],
),
const SizedBox(height: 8),
ListView.builder(
@@ -655,20 +569,10 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
children: [
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.6,
child: Text(
daily.name,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
),
),
child: Text(daily.name, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const Spacer(),
Text(
'${daily.quantity} ${getLocalizedMeasurementUnits(s, daily.portion)}',
style: TextStyles.textMSemibold.copyWith(
color: appTheme.black,
),
),
Text('${daily.quantity} ${getLocalizedMeasurementUnits(s, daily.portion)}', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
],
),
);

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
@@ -64,9 +62,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
void _scrollToBottom() {
_jumped = true;
scrollController.jumpTo(
scrollController.position.maxScrollExtent,
);
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
@override
@@ -75,17 +71,8 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
return PopScope(
child: MultiBlocProvider(
providers: [
BlocProvider<CommentsCubit>(
create: (context) => commentsCubit
..getComments(
widget.slug,
CommentSourceType.plan,
true,
),
),
BlocProvider<AboutPlanCubit>(
create: (context) => sl<AboutPlanCubit>()..getPlanWithLoading(widget.slug),
),
BlocProvider<CommentsCubit>(create: (context) => commentsCubit..getComments(widget.slug, CommentSourceType.plan, true)),
BlocProvider<AboutPlanCubit>(create: (context) => sl<AboutPlanCubit>()..getPlanWithLoading(widget.slug)),
],
child: BlocConsumer<CommentsCubit, CommentsState>(
listener: (BuildContext context, CommentsState commentsState) {
@@ -166,14 +153,13 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
padding: EdgeInsets.only(
top: 12,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(
color: Colors.black.withValues(alpha: 0.1),
),
),
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -189,9 +175,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
backgroundColor: appTheme.greenA700,
onPressed: () async {
final (chosenPlan, error) = await context.read<AboutPlanCubit>().chosePlan();
@@ -205,18 +189,9 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
return;
}
if (!context.mounted) return;
GoRouter.of(context).pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chosenPlan.chatId,
'sendInitialMessage': true,
},
);
GoRouter.of(context).pushReplacementNamed('chatScreen', extra: {'uuid': chosenPlan.chatId, 'sendInitialMessage': true});
},
label: Text(
s.to_order,
style: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
),
label: Text(s.to_order, style: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700)),
),
),
),
@@ -255,17 +230,10 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
FocusManager.instance.primaryFocus?.unfocus();
},
child: Container(
decoration: BoxDecoration(
color: appTheme.blueA400,
shape: BoxShape.circle,
),
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(
Icons.close_rounded,
size: 18,
color: Colors.white,
),
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
@@ -281,22 +249,12 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.black,
height: 1.2,
),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false, // убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), // вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.2,
color: Color(0xFFAEAEB2),
),
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
@@ -345,11 +303,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
),
height: 32,
width: 32,
child: Icon(
Icons.send_rounded,
size: 18,
color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black,
),
child: Icon(Icons.send_rounded, size: 18, color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black),
),
),
],
@@ -389,7 +343,9 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
),
body: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.only(bottom: 160),
padding: EdgeInsets.only(
bottom: 220 + MediaQuery.of(context).padding.bottom,
), // кнопка (52) + отступ (8) + поле комментария (52-200) + padding (12+12+56) + safe area
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -436,8 +392,9 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
'assets/icons/heart.svg',
width: 14,
height: 12,
colorFilter:
state.planModel?.wasLikedByMe == true ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null,
colorFilter: state.planModel?.wasLikedByMe == true
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
: null,
),
const SizedBox(width: 8),
Text(
@@ -467,24 +424,16 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
s.about_plan,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
child: Text(s.about_plan, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
InfoRow(plan: state.planModel, type: ProductType.plan),
ExpandableText(
text: state.planModel?.details ?? '',
),
ExpandableText(text: state.planModel?.details ?? ''),
if (state.planModel != null) ...[
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AuthorCard(
author: (state.planModel?.author)!,
title: s.plan_author,
),
child: AuthorCard(author: (state.planModel?.author)!, title: s.plan_author),
),
],
const SizedBox(height: 16),
@@ -503,22 +452,11 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
const SizedBox(height: 12),
],
switch (state.chosenOptionIndex) {
1 => OptionWidget(
option: state.planModel?.packet2Options,
duration: state.planModel?.duration ?? 0,
),
2 => OptionWidget(
option: state.planModel?.packet3Options,
duration: state.planModel?.duration ?? 0,
),
_ => OptionWidget(
option: state.planModel?.packet1Options,
duration: state.planModel?.duration ?? 0,
),
1 => OptionWidget(option: state.planModel?.packet2Options, duration: state.planModel?.duration ?? 0),
2 => OptionWidget(option: state.planModel?.packet3Options, duration: state.planModel?.duration ?? 0),
_ => OptionWidget(option: state.planModel?.packet1Options, duration: state.planModel?.duration ?? 0),
},
const SizedBox(
height: 16,
),
const SizedBox(height: 16),
MainBackgroundBouble(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
@@ -528,9 +466,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
alignment: Alignment.centerLeft,
child: Text(
'${state.planModel?.totalComments ?? 0} ${getCommentForm(state.planModel?.totalComments ?? 0, s)}',
style: TextStyles.textMMedium.copyWith(
color: appTheme.gray500,
),
style: TextStyles.textMMedium.copyWith(color: appTheme.gray500),
),
),
const SizedBox(height: 16),

View File

@@ -1,6 +1,4 @@
import 'dart:developer';
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
@@ -100,8 +98,14 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1)))),
padding: EdgeInsets.only(
top: 12,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
@@ -235,7 +239,9 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
),
body: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.only(bottom: 160),
padding: EdgeInsets.only(
bottom: 220 + MediaQuery.of(context).padding.bottom,
), // кнопка (52) + отступ (8) + поле комментария (52-200) + padding (12+12+56) + safe area
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -387,7 +393,11 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
if (state is CommentsStateLoading) const Loader(),
if (state is CommentsStateLoaded)
CommentsWidget(
id: recipeState.recipe.slug, commentSourceType: CommentSourceType.recipe, comment: state.comment, replyId: replyId),
id: recipeState.recipe.slug,
commentSourceType: CommentSourceType.recipe,
comment: state.comment,
replyId: replyId,
),
],
),
),

View File

@@ -105,7 +105,7 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
},
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: const Text("Подтвердите почту")),
appBar: AppBar(title: const Text("Подтвердите почту"), ),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(

View File

@@ -1,176 +1,178 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:eatvio_app/features/registration_v2/presentation/widgets/sex_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
class FirstStepContent extends StatefulWidget {
const FirstStepContent({super.key});
@override
State<FirstStepContent> createState() => _FirstStepContentState();
}
class _FirstStepContentState extends State<FirstStepContent> {
final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy);
final firstNameController = TextEditingController();
final lastNameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final dateController = TextEditingController();
@override
void initState() {
super.initState();
firstNameController.text = context.read<RegistrationCubitV2>().state.firstName;
lastNameController.text = context.read<RegistrationCubitV2>().state.lastName;
emailController.text = context.read<RegistrationCubitV2>().state.email;
phoneController.text = context.read<RegistrationCubitV2>().state.phone;
dateController.text = context.read<RegistrationCubitV2>().state.birthday?.format('dd.MM.yyyy') ?? '';
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<RegistrationCubitV2, RegistrationStateV2>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.first_name, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
hintText: s.your_name,
hasError: !state.isFirstNameValid,
controller: firstNameController,
onChanged: (val) {
context.read<RegistrationCubitV2>().setFirstName(val);
},
),
if (!state.isFirstNameValid) ...[const SizedBox(height: 4), ValidationErrorWidget(error: s.first_name_validation_error)],
],
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.last_name, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
hintText: s.your_last_name,
hasError: !state.isLastNameValid,
controller: lastNameController,
onChanged: (val) {
context.read<RegistrationCubitV2>().setLastName(val);
},
),
if (!state.isLastNameValid) ...[const SizedBox(height: 4), ValidationErrorWidget(error: s.last_name_error)],
],
),
),
],
),
const SizedBox(height: 12),
Text(s.email, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
CustomTextFormField(
hintText: s.type_email,
hasError: !state.isEmailValid,
controller: emailController,
onChanged: (val) {
context.read<RegistrationCubitV2>().setEmail(val);
},
),
if (!state.isEmailValid) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: state.emailDisplayError.isEmpty ? s.email_error : state.emailDisplayError),
],
const SizedBox(height: 12),
Text(s.phone, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
Column(
children: [
CustomTextFormField(
hintText: '+7 (999) 999 99 99',
controller: phoneController,
hasError: !state.isPhoneValid,
onChanged: (val) {
context.read<RegistrationCubitV2>().setPhone(val);
},
),
if (!state.isPhoneValid) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: state.phoneDisplayError.isEmpty ? s.phone_error : state.phoneDisplayError),
],
],
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.date_of_birth, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
textInputAction: TextInputAction.done,
hintText: '01.01.2000',
keyboardType: TextInputType.number,
inputFormatters: [maskFormatterForBirthday],
hasError: !state.isBirthdayValid,
controller: dateController,
onChanged: (val) {
final dateArray = val.split('.');
if (dateArray.length != 3) return;
final date = DateTime(int.parse(dateArray[2]), int.parse(dateArray[1]), int.parse(dateArray[0]));
context.read<RegistrationCubitV2>().setBirthday(date);
},
),
if (!state.isBirthdayValid) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: state.dateOfBirthDisplayError.isEmpty ? s.date_of_birth_error : state.dateOfBirthDisplayError),
],
],
),
),
const SizedBox(width: 12),
const Expanded(child: SexWidget()),
],
),
const Spacer(),
CustomOutlinedButton(
onPressed: () {
context.read<RegistrationCubitV2>().nextStep();
},
text: s.next,
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
),
const SizedBox(height: 20),
],
);
},
);
}
}
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:eatvio_app/features/registration_v2/presentation/widgets/sex_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
class FirstStepContent extends StatefulWidget {
const FirstStepContent({super.key});
@override
State<FirstStepContent> createState() => _FirstStepContentState();
}
class _FirstStepContentState extends State<FirstStepContent> {
final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy);
final firstNameController = TextEditingController();
final lastNameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final dateController = TextEditingController();
@override
void initState() {
super.initState();
firstNameController.text = context.read<RegistrationCubitV2>().state.firstName;
lastNameController.text = context.read<RegistrationCubitV2>().state.lastName;
emailController.text = context.read<RegistrationCubitV2>().state.email;
phoneController.text = context.read<RegistrationCubitV2>().state.phone;
dateController.text = context.read<RegistrationCubitV2>().state.birthday?.format('dd.MM.yyyy') ?? '';
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<RegistrationCubitV2, RegistrationStateV2>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.first_name, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
hintText: s.your_name,
hasError: !state.isFirstNameValid,
controller: firstNameController,
onChanged: (val) {
context.read<RegistrationCubitV2>().setFirstName(val);
},
),
if (!state.isFirstNameValid) ...[const SizedBox(height: 4), ValidationErrorWidget(error: s.first_name_validation_error)],
],
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.last_name, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
hintText: s.your_last_name,
hasError: !state.isLastNameValid,
controller: lastNameController,
onChanged: (val) {
context.read<RegistrationCubitV2>().setLastName(val);
},
),
if (!state.isLastNameValid) ...[const SizedBox(height: 4), ValidationErrorWidget(error: s.last_name_error)],
],
),
),
],
),
const SizedBox(height: 12),
Text(s.email, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
CustomTextFormField(
hintText: s.type_email,
hasError: !state.isEmailValid,
keyboardType: TextInputType.emailAddress,
controller: emailController,
onChanged: (val) {
context.read<RegistrationCubitV2>().setEmail(val);
},
),
if (!state.isEmailValid) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: state.emailDisplayError.isEmpty ? s.email_error : state.emailDisplayError),
],
const SizedBox(height: 12),
Text(s.phone, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
Column(
children: [
CustomTextFormField(
hintText: '+7 (999) 999 99 99',
controller: phoneController,
hasError: !state.isPhoneValid,
keyboardType: TextInputType.phone,
onChanged: (val) {
context.read<RegistrationCubitV2>().setPhone(val);
},
),
if (!state.isPhoneValid) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: state.phoneDisplayError.isEmpty ? s.phone_error : state.phoneDisplayError),
],
],
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.date_of_birth, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
textInputAction: TextInputAction.done,
hintText: '01.01.2000',
keyboardType: TextInputType.number,
inputFormatters: [maskFormatterForBirthday],
hasError: !state.isBirthdayValid,
controller: dateController,
onChanged: (val) {
final dateArray = val.split('.');
if (dateArray.length != 3) return;
final date = DateTime(int.parse(dateArray[2]), int.parse(dateArray[1]), int.parse(dateArray[0]));
context.read<RegistrationCubitV2>().setBirthday(date);
},
),
if (!state.isBirthdayValid) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: state.dateOfBirthDisplayError.isEmpty ? s.date_of_birth_error : state.dateOfBirthDisplayError),
],
],
),
),
const SizedBox(width: 12),
const Expanded(child: SexWidget()),
],
),
const Spacer(),
CustomOutlinedButton(
onPressed: () {
context.read<RegistrationCubitV2>().nextStep();
},
text: s.next,
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
),
const SizedBox(height: 20),
],
);
},
);
}
}

View File

@@ -33,8 +33,12 @@ class _SecondStepContentState extends State<SecondStepContent> {
@override
void initState() {
super.initState();
heightController.text = context.read<RegistrationCubitV2>().state.height.toString();
weightController.text = context.read<RegistrationCubitV2>().state.weight.toString();
heightController.text = context.read<RegistrationCubitV2>().state.height.toString() == '0'
? ''
: context.read<RegistrationCubitV2>().state.height.toString();
weightController.text = context.read<RegistrationCubitV2>().state.weight.toString() == '0'
? ''
: context.read<RegistrationCubitV2>().state.weight.toString();
}
@override
@@ -61,9 +65,13 @@ class _SecondStepContentState extends State<SecondStepContent> {
hintText: s.type_height,
hasError: !state.isHeightValid,
controller: heightController,
keyboardType: TextInputType.number,
textInputType: const TextInputType.numberWithOptions(),
onChanged: (val) {
context.read<RegistrationCubitV2>().setHeight(int.parse(val));
final height = int.tryParse(val);
if (height != null) {
context.read<RegistrationCubitV2>().setHeight(height);
}
},
),
if (!state.isHeightValid) ValidationErrorWidget(error: s.height_error),
@@ -81,9 +89,13 @@ class _SecondStepContentState extends State<SecondStepContent> {
hintText: s.type_weight,
hasError: !state.isWeightValid,
controller: weightController,
keyboardType: TextInputType.number,
textInputType: const TextInputType.numberWithOptions(),
onChanged: (val) {
context.read<RegistrationCubitV2>().setWeight(int.parse(val));
final weight = int.tryParse(val);
if (weight != null) {
context.read<RegistrationCubitV2>().setWeight(weight);
}
},
),
if (!state.isWeightValid) ValidationErrorWidget(error: s.weight_error),

View File

@@ -76,7 +76,7 @@ class _RegistrationBottomSheetState extends State<RegistrationBottomSheet> {
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
AppRouter.navigatorKey.currentContext?.go(RouteConstants.feedScreen);
AppRouter.navigatorKey.currentContext?.go(RouteConstants.verifyEmailScreen);
},
),
const SizedBox(width: 20),

View File

@@ -65,10 +65,16 @@ class _GeneralInfoScreenState extends State<GeneralInfoScreen> {
appBar: AppBar(
title: const Text('Общая информация'),
),
bottomSheet: _saveButton(),
bottomSheet: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 56), // высота bottomNavigationBar
child: _saveButton(),
),
),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 64),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 120), // 52 (кнопка) + 56 (навигация) + 12 (отступ)
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -45,9 +45,15 @@ class _OtherInfoScreenState extends State<OtherInfoScreen> {
appBar: AppBar(
title: const Text('Прочая информаиця'),
),
bottomSheet: _saveButton(),
bottomSheet: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 56), // высота bottomNavigationBar
child: _saveButton(),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 64),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 120), // 52 (кнопка) + 56 (навигация) + 12 (отступ)
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -75,10 +75,16 @@ class _PhysicalActivityScreenState extends State<PhysicalActivityScreen> {
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: const Text('Физическая нагрузка')),
bottomSheet: _saveButton(),
bottomSheet: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 56), // высота bottomNavigationBar
child: _saveButton(),
),
),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.only(top: 16, bottom: 80),
padding: const EdgeInsets.only(top: 16, bottom: 120), // 52 (кнопка) + 56 (навигация) + 12 (отступ)
child: Column(
children: [
Row(

View File

@@ -45,10 +45,16 @@ class _PhysicalFormScreenState extends State<PhysicalFormScreen> {
appBar: AppBar(
title: const Text('Физическая форма'),
),
bottomSheet: _saveButton(),
bottomSheet: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 56), // высота bottomNavigationBar
child: _saveButton(),
),
),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 64),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 120), // 52 (кнопка) + 56 (навигация) + 12 (отступ)
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -50,10 +50,16 @@ class _PressureScreenState extends State<PressureScreen> {
appBar: AppBar(
title: const Text('Давление'),
),
bottomSheet: _saveButton(),
bottomSheet: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 56), // высота bottomNavigationBar
child: _saveButton(),
),
),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 64),
padding: const EdgeInsets.all(16) + const EdgeInsets.only(bottom: 120), // 52 (кнопка) + 56 (навигация) + 12 (отступ)
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -1,10 +1,10 @@
name: eatvio_app
description: A new Flutter project.
version: 1.3.8+88
version: 1.3.10+90
publish_to: 'none'
environment:
sdk: '>=3.8.0 <4.0.0'
sdk: ^3.10.4
dependencies:
flutter:
@@ -15,7 +15,7 @@ dependencies:
flutter_bloc: ^9.1.1
cached_network_image: ^3.4.1
flutter_svg: ^2.2.0
flutter_svg: ^2.2.3
equatable: ^2.0.7
freezed_annotation: ^3.1.0
json_annotation: ^4.9.0
@@ -23,12 +23,12 @@ dependencies:
path_provider: ^2.1.5
carousel_slider_plus: ^7.1.1
flutter_form_builder: ^10.2.0
image_picker: ^1.2.0
image_picker: ^1.2.1
flutter_typeahead: ^5.2.0
go_router: ^17.0.0
go_router: ^17.0.1
keyboard_dismisser: ^3.0.0
retrofit: ^4.7.1
get_it: ^9.0.5
retrofit: 4.7.2
get_it: ^9.2.0
dio: ^5.9.0
dart_pusher_channels: ^1.2.3
photo_view: ^0.15.0
@@ -36,60 +36,60 @@ dependencies:
url_launcher: ^6.3.2
easy_debounce: ^2.0.3
badges: ^3.1.2
google_fonts: ^6.3.0
syncfusion_flutter_charts: ^31.2.10
syncfusion_flutter_gauges: ^31.2.10
google_fonts: ^6.3.3
syncfusion_flutter_charts: ^32.1.20
syncfusion_flutter_gauges: ^32.1.20
table_calendar: ^3.2.0
flutter_keyboard_visibility: ^6.0.0
share_plus: ^12.0.1
flutter_html: ^3.0.0
file_picker: ^10.3.1
audioplayers: ^6.5.0
file_picker: ^10.3.8
audioplayers: ^6.5.1
intl: ^0.20.2
mask_text_input_formatter: ^2.9.0
lottie: ^3.3.1
lottie: ^3.3.2
flutter_portal: ^1.1.4
fluttertoast: ^9.0.0
flutter_rating_bar: ^4.0.1
flutter_image_compress: ^2.4.0
image: ^4.5.4
image: ^4.7.2
dotted_border: ^3.1.0
http: ^1.5.0
flutter_native_splash: ^2.4.6
http: ^1.6.0
flutter_native_splash: ^2.4.7
webview_flutter: ^4.13.0
sentry_flutter: ^9.8.0
sentry_flutter: ^9.9.1
connectivity_plus: ^7.0.0
visibility_detector: ^0.4.0+2
firebase_core: ^4.0.0
firebase_messaging: ^16.0.0
firebase_core: ^4.3.0
firebase_messaging: ^16.1.0
http_parser: ^4.1.2
path: ^1.9.1
collection: ^1.19.1
shared_preferences: ^2.5.3
shared_preferences: ^2.5.4
version: ^3.0.2
package_info_plus: ^9.0.0
device_info_plus: ^12.2.0
device_info_plus: ^12.3.0
permission_handler: ^12.0.1
qr_code_scanner_plus: ^2.0.10+1
qr_code_scanner_plus: ^2.0.14
flutter_dotenv: ^6.0.0
uuid: ^4.5.2
appmetrica_plugin: ^3.3.0
vk_id: ^1.0.3
appmetrica_plugin: ^3.4.0
vk_id: ^1.0.4
flutter_login_yandex: ^0.0.5
markdown: ^7.3.0
flutter_password_generator: ^1.0.3
mobile_scanner: ^7.1.3
material_symbols_icons: ^4.2874.0
mobile_scanner: ^7.1.4
material_symbols_icons: ^4.2892.0
rxdart: ^0.27.7
gpassword: ^1.0.0
webview_flutter_platform_interface: ^2.14.0
dev_dependencies:
build_runner: ^2.7.0
build_runner: ^2.10.4
freezed: ^3.2.0
json_serializable: ^6.10.0
retrofit_generator: ^10.0.2
mockito: ^5.5.0
json_serializable: ^6.11.2
retrofit_generator: 10.0.6
mockito: ^5.6.1
mocktail: ^1.0.4
network_image_mock: ^2.1.1

View File

@@ -29,28 +29,37 @@ void main() {
});
test('navigateToRegisterViaSocial - принимает правильные параметры', () {
const authResult = AuthResultEntity(
accessToken: 'test_token',
refreshToken: 'refresh_token',
userId: '1',
clientId: 'test_client_id',
);
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,
);
expect(() => AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider), returnsNormally);
});
test('appRouter содержит маршруты', () {
final routes = AppRouter.appRouter.configuration.routes;
expect(routes, isNotEmpty);
});
test('appRouter имеет правильную конфигурацию', () {
expect(AppRouter.appRouter.configuration, isNotNull);
expect(AppRouter.appRouter.configuration.routes, isNotEmpty);
});
test('appRouter имеет redirect функцию', () {
expect(AppRouter.appRouter.configuration.redirect, isNotNull);
});
test('navigateToRegisterViaSocial вызывает pushNamed с правильными параметрами', () {
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 configuration routes содержит маршруты разных типов', () {
final routes = AppRouter.appRouter.configuration.routes;
expect(routes.length, greaterThan(0));
});
});
}

View File

@@ -1,143 +1,157 @@
import 'package:eatvio_app/core/constants/app_constants.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('AppConstants', () {
group('Sizes', () {
test('has correct padding values', () {
expect(AppConstants.defaultPadding, 16.0);
expect(AppConstants.smallPadding, 8.0);
expect(AppConstants.largePadding, 24.0);
expect(AppConstants.extraLargePadding, 32.0);
});
test('has correct border radius values', () {
expect(AppConstants.defaultBorderRadius, 8.0);
expect(AppConstants.smallBorderRadius, 4.0);
expect(AppConstants.largeBorderRadius, 16.0);
});
test('has correct button height values', () {
expect(AppConstants.defaultButtonHeight, 48.0);
expect(AppConstants.smallButtonHeight, 36.0);
expect(AppConstants.largeButtonHeight, 56.0);
});
test('has correct content width values', () {
expect(AppConstants.maxContentWidth, 400.0);
expect(AppConstants.minContentWidth, 280.0);
});
});
group('Animations', () {
test('has correct animation durations', () {
expect(AppConstants.defaultAnimationDuration, const Duration(milliseconds: 300));
expect(AppConstants.fastAnimationDuration, const Duration(milliseconds: 150));
expect(AppConstants.slowAnimationDuration, const Duration(milliseconds: 500));
});
});
group('Timeouts', () {
test('has correct timeout durations', () {
expect(AppConstants.defaultTimeout, const Duration(seconds: 30));
expect(AppConstants.shortTimeout, const Duration(seconds: 10));
expect(AppConstants.longTimeout, const Duration(minutes: 2));
});
});
group('Limits', () {
test('has correct size limits', () {
expect(AppConstants.maxImageSize, 5 * 1024 * 1024);
expect(AppConstants.maxVideoSize, 100 * 1024 * 1024);
});
test('has correct text length limits', () {
expect(AppConstants.maxTextLength, 1000);
expect(AppConstants.maxTitleLength, 100);
expect(AppConstants.maxDescriptionLength, 500);
});
});
group('Pagination', () {
test('has correct page sizes', () {
expect(AppConstants.defaultPageSize, 20);
expect(AppConstants.smallPageSize, 10);
expect(AppConstants.largePageSize, 50);
});
});
group('Formats', () {
test('has supported image formats', () {
expect(AppConstants.supportedImageFormats, contains('jpg'));
expect(AppConstants.supportedImageFormats, contains('jpeg'));
expect(AppConstants.supportedImageFormats, contains('png'));
expect(AppConstants.supportedImageFormats, contains('webp'));
});
test('has supported video formats', () {
expect(AppConstants.supportedVideoFormats, contains('mp4'));
expect(AppConstants.supportedVideoFormats, contains('mov'));
expect(AppConstants.supportedVideoFormats, contains('avi'));
});
});
group('Regex', () {
test('has email regex', () {
expect(AppConstants.emailRegex, isA<String>());
expect(AppConstants.emailRegex.isNotEmpty, isTrue);
});
test('has phone regex', () {
expect(AppConstants.phoneRegex, isA<String>());
expect(AppConstants.phoneRegex.isNotEmpty, isTrue);
});
test('has URL regex', () {
expect(AppConstants.urlRegex, isA<String>());
expect(AppConstants.urlRegex.isNotEmpty, isTrue);
});
});
group('Messages', () {
test('has error messages', () {
expect(AppConstants.defaultErrorMessage, isA<String>());
expect(AppConstants.networkErrorMessage, isA<String>());
expect(AppConstants.timeoutErrorMessage, isA<String>());
expect(AppConstants.unknownErrorMessage, isA<String>());
});
});
group('Keys', () {
test('has SharedPreferences keys', () {
expect(AppConstants.welcomeScreenShownKey, isA<String>());
expect(AppConstants.userTokenKey, isA<String>());
expect(AppConstants.userDataKey, isA<String>());
expect(AppConstants.settingsKey, isA<String>());
});
});
group('API Endpoints', () {
test('has API endpoints', () {
expect(AppConstants.apiVersion, '/api');
expect(AppConstants.authEndpoint, '/auth');
expect(AppConstants.usersEndpoint, '/users');
expect(AppConstants.postsEndpoint, '/posts');
expect(AppConstants.commentsEndpoint, '/comments');
});
});
});
}
import 'package:eatvio_app/core/constants/app_constants.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('AppConstants', () {
group('Sizes', () {
test('has correct padding values', () {
expect(AppConstants.defaultPadding, 16.0);
expect(AppConstants.smallPadding, 8.0);
expect(AppConstants.largePadding, 24.0);
expect(AppConstants.extraLargePadding, 32.0);
});
test('has correct border radius values', () {
expect(AppConstants.defaultBorderRadius, 8.0);
expect(AppConstants.smallBorderRadius, 4.0);
expect(AppConstants.largeBorderRadius, 16.0);
});
test('has correct button height values', () {
expect(AppConstants.defaultButtonHeight, 48.0);
expect(AppConstants.smallButtonHeight, 36.0);
expect(AppConstants.largeButtonHeight, 56.0);
});
test('has correct content width values', () {
expect(AppConstants.maxContentWidth, 400.0);
expect(AppConstants.minContentWidth, 280.0);
});
});
group('Animations', () {
test('has correct animation durations', () {
expect(AppConstants.defaultAnimationDuration, const Duration(milliseconds: 300));
expect(AppConstants.fastAnimationDuration, const Duration(milliseconds: 150));
expect(AppConstants.slowAnimationDuration, const Duration(milliseconds: 500));
});
});
group('Timeouts', () {
test('has correct timeout durations', () {
expect(AppConstants.defaultTimeout, const Duration(seconds: 30));
expect(AppConstants.shortTimeout, const Duration(seconds: 10));
expect(AppConstants.longTimeout, const Duration(minutes: 2));
});
});
group('Limits', () {
test('has correct size limits', () {
expect(AppConstants.maxImageSize, 5 * 1024 * 1024);
expect(AppConstants.maxVideoSize, 100 * 1024 * 1024);
});
test('has correct text length limits', () {
expect(AppConstants.maxTextLength, 1000);
expect(AppConstants.maxTitleLength, 100);
expect(AppConstants.maxDescriptionLength, 500);
});
});
group('Pagination', () {
test('has correct page sizes', () {
expect(AppConstants.defaultPageSize, 20);
expect(AppConstants.smallPageSize, 10);
expect(AppConstants.largePageSize, 50);
});
});
group('Formats', () {
test('has supported image formats', () {
expect(AppConstants.supportedImageFormats, contains('jpg'));
expect(AppConstants.supportedImageFormats, contains('jpeg'));
expect(AppConstants.supportedImageFormats, contains('png'));
expect(AppConstants.supportedImageFormats, contains('webp'));
});
test('has supported video formats', () {
expect(AppConstants.supportedVideoFormats, contains('mp4'));
expect(AppConstants.supportedVideoFormats, contains('mov'));
expect(AppConstants.supportedVideoFormats, contains('avi'));
});
});
group('Regex', () {
test('has email regex', () {
expect(AppConstants.emailRegex, isA<String>());
expect(AppConstants.emailRegex.isNotEmpty, isTrue);
});
test('has phone regex', () {
expect(AppConstants.phoneRegex, isA<String>());
expect(AppConstants.phoneRegex.isNotEmpty, isTrue);
});
test('has URL regex', () {
expect(AppConstants.urlRegex, isA<String>());
expect(AppConstants.urlRegex.isNotEmpty, isTrue);
});
});
group('Messages', () {
test('has error messages', () {
expect(AppConstants.defaultErrorMessage, isA<String>());
expect(AppConstants.networkErrorMessage, isA<String>());
expect(AppConstants.timeoutErrorMessage, isA<String>());
expect(AppConstants.unknownErrorMessage, isA<String>());
});
});
group('Keys', () {
test('has SharedPreferences keys', () {
expect(AppConstants.welcomeScreenShownKey, isA<String>());
expect(AppConstants.userTokenKey, isA<String>());
expect(AppConstants.userDataKey, isA<String>());
expect(AppConstants.settingsKey, isA<String>());
});
});
group('API Endpoints', () {
test('has API endpoints', () {
expect(AppConstants.apiVersion, '/api');
expect(AppConstants.authEndpoint, '/auth');
expect(AppConstants.usersEndpoint, '/users');
expect(AppConstants.postsEndpoint, '/posts');
expect(AppConstants.commentsEndpoint, '/comments');
});
});
});
}

View File

@@ -1,78 +1,92 @@
import 'package:eatvio_app/core/cubit/state_utils.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('StateUtils', () {
test('isLoading returns true for loading state', () {
expect(StateUtils.isLoading('loading'), isTrue);
expect(StateUtils.isLoading('Loading'), isTrue);
expect(StateUtils.isLoading('StateLoading'), isTrue);
expect(StateUtils.isLoading('state_loading'), isTrue);
});
test('isLoading returns false for non-loading state', () {
expect(StateUtils.isLoading('loaded'), isFalse);
expect(StateUtils.isLoading('error'), isFalse);
expect(StateUtils.isLoading('success'), isFalse);
});
test('isLoaded returns true for loaded state', () {
expect(StateUtils.isLoaded('loaded'), isTrue);
expect(StateUtils.isLoaded('Loaded'), isTrue);
expect(StateUtils.isLoaded('StateLoaded'), isTrue);
expect(StateUtils.isLoaded('state_loaded'), isTrue);
});
test('isLoaded returns false for non-loaded state', () {
expect(StateUtils.isLoaded('loading'), isFalse);
expect(StateUtils.isLoaded('error'), isFalse);
expect(StateUtils.isLoaded('initial'), isFalse);
});
test('isError returns true for error state', () {
expect(StateUtils.isError('error'), isTrue);
expect(StateUtils.isError('Error'), isTrue);
expect(StateUtils.isError('StateError'), isTrue);
expect(StateUtils.isError('state_error'), isTrue);
});
test('isError returns false for non-error state', () {
expect(StateUtils.isError('loading'), isFalse);
expect(StateUtils.isError('loaded'), isFalse);
expect(StateUtils.isError('success'), isFalse);
});
test('getErrorMessage returns null for non-error state', () {
expect(StateUtils.getErrorMessage('loading'), isNull);
expect(StateUtils.getErrorMessage('loaded'), isNull);
expect(StateUtils.getErrorMessage('success'), isNull);
});
test('getErrorMessage extracts error message from error state', () {
const errorState = 'error: Something went wrong';
final message = StateUtils.getErrorMessage(errorState);
expect(message, isNotNull);
expect(message, contains('Something'));
});
test('getErrorMessage handles various error state formats', () {
expect(StateUtils.getErrorMessage('error: test'), isNotNull);
expect(StateUtils.getErrorMessage('Error: test message'), isNotNull);
});
});
}
import 'package:eatvio_app/core/cubit/state_utils.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('StateUtils', () {
test('isLoading returns true for loading state', () {
expect(StateUtils.isLoading('loading'), isTrue);
expect(StateUtils.isLoading('Loading'), isTrue);
expect(StateUtils.isLoading('StateLoading'), isTrue);
expect(StateUtils.isLoading('state_loading'), isTrue);
});
test('isLoading returns false for non-loading state', () {
expect(StateUtils.isLoading('loaded'), isFalse);
expect(StateUtils.isLoading('error'), isFalse);
expect(StateUtils.isLoading('success'), isFalse);
});
test('isLoaded returns true for loaded state', () {
expect(StateUtils.isLoaded('loaded'), isTrue);
expect(StateUtils.isLoaded('Loaded'), isTrue);
expect(StateUtils.isLoaded('StateLoaded'), isTrue);
expect(StateUtils.isLoaded('state_loaded'), isTrue);
});
test('isLoaded returns false for non-loaded state', () {
expect(StateUtils.isLoaded('loading'), isFalse);
expect(StateUtils.isLoaded('error'), isFalse);
expect(StateUtils.isLoaded('initial'), isFalse);
});
test('isError returns true for error state', () {
expect(StateUtils.isError('error'), isTrue);
expect(StateUtils.isError('Error'), isTrue);
expect(StateUtils.isError('StateError'), isTrue);
expect(StateUtils.isError('state_error'), isTrue);
});
test('isError returns false for non-error state', () {
expect(StateUtils.isError('loading'), isFalse);
expect(StateUtils.isError('loaded'), isFalse);
expect(StateUtils.isError('success'), isFalse);
});
test('getErrorMessage returns null for non-error state', () {
expect(StateUtils.getErrorMessage('loading'), isNull);
expect(StateUtils.getErrorMessage('loaded'), isNull);
expect(StateUtils.getErrorMessage('success'), isNull);
});
test('getErrorMessage extracts error message from error state', () {
const errorState = 'error: Something went wrong';
final message = StateUtils.getErrorMessage(errorState);
expect(message, isNotNull);
expect(message, contains('Something'));
});
test('getErrorMessage handles various error state formats', () {
expect(StateUtils.getErrorMessage('error: test'), isNotNull);
expect(StateUtils.getErrorMessage('Error: test message'), isNotNull);
});
});
}

View File

@@ -1,92 +1,106 @@
import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('SelectionPopupModel', () {
test('creates model with all parameters', () {
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, true);
});
test('creates model with default isSelected value', () {
const model = SelectionPopupModel(
title: 'Test Title',
value: 'Test Value',
);
expect(model.isSelected, false);
});
test('creates model with null id', () {
const model = SelectionPopupModel(
id: null,
title: 'Test Title',
value: 'Test Value',
);
expect(model.id, null);
expect(model.title, 'Test Title');
});
test('creates model with different value types', () {
const model1 = SelectionPopupModel(
title: 'Test',
value: 123,
);
const model2 = SelectionPopupModel(
title: 'Test',
value: 'String Value',
);
const model3 = SelectionPopupModel(
title: 'Test',
value: true,
);
expect(model1.value, 123);
expect(model2.value, 'String Value');
expect(model3.value, true);
});
test('can be compared', () {
const model1 = SelectionPopupModel(
id: 1,
title: 'Test',
value: 'Value',
);
const model2 = SelectionPopupModel(
id: 1,
title: 'Test',
value: 'Value',
);
expect(model1 == model2, true);
});
});
}
import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('SelectionPopupModel', () {
test('creates model with all parameters', () {
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, true);
});
test('creates model with default isSelected value', () {
const model = SelectionPopupModel(
title: 'Test Title',
value: 'Test Value',
);
expect(model.isSelected, false);
});
test('creates model with null id', () {
const model = SelectionPopupModel(
id: null,
title: 'Test Title',
value: 'Test Value',
);
expect(model.id, null);
expect(model.title, 'Test Title');
});
test('creates model with different value types', () {
const model1 = SelectionPopupModel(
title: 'Test',
value: 123,
);
const model2 = SelectionPopupModel(
title: 'Test',
value: 'String Value',
);
const model3 = SelectionPopupModel(
title: 'Test',
value: true,
);
expect(model1.value, 123);
expect(model2.value, 'String Value');
expect(model3.value, true);
});
test('can be compared', () {
const model1 = SelectionPopupModel(
id: 1,
title: 'Test',
value: 'Value',
);
const model2 = SelectionPopupModel(
id: 1,
title: 'Test',
value: 'Value',
);
expect(model1 == model2, true);
});
});
}

View File

@@ -1,41 +1,55 @@
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('PrefsKeys', () {
test('has all expected values', () {
expect(PrefsKeys.values.length, 4);
expect(PrefsKeys.values, contains(PrefsKeys.hasAnonymousTokenBeenSent));
expect(PrefsKeys.values, contains(PrefsKeys.welcomeScreenWasShowed));
expect(PrefsKeys.values, contains(PrefsKeys.registrationDate));
expect(PrefsKeys.values, contains(PrefsKeys.marketingBottomSheetHasBeenShown));
});
test('can be compared', () {
expect(PrefsKeys.hasAnonymousTokenBeenSent == PrefsKeys.hasAnonymousTokenBeenSent, true);
expect(PrefsKeys.hasAnonymousTokenBeenSent == PrefsKeys.welcomeScreenWasShowed, false);
});
test('has correct name property', () {
expect(PrefsKeys.hasAnonymousTokenBeenSent.name, 'hasAnonymousTokenBeenSent');
expect(PrefsKeys.welcomeScreenWasShowed.name, 'welcomeScreenWasShowed');
expect(PrefsKeys.registrationDate.name, 'registrationDate');
expect(PrefsKeys.marketingBottomSheetHasBeenShown.name, 'marketingBottomSheetHasBeenShown');
});
});
}
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('PrefsKeys', () {
test('has all expected values', () {
expect(PrefsKeys.values.length, 4);
expect(PrefsKeys.values, contains(PrefsKeys.hasAnonymousTokenBeenSent));
expect(PrefsKeys.values, contains(PrefsKeys.welcomeScreenWasShowed));
expect(PrefsKeys.values, contains(PrefsKeys.registrationDate));
expect(PrefsKeys.values, contains(PrefsKeys.marketingBottomSheetHasBeenShown));
});
test('can be compared', () {
expect(PrefsKeys.hasAnonymousTokenBeenSent == PrefsKeys.hasAnonymousTokenBeenSent, true);
expect(PrefsKeys.hasAnonymousTokenBeenSent == PrefsKeys.welcomeScreenWasShowed, false);
});
test('has correct name property', () {
expect(PrefsKeys.hasAnonymousTokenBeenSent.name, 'hasAnonymousTokenBeenSent');
expect(PrefsKeys.welcomeScreenWasShowed.name, 'welcomeScreenWasShowed');
expect(PrefsKeys.registrationDate.name, 'registrationDate');
expect(PrefsKeys.marketingBottomSheetHasBeenShown.name, 'marketingBottomSheetHasBeenShown');
});
});
}

View File

@@ -1,67 +1,81 @@
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_state.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('InternetConnectionState', () {
test('creates state with default isInternetAvailable value', () {
final state = InternetConnectionState();
expect(state.isInternetAvailable, true);
});
test('creates state with isInternetAvailable set to false', () {
final state = InternetConnectionState(
isInternetAvailable: false,
);
expect(state.isInternetAvailable, false);
});
test('creates state with isInternetAvailable set to true', () {
final state = InternetConnectionState(
isInternetAvailable: true,
);
expect(state.isInternetAvailable, true);
});
test('can be compared', () {
final state1 = InternetConnectionState(
isInternetAvailable: true,
);
final state2 = InternetConnectionState(
isInternetAvailable: true,
);
expect(state1 == state2, true);
});
test('different states are not equal', () {
final state1 = InternetConnectionState(
isInternetAvailable: true,
);
final state2 = InternetConnectionState(
isInternetAvailable: false,
);
expect(state1 == state2, false);
});
});
}
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_state.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('InternetConnectionState', () {
test('creates state with default isInternetAvailable value', () {
final state = InternetConnectionState();
expect(state.isInternetAvailable, true);
});
test('creates state with isInternetAvailable set to false', () {
final state = InternetConnectionState(
isInternetAvailable: false,
);
expect(state.isInternetAvailable, false);
});
test('creates state with isInternetAvailable set to true', () {
final state = InternetConnectionState(
isInternetAvailable: true,
);
expect(state.isInternetAvailable, true);
});
test('can be compared', () {
final state1 = InternetConnectionState(
isInternetAvailable: true,
);
final state2 = InternetConnectionState(
isInternetAvailable: true,
);
expect(state1 == state2, true);
});
test('different states are not equal', () {
final state1 = InternetConnectionState(
isInternetAvailable: true,
);
final state2 = InternetConnectionState(
isInternetAvailable: false,
);
expect(state1 == state2, false);
});
});
}

View File

@@ -1,37 +1,51 @@
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('PrefsKeys', () {
test('все значения enum определены', () {
expect(PrefsKeys.values.length, 4);
expect(PrefsKeys.values, contains(PrefsKeys.hasAnonymousTokenBeenSent));
expect(PrefsKeys.values, contains(PrefsKeys.welcomeScreenWasShowed));
expect(PrefsKeys.values, contains(PrefsKeys.registrationDate));
expect(PrefsKeys.values, contains(PrefsKeys.marketingBottomSheetHasBeenShown));
});
test('имена ключей корректны', () {
expect(PrefsKeys.hasAnonymousTokenBeenSent.name, 'hasAnonymousTokenBeenSent');
expect(PrefsKeys.welcomeScreenWasShowed.name, 'welcomeScreenWasShowed');
expect(PrefsKeys.registrationDate.name, 'registrationDate');
expect(PrefsKeys.marketingBottomSheetHasBeenShown.name, 'marketingBottomSheetHasBeenShown');
});
});
}
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('PrefsKeys', () {
test('все значения enum определены', () {
expect(PrefsKeys.values.length, 4);
expect(PrefsKeys.values, contains(PrefsKeys.hasAnonymousTokenBeenSent));
expect(PrefsKeys.values, contains(PrefsKeys.welcomeScreenWasShowed));
expect(PrefsKeys.values, contains(PrefsKeys.registrationDate));
expect(PrefsKeys.values, contains(PrefsKeys.marketingBottomSheetHasBeenShown));
});
test('имена ключей корректны', () {
expect(PrefsKeys.hasAnonymousTokenBeenSent.name, 'hasAnonymousTokenBeenSent');
expect(PrefsKeys.welcomeScreenWasShowed.name, 'welcomeScreenWasShowed');
expect(PrefsKeys.registrationDate.name, 'registrationDate');
expect(PrefsKeys.marketingBottomSheetHasBeenShown.name, 'marketingBottomSheetHasBeenShown');
});
});
}

View File

@@ -1,34 +1,48 @@
import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
void main() {
group('MainNavigationRoutes', () {
test('createRoutes возвращает список маршрутов', () {
final routes = MainNavigationRoutes.createRoutes();
expect(routes, isNotEmpty);
expect(routes, isA<List<RouteBase>>());
});
test('createRoutes содержит ShellRoute', () {
final routes = MainNavigationRoutes.createRoutes();
expect(routes.first, isA<ShellRoute>());
});
});
}
import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
void main() {
group('MainNavigationRoutes', () {
test('createRoutes возвращает список маршрутов', () {
final routes = MainNavigationRoutes.createRoutes();
expect(routes, isNotEmpty);
expect(routes, isA<List<RouteBase>>());
});
test('createRoutes содержит ShellRoute', () {
final routes = MainNavigationRoutes.createRoutes();
expect(routes.first, isA<ShellRoute>());
});
});
}

View File

@@ -12,27 +12,70 @@ void main() {
test('createRoutes содержит маршрут welcomeScreen', () {
final routes = OtherRoutes.createRoutes();
final welcomeRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/welcome-screen',
final welcomeRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/welcome-screen', orElse: () => throw Exception('Route not found'));
expect(welcomeRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут updateScreen', () {
final routes = OtherRoutes.createRoutes();
final updateRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/update-screen', orElse: () => throw Exception('Route not found'));
expect(updateRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут calendarScreen', () {
final routes = OtherRoutes.createRoutes();
final calendarRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/calendar-screen',
orElse: () => throw Exception('Route not found'),
);
expect(welcomeRoute, isA<GoRoute>());
expect(calendarRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут supportScreen', () {
final routes = OtherRoutes.createRoutes();
final supportRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/support-screen', orElse: () => throw Exception('Route not found'));
expect(supportRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут glossaryScreen', () {
final routes = OtherRoutes.createRoutes();
final glossaryRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/glossary-screen',
orElse: () => throw Exception('Route not found'),
);
expect(glossaryRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут becomeConsultantScreen', () {
final routes = OtherRoutes.createRoutes();
final becomeConsultantRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/become-consultant-screen',
orElse: () => throw Exception('Route not found'),
);
expect(becomeConsultantRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут createPlanScreen', () {
final routes = OtherRoutes.createRoutes();
final createPlanRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/create-plan-screen',
orElse: () => throw Exception('Route not found'),
);
expect(createPlanRoute, isA<GoRoute>());
});
test('createRoutes содержит маршрут с redirect функцией для welcomeScreen', () {
final routes = OtherRoutes.createRoutes();
final welcomeRoute =
routes.firstWhere((route) => route is GoRoute && route.path == '/welcome-screen', orElse: () => throw Exception('Route not found')) as GoRoute;
expect(welcomeRoute.redirect, isNotNull);
});
test('createRoutes содержит маршрут с redirect функцией для updateScreen', () {
final routes = OtherRoutes.createRoutes();
final updateRoute =
routes.firstWhere((route) => route is GoRoute && route.path == '/update-screen', orElse: () => throw Exception('Route not found')) as GoRoute;
expect(updateRoute.redirect, isNotNull);
});
});
}

View File

@@ -1,117 +1,131 @@
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('RouteConstants', () {
test('has all auth routes defined', () {
expect(RouteConstants.authScreen, '/auth-screen');
expect(RouteConstants.registrationScreen, 'registration-screen');
expect(RouteConstants.registrationScreenV2, 'registration-screen-v2');
expect(RouteConstants.forgetPasswordScreen, 'forget-password-screen');
expect(RouteConstants.scanQRCodeScreen, 'scan-qr-code-screen');
expect(RouteConstants.verifyScreen, '/verify-screen');
expect(RouteConstants.verifyEmailScreen, '/verify-email-screen');
expect(RouteConstants.registerViaSocialScreen, '/register-via-social-screen');
});
test('has all main navigation routes defined', () {
expect(RouteConstants.myProfileScreen, '/my-profile-screen');
expect(RouteConstants.feedScreen, '/feed-screen');
expect(RouteConstants.diaryScreen, '/diary-screen');
expect(RouteConstants.consultationsScreen, '/consultations-screen');
expect(RouteConstants.chatSelectScreen, '/chat-select-screen');
expect(RouteConstants.notificationsScreen, '/notifications-screen');
expect(RouteConstants.menuScreen, '/menu-screen');
});
test('has all content routes defined', () {
expect(RouteConstants.aboutPlanScreen, '/about-plan-screen');
expect(RouteConstants.aboutRecipesScreen, '/about-recipes-screen');
expect(RouteConstants.aboutDietScreen, '/about-diet-screen');
expect(RouteConstants.blogPostScreen, '/blog-post-screen');
expect(RouteConstants.blogPostCreateScreen, '/blog-post-create-screen');
});
test('has all profile routes defined', () {
expect(RouteConstants.profileScreen, '/profile-screen');
expect(RouteConstants.subsScreen, '/subs-screen');
});
test('has all settings routes defined', () {
expect(RouteConstants.settingsScreen, '/settings-screen');
expect(RouteConstants.generalInfoScreen, 'general-info-screen');
expect(RouteConstants.otherInfoScreen, 'other-info-screen');
expect(RouteConstants.physicalActivityScreen, 'physical-activity-screen');
expect(RouteConstants.physicalFormScreen, 'physical-form-screen');
expect(RouteConstants.pressureScreen, 'pressure-screen');
expect(RouteConstants.changePasswordScreen, 'change-password-screen');
expect(RouteConstants.changeEmailScreen, 'change-email-screen');
});
test('has all consultant routes defined', () {
expect(RouteConstants.becomeConsultantScreen, '/become-consultant-screen');
expect(RouteConstants.stepperBecomeConsultantScreen, '/stepper-become-consultant-screen');
expect(RouteConstants.consultantRatingScreen, '/consultant-rating-screen');
});
test('has all other routes defined', () {
expect(RouteConstants.welcomeScreen, '/welcome-screen');
expect(RouteConstants.updateScreen, '/update-screen');
expect(RouteConstants.calendarScreen, '/calendar-screen');
expect(RouteConstants.supportScreen, '/support-screen');
expect(RouteConstants.photoViewScreen, '/photo-view-screen');
expect(RouteConstants.chatScreen, '/chat-screen');
expect(RouteConstants.emptyChatScreen, '/empty-chat-screen');
expect(RouteConstants.glossaryScreen, '/glossary-screen');
expect(RouteConstants.blockedUsersScreen, '/blocked-users-screen');
expect(RouteConstants.notificationsSettingsScreen, '/notifications-settings-screen');
});
test('has all finance routes defined', () {
expect(RouteConstants.userFinanceScreen, '/user-finance-screen');
expect(RouteConstants.cardSettingsScreen, '/card-settings-screen');
expect(RouteConstants.orderInfoScreen, '/order-info-screen');
expect(RouteConstants.paymentScreen, '/payment-screen');
});
test('has all content creation routes defined', () {
expect(RouteConstants.recipeCreateScreen, '/recipe-create-screen');
expect(RouteConstants.createDietScreen, '/create-diet-screen');
expect(RouteConstants.dietPreviewScreen, '/diet-preview-screen');
expect(RouteConstants.createPlanScreen, '/create-plan-screen');
expect(RouteConstants.myDietsScreen, '/my-diets-screen');
expect(RouteConstants.myPlansScreen, '/my-plans-screen');
expect(RouteConstants.myRecipesScreen, '/my-recipes-screen');
});
test('has all user details routes defined', () {
expect(RouteConstants.userDetailsScreen, '/user-details-screen');
expect(RouteConstants.userDiaryScreen, '/user-diary-screen');
});
test('has dev tools route defined', () {
expect(RouteConstants.devToolsScreen, '/dev-tools-screen');
});
test('has all archive routes defined', () {
expect(RouteConstants.consultationsArchiveScreen, '/consultations-archive-screen');
expect(RouteConstants.consultantFinancesScreen, '/consultant-finances-screen');
expect(RouteConstants.userPurchasesScreen, '/user-purchases-screen');
});
});
}
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('RouteConstants', () {
test('has all auth routes defined', () {
expect(RouteConstants.authScreen, '/auth-screen');
expect(RouteConstants.registrationScreen, 'registration-screen');
expect(RouteConstants.registrationScreenV2, 'registration-screen-v2');
expect(RouteConstants.forgetPasswordScreen, 'forget-password-screen');
expect(RouteConstants.scanQRCodeScreen, 'scan-qr-code-screen');
expect(RouteConstants.verifyScreen, '/verify-screen');
expect(RouteConstants.verifyEmailScreen, '/verify-email-screen');
expect(RouteConstants.registerViaSocialScreen, '/register-via-social-screen');
});
test('has all main navigation routes defined', () {
expect(RouteConstants.myProfileScreen, '/my-profile-screen');
expect(RouteConstants.feedScreen, '/feed-screen');
expect(RouteConstants.diaryScreen, '/diary-screen');
expect(RouteConstants.consultationsScreen, '/consultations-screen');
expect(RouteConstants.chatSelectScreen, '/chat-select-screen');
expect(RouteConstants.notificationsScreen, '/notifications-screen');
expect(RouteConstants.menuScreen, '/menu-screen');
});
test('has all content routes defined', () {
expect(RouteConstants.aboutPlanScreen, '/about-plan-screen');
expect(RouteConstants.aboutRecipesScreen, '/about-recipes-screen');
expect(RouteConstants.aboutDietScreen, '/about-diet-screen');
expect(RouteConstants.blogPostScreen, '/blog-post-screen');
expect(RouteConstants.blogPostCreateScreen, '/blog-post-create-screen');
});
test('has all profile routes defined', () {
expect(RouteConstants.profileScreen, '/profile-screen');
expect(RouteConstants.subsScreen, '/subs-screen');
});
test('has all settings routes defined', () {
expect(RouteConstants.settingsScreen, '/settings-screen');
expect(RouteConstants.generalInfoScreen, 'general-info-screen');
expect(RouteConstants.otherInfoScreen, 'other-info-screen');
expect(RouteConstants.physicalActivityScreen, 'physical-activity-screen');
expect(RouteConstants.physicalFormScreen, 'physical-form-screen');
expect(RouteConstants.pressureScreen, 'pressure-screen');
expect(RouteConstants.changePasswordScreen, 'change-password-screen');
expect(RouteConstants.changeEmailScreen, 'change-email-screen');
});
test('has all consultant routes defined', () {
expect(RouteConstants.becomeConsultantScreen, '/become-consultant-screen');
expect(RouteConstants.stepperBecomeConsultantScreen, '/stepper-become-consultant-screen');
expect(RouteConstants.consultantRatingScreen, '/consultant-rating-screen');
});
test('has all other routes defined', () {
expect(RouteConstants.welcomeScreen, '/welcome-screen');
expect(RouteConstants.updateScreen, '/update-screen');
expect(RouteConstants.calendarScreen, '/calendar-screen');
expect(RouteConstants.supportScreen, '/support-screen');
expect(RouteConstants.photoViewScreen, '/photo-view-screen');
expect(RouteConstants.chatScreen, '/chat-screen');
expect(RouteConstants.emptyChatScreen, '/empty-chat-screen');
expect(RouteConstants.glossaryScreen, '/glossary-screen');
expect(RouteConstants.blockedUsersScreen, '/blocked-users-screen');
expect(RouteConstants.notificationsSettingsScreen, '/notifications-settings-screen');
});
test('has all finance routes defined', () {
expect(RouteConstants.userFinanceScreen, '/user-finance-screen');
expect(RouteConstants.cardSettingsScreen, '/card-settings-screen');
expect(RouteConstants.orderInfoScreen, '/order-info-screen');
expect(RouteConstants.paymentScreen, '/payment-screen');
});
test('has all content creation routes defined', () {
expect(RouteConstants.recipeCreateScreen, '/recipe-create-screen');
expect(RouteConstants.createDietScreen, '/create-diet-screen');
expect(RouteConstants.dietPreviewScreen, '/diet-preview-screen');
expect(RouteConstants.createPlanScreen, '/create-plan-screen');
expect(RouteConstants.myDietsScreen, '/my-diets-screen');
expect(RouteConstants.myPlansScreen, '/my-plans-screen');
expect(RouteConstants.myRecipesScreen, '/my-recipes-screen');
});
test('has all user details routes defined', () {
expect(RouteConstants.userDetailsScreen, '/user-details-screen');
expect(RouteConstants.userDiaryScreen, '/user-diary-screen');
});
test('has dev tools route defined', () {
expect(RouteConstants.devToolsScreen, '/dev-tools-screen');
});
test('has all archive routes defined', () {
expect(RouteConstants.consultationsArchiveScreen, '/consultations-archive-screen');
expect(RouteConstants.consultantFinancesScreen, '/consultant-finances-screen');
expect(RouteConstants.userPurchasesScreen, '/user-purchases-screen');
});
});
}

View File

@@ -1,41 +1,55 @@
import 'package:eatvio_app/core/routing/settings_routes.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
void main() {
group('SettingsRoutes', () {
test('createRoutes возвращает список маршрутов', () {
final routes = SettingsRoutes.createRoutes();
expect(routes, isNotEmpty);
expect(routes, isA<List<RouteBase>>());
});
test('createRoutes содержит маршрут settingsScreen', () {
final routes = SettingsRoutes.createRoutes();
final settingsRoute = routes.first as GoRoute;
expect(settingsRoute.path, '/settings-screen');
});
test('createRoutes содержит вложенные маршруты', () {
final routes = SettingsRoutes.createRoutes();
final settingsRoute = routes.first as GoRoute;
expect(settingsRoute.routes, isNotEmpty);
});
});
}
import 'package:eatvio_app/core/routing/settings_routes.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
void main() {
group('SettingsRoutes', () {
test('createRoutes возвращает список маршрутов', () {
final routes = SettingsRoutes.createRoutes();
expect(routes, isNotEmpty);
expect(routes, isA<List<RouteBase>>());
});
test('createRoutes содержит маршрут settingsScreen', () {
final routes = SettingsRoutes.createRoutes();
final settingsRoute = routes.first as GoRoute;
expect(settingsRoute.path, '/settings-screen');
});
test('createRoutes содержит вложенные маршруты', () {
final routes = SettingsRoutes.createRoutes();
final settingsRoute = routes.first as GoRoute;
expect(settingsRoute.routes, isNotEmpty);
});
});
}

View File

@@ -3,35 +3,55 @@ import 'package:flutter_test/flutter_test.dart';
void main() {
group('ImageConstant', () {
test('has correct image path', () {
test('imagePath is correct', () {
expect(ImageConstant.imagePath, 'assets/images');
});
test('has common images defined', () {
expect(ImageConstant.imgArrowLeft, isA<String>());
expect(ImageConstant.imgLogoEatvio, isA<String>());
expect(ImageConstant.imgFavorite, isA<String>());
expect(ImageConstant.imageNotFound, isA<String>());
test('common images are defined', () {
expect(ImageConstant.imgArrowLeft, isNotEmpty);
expect(ImageConstant.imgLogoEatvio, isNotEmpty);
expect(ImageConstant.imgFavorite, isNotEmpty);
expect(ImageConstant.imageNotFound, isNotEmpty);
expect(ImageConstant.imaGlass, isNotEmpty);
});
test('image paths contain correct base path', () {
expect(ImageConstant.imgArrowLeft, contains('assets/images'));
expect(ImageConstant.imgLogoEatvio, contains('assets/images'));
expect(ImageConstant.imgFavorite, contains('assets/images'));
test('settings images are defined', () {
expect(ImageConstant.imgClose, isNotEmpty);
expect(ImageConstant.imgOverflowmenu, isNotEmpty);
});
test('imageNotFound points to correct file', () {
expect(ImageConstant.imageNotFound, 'assets/images/image_not_found.png');
test('blog images are defined', () {
expect(ImageConstant.imgRectangle14, isNotEmpty);
expect(ImageConstant.imgRectangle14220x341, isNotEmpty);
});
test('has settings images defined', () {
expect(ImageConstant.imgClose, isA<String>());
expect(ImageConstant.imgOverflowmenu, isA<String>());
});
test('all image paths contain imagePath prefix or assets path', () {
final allImages = [
ImageConstant.img2456927096976,
ImageConstant.imgArrowLeft,
ImageConstant.imgLogoEatvio,
ImageConstant.imgFavorite,
ImageConstant.imageNotFound,
ImageConstant.imaGlass,
ImageConstant.imgClose,
ImageConstant.imgOverflowmenu,
ImageConstant.imgRectangle14,
ImageConstant.imgRectangle14220x341,
ImageConstant.imgCrop,
ImageConstant.imgRectangle14220x375,
ImageConstant.imgVector,
ImageConstant.imgEllipse132x32,
ImageConstant.imgLike6,
ImageConstant.imgLike5,
ImageConstant.imgEllipse11,
ImageConstant.imgCloseBlueGray900,
ImageConstant.imgSend1,
];
test('has blog images defined', () {
expect(ImageConstant.imgRectangle14, isA<String>());
expect(ImageConstant.imgRectangle14220x341, isA<String>());
for (final image in allImages) {
expect(image, isNotEmpty);
expect(image.contains('assets/') || image.contains(ImageConstant.imagePath), isTrue);
}
});
});
}

View File

@@ -1,35 +1,49 @@
import 'package:eatvio_app/core/views/cubit/views_state.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ViewsCubitState', () {
test('creates initial state', () {
final state = ViewsCubitState();
expect(state, isA<ViewsCubitState>());
});
test('can create multiple instances', () {
final state1 = ViewsCubitState();
final state2 = ViewsCubitState();
expect(state1, isA<ViewsCubitState>());
expect(state2, isA<ViewsCubitState>());
});
});
}
import 'package:eatvio_app/core/views/cubit/views_state.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ViewsCubitState', () {
test('creates initial state', () {
final state = ViewsCubitState();
expect(state, isA<ViewsCubitState>());
});
test('can create multiple instances', () {
final state1 = ViewsCubitState();
final state2 = ViewsCubitState();
expect(state1, isA<ViewsCubitState>());
expect(state2, isA<ViewsCubitState>());
});
});
}

View File

@@ -0,0 +1,90 @@
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/websockets/ws_manager.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'ws_manager_test.mocks.dart';
@GenerateNiceMocks([MockSpec<WsService>()])
void main() {
group('WsManager', () {
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'));
});
test('initialize returns early when client is not null', () async {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
await manager.initialize();
expect(manager, isNotNull);
});
test('newMessagesStream returns null when channel is null', () {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
expect(manager.newMessagesStream, isNull);
});
test('newNotificationsStream returns null when channel is null', () {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
expect(manager.newNotificationsStream, isNull);
});
test('directChatStream returns null when channel is null', () {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
expect(manager.directChatStream, isNull);
});
test('clientTypingStream returns null when channel is null', () {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
expect(manager.clientTypingStream, isNull);
});
test('subscribeDirectChatChannel does nothing when channel is null', () {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
// Не должно быть исключений
manager.subscribeDirectChatChannel();
});
test('unsubscribeDirectChatChannel does nothing when channel is null', () {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
// Не должно быть исключений
manager.unsubscribeDirectChatChannel();
});
test('clear sets client and userId to null', () async {
final wsService = MockWsService();
final manager = WsManager(wsService: wsService);
manager.userId = 1;
await manager.clear();
expect(manager.client, isNull);
expect(manager.userId, isNull);
});
});
}

View File

@@ -1,71 +1,31 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/add_item_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('AddItemButton', () {
testWidgets('displays add button', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: AddItemButton(),
),
),
);
expect(find.byType(AddItemButton), findsOneWidget);
expect(find.byIcon(Icons.add_rounded), findsOneWidget);
});
testWidgets('calls onTap when provided', (tester) async {
bool wasCalled = false;
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: AddItemButton(
onTap: () {
wasCalled = true;
},
),
),
),
);
await tester.tap(find.byType(AddItemButton));
await tester.pump();
expect(wasCalled, isTrue);
});
testWidgets('has circular shape', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: AddItemButton(),
),
),
);
expect(find.byType(AddItemButton), findsOneWidget);
});
});
}
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/add_item_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
void main() {
group('AddItemButton smoke', () {
Widget wrap(Widget child) {
return MaterialApp.router(
theme: ThemeHelper().themeData(),
routerConfig: GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => Scaffold(body: child),
),
GoRoute(path: '/blog-post-create', name: 'blogPostCreateScreen', builder: (context, state) => const SizedBox.shrink()),
],
),
);
}
testWidgets('renders add item button', (tester) async {
await tester.pumpWidget(wrap(const AddItemButton()));
await tester.pump();
expect(find.byType(AddItemButton), findsOneWidget);
});
});
}

View File

@@ -1,123 +1,31 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/animated_option_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('AnimatedOptionButton', () {
testWidgets('displays animated option button', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: AnimatedOptionButton(
text: 'Option 1',
isSelected: false,
onTap: () {},
),
),
),
);
expect(find.byType(AnimatedOptionButton), findsOneWidget);
expect(find.text('Option 1'), findsOneWidget);
});
testWidgets('displays selected state', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: AnimatedOptionButton(
text: 'Option 1',
isSelected: true,
onTap: () {},
),
),
),
);
expect(find.byType(AnimatedOptionButton), findsOneWidget);
expect(find.text('Option 1'), findsOneWidget);
});
testWidgets('calls onTap when tapped', (tester) async {
bool tapped = false;
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: AnimatedOptionButton(
text: 'Option 1',
isSelected: false,
onTap: () {
tapped = true;
},
),
),
),
);
await tester.tap(find.byType(AnimatedOptionButton));
await tester.pump();
expect(tapped, true);
});
testWidgets('accepts custom text style', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: AnimatedOptionButton(
text: 'Option 1',
isSelected: false,
onTap: () {},
textStyle: const TextStyle(fontSize: 20),
),
),
),
);
expect(find.byType(AnimatedOptionButton), findsOneWidget);
});
testWidgets('animates between selected and unselected states', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: StatefulBuilder(
builder: (context, setState) {
return Scaffold(
body: AnimatedOptionButton(
text: 'Option 1',
isSelected: false,
onTap: () {
setState(() {});
},
),
);
},
),
),
);
expect(find.byType(AnimatedOptionButton), findsOneWidget);
});
});
}
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/animated_option_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('AnimatedOptionButton smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
testWidgets('renders animated option button', (tester) async {
await tester.pumpWidget(wrap(AnimatedOptionButton(text: 'Option 1', isSelected: false, onTap: () {})));
await tester.pump();
expect(find.byType(AnimatedOptionButton), findsOneWidget);
expect(find.text('Option 1'), findsOneWidget);
});
testWidgets('renders animated option button as selected', (tester) async {
await tester.pumpWidget(wrap(AnimatedOptionButton(text: 'Option 2', isSelected: true, onTap: () {})));
await tester.pump();
expect(find.byType(AnimatedOptionButton), findsOneWidget);
expect(find.text('Option 2'), findsOneWidget);
});
});
}

View File

@@ -1,60 +1,74 @@
import 'package:eatvio_app/core/widgets/base_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('BaseButton', () {
testWidgets('renders as SizedBox.shrink', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BaseButton(
text: 'Test Button',
onPressed: () {},
),
),
),
);
expect(find.byType(BaseButton), findsOneWidget);
expect(find.byType(SizedBox), findsWidgets);
});
testWidgets('accepts all parameters', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BaseButton(
text: 'Test Button',
onPressed: () {},
buttonStyle: const ButtonStyle(),
buttonTextStyle: const TextStyle(),
isDisabled: false,
height: 50.0,
width: 100.0,
margin: const EdgeInsets.all(10),
alignment: Alignment.center,
),
),
),
);
expect(find.byType(BaseButton), findsOneWidget);
});
});
}
import 'package:eatvio_app/core/widgets/base_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('BaseButton', () {
testWidgets('renders as SizedBox.shrink', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BaseButton(
text: 'Test Button',
onPressed: () {},
),
),
),
);
expect(find.byType(BaseButton), findsOneWidget);
expect(find.byType(SizedBox), findsWidgets);
});
testWidgets('accepts all parameters', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BaseButton(
text: 'Test Button',
onPressed: () {},
buttonStyle: const ButtonStyle(),
buttonTextStyle: const TextStyle(),
isDisabled: false,
height: 50.0,
width: 100.0,
margin: const EdgeInsets.all(10),
alignment: Alignment.center,
),
),
),
);
expect(find.byType(BaseButton), findsOneWidget);
});
});
}

View File

@@ -4,97 +4,26 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('BookmarkButton', () {
testWidgets('displays bookmark border when not favorite', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: BookmarkButton(
onTap: () {},
color: Colors.white,
isFavorite: false,
width: 40,
height: 40,
size: 24,
),
),
),
group('BookmarkButton smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
expect(find.byType(BookmarkButton), findsOneWidget);
expect(find.byIcon(Icons.bookmark_border_rounded), findsOneWidget);
});
testWidgets('displays filled bookmark when favorite', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: BookmarkButton(
onTap: () {},
color: Colors.white,
isFavorite: true,
width: 40,
height: 40,
size: 24,
),
),
),
);
expect(find.byType(BookmarkButton), findsOneWidget);
expect(find.byIcon(Icons.bookmark), findsOneWidget);
});
testWidgets('calls onTap when tapped', (tester) async {
bool wasCalled = false;
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: BookmarkButton(
onTap: () {
wasCalled = true;
},
color: Colors.white,
isFavorite: false,
width: 40,
height: 40,
size: 24,
),
),
),
);
await tester.tap(find.byType(BookmarkButton));
testWidgets('renders bookmark button', (tester) async {
await tester.pumpWidget(wrap(BookmarkButton(onTap: () {}, color: Colors.white, isFavorite: false, width: 40, height: 40, size: 20)));
await tester.pump();
expect(wasCalled, isTrue);
expect(find.byType(BookmarkButton), findsOneWidget);
});
testWidgets('accepts custom shape and borderRadius', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: BookmarkButton(
onTap: () {},
color: Colors.white,
isFavorite: false,
width: 40,
height: 40,
size: 24,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(8),
),
),
),
);
await tester.pumpAndSettle();
testWidgets('renders bookmark button as favorite', (tester) async {
await tester.pumpWidget(wrap(BookmarkButton(onTap: () {}, color: Colors.white, isFavorite: true, width: 40, height: 40, size: 20)));
await tester.pump();
expect(find.byType(BookmarkButton), findsOneWidget);
});
});
}

View File

@@ -1,158 +1,49 @@
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_action_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('BottomSheetActionWidget', () {
testWidgets('displays text and icon correctly', (WidgetTester tester) async {
bool tapped = false;
group('BottomSheetActionWidget smoke', () {
setUpAll(() async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
});
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: BottomSheetActionWidget(
text: 'Test Action',
iconPath: 'assets/icons/edit_paint.svg',
onTap: () {
tapped = true;
},
),
),
),
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: Scaffold(body: Builder(builder: (context) => child)),
);
}
testWidgets('renders bottom sheet action widget', (tester) async {
await tester.pumpWidget(wrap(BottomSheetActionWidget(text: 'Test Action', iconPath: 'assets/icons/ok.svg', onTap: () {})));
await tester.pumpAndSettle();
expect(find.byType(BottomSheetActionWidget), findsOneWidget);
expect(find.text('Test Action'), findsOneWidget);
expect(tapped, false);
await tester.tap(find.text('Test Action'));
await tester.pumpAndSettle();
expect(tapped, true);
});
testWidgets('pops navigator when needToPop is true', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: Builder(
builder: (context) => ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => Scaffold(
body: BottomSheetActionWidget(
text: 'Pop Action',
iconPath: 'assets/icons/edit_paint.svg',
onTap: () {},
),
),
),
);
},
child: const Text('Open'),
),
),
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
expect(find.text('Pop Action'), findsOneWidget);
await tester.tap(find.text('Pop Action'));
await tester.pumpAndSettle();
expect(find.text('Pop Action'), findsNothing);
});
testWidgets('does not pop navigator when needToPop is false', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: Builder(
builder: (context) => ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => Scaffold(
body: BottomSheetActionWidget(
text: 'No Pop Action',
iconPath: 'assets/icons/edit_paint.svg',
onTap: () {},
needToPop: false,
),
),
),
);
},
child: const Text('Open'),
),
),
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
expect(find.text('No Pop Action'), findsOneWidget);
await tester.tap(find.text('No Pop Action'));
await tester.pumpAndSettle();
expect(find.text('No Pop Action'), findsOneWidget);
});
testWidgets('displays with custom color', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: BottomSheetActionWidget(
text: 'Colored Action',
iconPath: 'assets/icons/edit_paint.svg',
onTap: () {},
color: Colors.red,
),
),
),
);
expect(find.text('Colored Action'), findsOneWidget);
});
testWidgets('calls onTap callback', (WidgetTester tester) async {
int tapCount = 0;
testWidgets('renders bottom sheet action widget with needToPop false', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: BottomSheetActionWidget(
text: 'Tap Me',
iconPath: 'assets/icons/edit_paint.svg',
onTap: () {
tapCount++;
},
),
),
),
wrap(BottomSheetActionWidget(text: 'Test Action', iconPath: 'assets/icons/ok.svg', onTap: () {}, needToPop: false)),
);
expect(tapCount, 0);
await tester.tap(find.text('Tap Me'));
await tester.pumpAndSettle();
expect(tapCount, 1);
expect(find.byType(BottomSheetActionWidget), findsOneWidget);
expect(find.text('Test Action'), findsOneWidget);
});
});
}

View File

@@ -4,48 +4,19 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('BottomSheetHandlerBar', () {
testWidgets('displays handler bar', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: BottomSheetHandlerBar(),
),
),
group('BottomSheetHandlerBar smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
testWidgets('renders bottom sheet handler bar', (tester) async {
await tester.pumpWidget(wrap(const BottomSheetHandlerBar()));
await tester.pump();
expect(find.byType(BottomSheetHandlerBar), findsOneWidget);
expect(find.byType(Container), findsOneWidget);
});
testWidgets('has correct dimensions', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: BottomSheetHandlerBar(),
),
),
);
final container = tester.widget<Container>(find.byType(Container).first);
expect(container.constraints, isNotNull);
});
testWidgets('has correct styling', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: BottomSheetHandlerBar(),
),
),
);
final container = tester.widget<Container>(find.byType(Container).first);
expect(container.decoration, isNotNull);
expect(container.decoration, isA<BoxDecoration>());
});
});
}

View File

@@ -1,142 +1,156 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('showBottomSheetWithBlur', () {
testWidgets('shows bottom sheet with blur', (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'),
);
},
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 without blur when hasBlur is 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'),
hasBlur: 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('accepts custom height', (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'),
height: 300.0,
);
},
child: const Text('Show Bottom Sheet'),
),
);
},
),
),
);
await tester.tap(find.text('Show Bottom Sheet'));
await tester.pumpAndSettle();
expect(find.text('Test Content'), findsOneWidget);
});
testWidgets('can be dismissed', (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'),
);
},
child: const Text('Show Bottom Sheet'),
),
);
},
),
),
);
await tester.tap(find.text('Show Bottom Sheet'));
await tester.pumpAndSettle();
expect(find.text('Test Content'), findsOneWidget);
await tester.tapAt(const Offset(0, 0));
await tester.pumpAndSettle();
expect(find.text('Test Content'), findsNothing);
});
});
}
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('showBottomSheetWithBlur', () {
testWidgets('shows bottom sheet with blur', (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'),
);
},
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 without blur when hasBlur is 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'),
hasBlur: 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('accepts custom height', (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'),
height: 300.0,
);
},
child: const Text('Show Bottom Sheet'),
),
);
},
),
),
);
await tester.tap(find.text('Show Bottom Sheet'));
await tester.pumpAndSettle();
expect(find.text('Test Content'), findsOneWidget);
});
testWidgets('can be dismissed', (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'),
);
},
child: const Text('Show Bottom Sheet'),
),
);
},
),
),
);
await tester.tap(find.text('Show Bottom Sheet'));
await tester.pumpAndSettle();
expect(find.text('Test Content'), findsOneWidget);
await tester.tapAt(const Offset(0, 0));
await tester.pumpAndSettle();
expect(find.text('Test Content'), findsNothing);
});
});
}

View File

@@ -1,103 +1,31 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/cpfc_container.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('CpfcContainer', () {
Widget createTestWidget({
required double calories,
required double proteins,
required double fats,
required double carbohydrates,
}) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru'), Locale('en')],
locale: const Locale('ru'),
home: Scaffold(
body: CpfcContainer(
calories: calories,
proteins: proteins,
fats: fats,
carbohydrates: carbohydrates,
),
),
);
}
testWidgets('displays calories, proteins, fats, and carbohydrates', (tester) async {
await tester.pumpWidget(
createTestWidget(
calories: 2000.0,
proteins: 150.0,
fats: 65.0,
carbohydrates: 250.0,
),
);
await tester.pumpAndSettle();
expect(find.byType(CpfcContainer), findsOneWidget);
expect(find.text('2000.0'), findsOneWidget);
expect(find.text('150.0'), findsOneWidget);
expect(find.text('65.0'), findsOneWidget);
expect(find.text('250.0'), findsOneWidget);
});
testWidgets('displays zero values', (tester) async {
await tester.pumpWidget(
createTestWidget(
calories: 0.0,
proteins: 0.0,
fats: 0.0,
carbohydrates: 0.0,
),
);
await tester.pumpAndSettle();
expect(find.byType(CpfcContainer), findsOneWidget);
expect(find.text('0.0'), findsNWidgets(4));
});
testWidgets('displays decimal values correctly', (tester) async {
await tester.pumpWidget(
createTestWidget(
calories: 1234.567,
proteins: 98.765,
fats: 45.321,
carbohydrates: 234.567,
),
);
await tester.pumpAndSettle();
expect(find.byType(CpfcContainer), findsOneWidget);
expect(find.text('1234.6'), findsOneWidget);
expect(find.text('98.8'), findsOneWidget);
expect(find.text('45.3'), findsOneWidget);
expect(find.text('234.6'), findsOneWidget);
});
});
}
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/cpfc_container.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('CpfcContainer smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: Scaffold(body: child),
);
}
testWidgets('renders cpfc container', (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.byType(CpfcContainer), findsOneWidget);
});
});
}

View File

@@ -4,33 +4,19 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('CustomDivider', () {
testWidgets('displays divider', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: CustomDivider(),
),
),
group('CustomDivider smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
testWidgets('renders custom divider', (tester) async {
await tester.pumpWidget(wrap(const CustomDivider()));
await tester.pump();
expect(find.byType(CustomDivider), findsOneWidget);
expect(find.byType(Divider), findsOneWidget);
});
testWidgets('has correct styling', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: CustomDivider(),
),
),
);
final divider = tester.widget<Divider>(find.byType(Divider));
expect(divider.height, 1);
});
});
}

View File

@@ -4,101 +4,30 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('CustomDropDownButton', () {
testWidgets('displays dropdown button', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: Form(
child: CustomDropDownButton<String>(
items: const [
DropdownMenuItem(value: 'option1', child: Text('Option 1')),
DropdownMenuItem(value: 'option2', child: Text('Option 2')),
],
value: 'option1',
onChanged: (value) {},
),
),
),
),
group('CustomDropDownButton smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
await tester.pumpAndSettle();
}
expect(find.byType(DropdownButtonFormField<String>), findsOneWidget);
testWidgets('renders custom drop down button', (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'), onChanged: null)));
await tester.pump();
expect(find.byType(CustomDropDownButton<String>), findsOneWidget);
});
testWidgets('displays hint when value is null', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: Form(
child: CustomDropDownButton<String>(
items: const [
DropdownMenuItem(value: 'option1', child: Text('Option 1')),
DropdownMenuItem(value: 'option2', child: Text('Option 2')),
],
hint: const Text('Select option'),
onChanged: (value) {},
),
),
),
),
);
await tester.pumpAndSettle();
testWidgets('renders custom drop down button with value', (tester) async {
const items = [DropdownMenuItem(value: 'option1', child: Text('Option 1')), DropdownMenuItem(value: 'option2', child: Text('Option 2'))];
expect(find.byType(DropdownButtonFormField<String>), findsOneWidget);
expect(find.text('Select option'), findsOneWidget);
});
await tester.pumpWidget(wrap(const CustomDropDownButton<String>(items: items, value: 'option1', hint: Text('Select option'), onChanged: null)));
await tester.pump();
testWidgets('applies error styling when hasError is true', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: Form(
child: CustomDropDownButton<String>(
items: const [
DropdownMenuItem(value: 'option1', child: Text('Option 1')),
DropdownMenuItem(value: 'option2', child: Text('Option 2')),
],
value: 'option1',
hasError: true,
onChanged: (value) {},
),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(DropdownButtonFormField<String>), findsOneWidget);
});
testWidgets('expands when isExpanded is true', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: Form(
child: CustomDropDownButton<String>(
items: const [
DropdownMenuItem(value: 'option1', child: Text('Option 1')),
DropdownMenuItem(value: 'option2', child: Text('Option 2')),
],
value: 'option1',
isExpanded: true,
onChanged: (value) {},
),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(DropdownButtonFormField<String>), findsOneWidget);
expect(find.byType(CustomDropDownButton<String>), findsOneWidget);
});
});
}

View File

@@ -5,134 +5,32 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('CustomDropDown', () {
const items = [
SelectionPopupModel(id: 1, title: 'Option 1'),
SelectionPopupModel(id: 2, title: 'Option 2'),
];
testWidgets('displays dropdown with items', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: Form(
child: CustomDropDown(
items: items,
value: items[0],
onChanged: (value) {},
),
),
),
),
group('CustomDropDown smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
await tester.pumpAndSettle();
}
expect(find.byType(DropdownButtonFormField<SelectionPopupModel>), findsOneWidget);
testWidgets('renders custom drop down', (tester) async {
const items = [SelectionPopupModel(id: 1, title: 'Option 1'), SelectionPopupModel(id: 2, title: 'Option 2')];
const value = SelectionPopupModel(id: 1, title: 'Option 1');
await tester.pumpWidget(wrap(const CustomDropDown(items: items, value: value, hintText: 'Select option')));
await tester.pump();
expect(find.byType(CustomDropDown), findsOneWidget);
});
testWidgets('displays hint when value is null', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: Form(
child: CustomDropDown(
items: items,
hintText: 'Select option',
onChanged: (value) {},
),
),
),
),
);
await tester.pumpAndSettle();
testWidgets('renders custom drop down without value', (tester) async {
const items = [SelectionPopupModel(id: 1, title: 'Option 1'), SelectionPopupModel(id: 2, title: 'Option 2')];
expect(find.byType(DropdownButtonFormField<SelectionPopupModel>), findsOneWidget);
expect(find.text('Select option'), findsOneWidget);
});
await tester.pumpWidget(wrap(const CustomDropDown(items: items, hintText: 'Select option')));
await tester.pump();
testWidgets('displays selected value', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: Form(
child: CustomDropDown(
items: items,
value: items[1],
onChanged: (value) {},
),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(DropdownButtonFormField<SelectionPopupModel>), findsOneWidget);
});
testWidgets('applies custom alignment', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: Form(
child: CustomDropDown(
items: items,
alignment: Alignment.centerLeft,
onChanged: (value) {},
),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(DropdownButtonFormField<SelectionPopupModel>), findsOneWidget);
});
testWidgets('applies custom width', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: Form(
child: CustomDropDown(
items: items,
width: 200,
onChanged: (value) {},
),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(DropdownButtonFormField<SelectionPopupModel>), findsOneWidget);
});
testWidgets('calls onChanged when value changes', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: Form(
child: CustomDropDown(
items: items,
value: items[0],
onChanged: (value) {
// Value changed
},
),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(DropdownButtonFormField<SelectionPopupModel>), findsOneWidget);
expect(find.byType(CustomDropDown), findsOneWidget);
});
});
}

View File

@@ -4,156 +4,35 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('CustomOutlinedButton', () {
testWidgets('displays button with text', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomOutlinedButton(
text: 'Test Button',
onPressed: () {},
),
),
),
group('CustomOutlinedButton smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
testWidgets('renders custom outlined button', (tester) async {
await tester.pumpWidget(wrap(const CustomOutlinedButton(text: 'Test Button')));
await tester.pump();
expect(find.byType(CustomOutlinedButton), findsOneWidget);
expect(find.text('Test Button'), findsOneWidget);
});
testWidgets('calls onPressed when tapped', (tester) async {
bool wasCalled = false;
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomOutlinedButton(
text: 'Test Button',
onPressed: () {
wasCalled = true;
},
),
),
),
);
await tester.tap(find.byType(OutlinedButton));
testWidgets('renders custom outlined button with left icon', (tester) async {
await tester.pumpWidget(wrap(const CustomOutlinedButton(text: 'Test Button', leftIcon: Icon(Icons.add))));
await tester.pump();
expect(wasCalled, isTrue);
});
testWidgets('disables button when isDisabled is true', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomOutlinedButton(
text: 'Test Button',
isDisabled: true,
onPressed: () {},
),
),
),
);
expect(find.byType(CustomOutlinedButton), findsOneWidget);
final button = tester.widget<OutlinedButton>(find.byType(OutlinedButton));
expect(button.onPressed, isNull);
});
testWidgets('applies custom height', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomOutlinedButton(
text: 'Test Button',
height: 60,
onPressed: () {},
),
),
),
);
expect(find.byType(CustomOutlinedButton), findsOneWidget);
});
testWidgets('applies custom width', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomOutlinedButton(
text: 'Test Button',
width: 200,
onPressed: () {},
),
),
),
);
expect(find.byType(CustomOutlinedButton), findsOneWidget);
});
testWidgets('displays left icon when provided', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomOutlinedButton(
text: 'Test Button',
leftIcon: const Icon(Icons.add),
buttonStyle: OutlinedButton.styleFrom(),
onPressed: () {},
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(CustomOutlinedButton), findsOneWidget);
expect(find.byIcon(Icons.add), findsOneWidget);
});
testWidgets('displays right icon when provided', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomOutlinedButton(
text: 'Test Button',
rightIcon: const Icon(Icons.arrow_forward),
buttonStyle: OutlinedButton.styleFrom(),
onPressed: () {},
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(CustomOutlinedButton), findsOneWidget);
expect(find.byIcon(Icons.arrow_forward), findsOneWidget);
});
testWidgets('applies custom alignment', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomOutlinedButton(
text: 'Test Button',
alignment: Alignment.centerLeft,
buttonStyle: OutlinedButton.styleFrom(),
onPressed: () {},
),
),
),
);
testWidgets('renders disabled custom outlined button', (tester) async {
await tester.pumpWidget(wrap(const CustomOutlinedButton(text: 'Test Button', isDisabled: true)));
await tester.pump();
expect(find.byType(CustomOutlinedButton), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,90 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('CustomPopup 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(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Scaffold(body: child),
);
}
testWidgets('renders custom popup for author', (tester) async {
await tester.pumpWidget(wrap(const CustomPopup(isAuthor: true, path: 'test/path', userSlug: 'test-user')));
await tester.pump();
expect(find.byType(CustomPopup), findsOneWidget);
expect(find.byIcon(Icons.more_horiz_rounded), findsOneWidget);
});
testWidgets('renders custom popup for non-author', (tester) async {
await tester.pumpWidget(
wrap(const CustomPopup(isAuthor: false, path: 'test/path', userSlug: 'test-user', complainedContentType: ComplainedContent.posts)),
);
await tester.pump();
expect(find.byType(CustomPopup), findsOneWidget);
expect(find.byIcon(Icons.more_horiz_rounded), findsOneWidget);
});
testWidgets('renders custom popup for non-author with recipe', (tester) async {
await tester.pumpWidget(
wrap(const CustomPopup(isAuthor: false, path: 'test/path', userSlug: 'test-user', complainedContentType: ComplainedContent.recipe)),
);
await tester.pump();
expect(find.byType(CustomPopup), findsOneWidget);
expect(find.byIcon(Icons.more_horiz_rounded), findsOneWidget);
});
testWidgets('renders custom popup for non-author with diet', (tester) async {
await tester.pumpWidget(
wrap(const CustomPopup(isAuthor: false, path: 'test/path', userSlug: 'test-user', complainedContentType: ComplainedContent.diet)),
);
await tester.pump();
expect(find.byType(CustomPopup), findsOneWidget);
expect(find.byIcon(Icons.more_horiz_rounded), findsOneWidget);
});
testWidgets('renders custom popup for non-author with plan', (tester) async {
await tester.pumpWidget(
wrap(const CustomPopup(isAuthor: false, path: 'test/path', userSlug: 'test-user', complainedContentType: ComplainedContent.plan)),
);
await tester.pump();
expect(find.byType(CustomPopup), findsOneWidget);
expect(find.byIcon(Icons.more_horiz_rounded), findsOneWidget);
});
testWidgets('renders custom popup for author with posts', (tester) async {
await tester.pumpWidget(
wrap(const CustomPopup(isAuthor: true, path: 'test/path', userSlug: 'test-user', complainedContentType: ComplainedContent.posts)),
);
await tester.pump();
expect(find.byType(CustomPopup), findsOneWidget);
expect(find.byIcon(Icons.more_horiz_rounded), findsOneWidget);
});
});
}

View File

@@ -1,171 +1,160 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('CustomSearchBar', () {
testWidgets('displays custom search bar', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {},
),
),
),
);
expect(find.byType(CustomSearchBar), findsOneWidget);
expect(find.byType(TextFormField), findsOneWidget);
});
testWidgets('displays search icon when enableIcon is true', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {},
enableIcon: true,
),
),
),
);
expect(find.byIcon(Icons.search), findsOneWidget);
});
testWidgets('does not display search icon when enableIcon is false', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {},
enableIcon: false,
),
),
),
);
expect(find.byIcon(Icons.search), findsNothing);
});
testWidgets('calls onChanged when text is entered', (tester) async {
final controller = TextEditingController();
String? changedValue;
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {
changedValue = value;
},
),
),
),
);
await tester.enterText(find.byType(TextFormField), 'test');
await tester.pump();
expect(changedValue, 'test');
});
testWidgets('displays hint text', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {},
),
),
),
);
expect(find.text('Поиск'), findsOneWidget);
});
testWidgets('accepts custom width', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {},
width: 200.0,
),
),
),
);
expect(find.byType(CustomSearchBar), findsOneWidget);
});
testWidgets('accepts custom backgroundColor', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {},
backgroundColor: Colors.red,
),
),
),
);
expect(find.byType(CustomSearchBar), findsOneWidget);
});
testWidgets('accepts custom borderRadius', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {},
borderRadius: 20.0,
),
),
),
);
expect(find.byType(CustomSearchBar), findsOneWidget);
});
});
}
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('CustomSearchBar smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
testWidgets('renders custom search bar', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(wrap(CustomSearchBar(searchController: controller, onChanged: (_) {})));
await tester.pump();
expect(find.byType(CustomSearchBar), findsOneWidget);
expect(find.byType(TextFormField), findsOneWidget);
});
testWidgets('renders custom search bar with icon', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(wrap(CustomSearchBar(searchController: controller, onChanged: (_) {}, enableIcon: true)));
await tester.pump();
expect(find.byIcon(Icons.search), findsOneWidget);
});
testWidgets('renders custom search bar without icon', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(wrap(CustomSearchBar(searchController: controller, onChanged: (_) {}, enableIcon: false)));
await tester.pump();
expect(find.byIcon(Icons.search), findsNothing);
});
testWidgets('calls onChanged when text is entered', (tester) async {
final controller = TextEditingController();
String? changedValue;
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {
changedValue = value;
},
),
),
),
);
await tester.enterText(find.byType(TextFormField), 'test');
await tester.pump();
expect(changedValue, 'test');
});
testWidgets('displays hint text', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {},
),
),
),
);
expect(find.text('Поиск'), findsOneWidget);
});
testWidgets('accepts custom width', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {},
width: 200.0,
),
),
),
);
expect(find.byType(CustomSearchBar), findsOneWidget);
});
testWidgets('accepts custom backgroundColor', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {},
backgroundColor: Colors.red,
),
),
),
);
expect(find.byType(CustomSearchBar), findsOneWidget);
});
testWidgets('accepts custom borderRadius', (tester) async {
final controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomSearchBar(
searchController: controller,
onChanged: (value) {},
borderRadius: 20.0,
),
),
),
);
expect(find.byType(CustomSearchBar), findsOneWidget);
});
});
}

View File

@@ -5,159 +5,41 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('CustomTextFormField', () {
testWidgets('displays text form field', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: CustomTextFormField(),
),
),
group('CustomTextFormField smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
expect(find.byType(CustomTextFormField), findsOneWidget);
expect(find.byType(TextFormField), findsOneWidget);
});
testWidgets('displays hint text', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: CustomTextFormField(
hintText: 'Enter text',
),
),
),
);
expect(find.text('Enter text'), findsOneWidget);
});
testWidgets('displays initial value', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: CustomTextFormField(
initialValue: 'Initial text',
),
),
),
);
expect(find.text('Initial text'), findsOneWidget);
});
testWidgets('calls onChanged when text is entered', (tester) async {
String? changedValue;
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomTextFormField(
onChanged: (value) {
changedValue = value;
},
),
),
),
);
await tester.enterText(find.byType(TextFormField), 'test');
testWidgets('renders custom text form field', (tester) async {
await tester.pumpWidget(wrap(const CustomTextFormField(hintText: 'Enter text')));
await tester.pump();
expect(changedValue, 'test');
expect(find.byType(CustomTextFormField), findsOneWidget);
});
testWidgets('displays suffix text', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: CustomTextFormField(
suffixText: 'kg',
),
),
),
);
testWidgets('renders custom text form field with controller', (tester) async {
final controller = TextEditingController(text: 'Test text');
await tester.pumpWidget(wrap(CustomTextFormField(controller: controller, hintText: 'Enter text')));
await tester.pump();
expect(find.byType(CustomTextFormField), findsOneWidget);
expect(find.text('Test text'), findsOneWidget);
});
testWidgets('renders custom text form field with input formatters', (tester) async {
await tester.pumpWidget(wrap(CustomTextFormField(hintText: 'Enter numbers', inputFormatters: [FilteringTextInputFormatter.digitsOnly])));
await tester.pump();
expect(find.byType(CustomTextFormField), findsOneWidget);
});
testWidgets('displays error border when hasError is true', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: CustomTextFormField(
hasError: true,
),
),
),
);
expect(find.byType(CustomTextFormField), findsOneWidget);
});
testWidgets('is read-only when readOnly is true', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: CustomTextFormField(
readOnly: true,
initialValue: 'Read only text',
),
),
),
);
expect(find.byType(CustomTextFormField), findsOneWidget);
});
testWidgets('accepts custom keyboard type', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: CustomTextFormField(
keyboardType: TextInputType.number,
),
),
),
);
expect(find.byType(CustomTextFormField), findsOneWidget);
});
testWidgets('accepts input formatters', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: CustomTextFormField(
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
),
),
),
);
expect(find.byType(CustomTextFormField), findsOneWidget);
});
testWidgets('accepts max length', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: CustomTextFormField(
maxLength: 10,
),
),
),
);
testWidgets('renders custom text form field with obscure text', (tester) async {
await tester.pumpWidget(wrap(const CustomTextFormField(hintText: 'Enter password', obscureText: true)));
await tester.pump();
expect(find.byType(CustomTextFormField), findsOneWidget);
});

View File

@@ -6,58 +6,26 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('DataErrorWidget', () {
testWidgets('displays error message and reload button', (tester) async {
Future<void> onReload() async {
// Reload action
}
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru'), Locale('en')],
locale: const Locale('ru'),
home: Scaffold(
body: DataErrorWidget(onReload: onReload),
),
),
group('DataErrorWidget smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: Scaffold(body: child),
);
await tester.pumpAndSettle();
}
expect(find.byType(DataErrorWidget), findsOneWidget);
});
testWidgets('shows loader when reloading', (tester) async {
Future<void> onReload() async {
await Future.delayed(const Duration(milliseconds: 100));
}
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru'), Locale('en')],
locale: const Locale('ru'),
home: Scaffold(
body: DataErrorWidget(onReload: onReload),
),
),
);
await tester.pumpAndSettle();
testWidgets('renders data error widget', (tester) async {
await tester.pumpWidget(wrap(DataErrorWidget(onReload: () async {})));
await tester.pump();
expect(find.byType(DataErrorWidget), findsOneWidget);
});
});
}

View File

@@ -1,115 +1,35 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('FeedIconButton', () {
testWidgets('displays feed icon button with icon', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: FeedIconButton(
icon: Icons.favorite,
),
),
),
group('FeedIconButton smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
testWidgets('renders feed icon button with icon', (tester) async {
await tester.pumpWidget(wrap(FeedIconButton(icon: Icons.favorite, onTap: () {})));
await tester.pump();
expect(find.byType(FeedIconButton), findsOneWidget);
expect(find.byIcon(Icons.favorite), findsOneWidget);
});
testWidgets('displays feed icon button with svg icon', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: FeedIconButton(
svgIcon: SvgPicture.asset('assets/icons/filter2.svg'),
),
),
),
);
expect(find.byType(FeedIconButton), findsOneWidget);
});
testWidgets('displays feed icon button with text', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: FeedIconButton(
text: 'Like',
),
),
),
);
expect(find.byType(FeedIconButton), findsOneWidget);
expect(find.text('Like'), findsOneWidget);
});
testWidgets('calls onTap when tapped', (tester) async {
bool tapped = false;
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: FeedIconButton(
icon: Icons.favorite,
onTap: () {
tapped = true;
},
),
),
),
);
await tester.tap(find.byType(FeedIconButton));
testWidgets('renders feed icon button with text', (tester) async {
await tester.pumpWidget(wrap(FeedIconButton(text: 'Test', onTap: () {})));
await tester.pump();
expect(tapped, true);
expect(find.text('Test'), findsOneWidget);
});
testWidgets('displays icon and text together', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: FeedIconButton(
icon: Icons.favorite,
text: 'Like',
),
),
),
);
expect(find.byType(FeedIconButton), findsOneWidget);
expect(find.byIcon(Icons.favorite), findsOneWidget);
expect(find.text('Like'), findsOneWidget);
});
testWidgets('uses default onTap when onTap is null', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: FeedIconButton(
icon: Icons.favorite,
),
),
),
);
await tester.tap(find.byType(FeedIconButton));
testWidgets('renders feed icon button with icon and text', (tester) async {
await tester.pumpWidget(wrap(FeedIconButton(icon: Icons.star, text: 'Star', onTap: () {})));
await tester.pump();
expect(find.byType(FeedIconButton), findsOneWidget);
expect(find.byIcon(Icons.star), findsOneWidget);
expect(find.text('Star'), findsOneWidget);
});
});
}

View File

@@ -1,14 +1,17 @@
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/recipes_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('InfoRow', () {
Widget createTestWidget(Widget child) {
group('InfoRow smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
@@ -17,45 +20,80 @@ void main() {
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Scaffold(
body: child,
),
supportedLocales: const [Locale('ru')],
home: Portal(child: Scaffold(body: child)),
);
}
testWidgets('displays info row widget', (tester) async {
await tester.pumpWidget(
createTestWidget(
const InfoRow(
type: ProductType.diet,
),
testWidgets('renders info row for diet', (tester) async {
const diet = Diet(
id: 1,
slug: 'test-diet',
name: 'Test Diet',
image: '/test.jpg',
duration: 7,
state: DietState.active,
type: DietType(light: true, veg: false, carb: false, meat: false),
details: 'Test diet description',
dailySchedule: {},
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,
dailyNutrition: [],
);
await tester.pumpWidget(wrap(const InfoRow(type: ProductType.diet, diet: diet)));
await tester.pump();
expect(find.byType(InfoRow), findsOneWidget);
});
testWidgets('displays info row for plan type', (tester) async {
await tester.pumpWidget(
createTestWidget(
const InfoRow(
type: ProductType.plan,
),
testWidgets('renders info row for recipe', (tester) async {
const recipe = Recipe(
id: 1,
slug: 'test-recipe',
title: 'Test Recipe',
image: '/test.jpg',
state: 'active',
cookingTime: 30,
activeCookingTime: 15,
ingredients: [],
description: 'Test description',
steps: [],
videoUrl: null,
createdAt: null,
updatedAt: null,
type: 'main',
commentsCount: 5,
wasLikedByMe: false,
isFavorite: false,
calories: 200,
servings: 4,
likesCount: 10,
viewsCount: 100,
tags: [],
author: Author(
authorSlug: 'test-author',
authorName: 'Test Author',
authorProfileIcon: '/test.jpg',
authorIsMe: false,
isEntitySubscribedByMe: false,
authorPlaceInRating: null,
),
);
expect(find.byType(InfoRow), findsOneWidget);
});
testWidgets('displays info row for recipe type', (tester) async {
await tester.pumpWidget(
createTestWidget(
const InfoRow(
type: ProductType.recipe,
),
),
);
await tester.pumpWidget(wrap(const InfoRow(type: ProductType.recipe, recipe: recipe)));
await tester.pump();
expect(find.byType(InfoRow), findsOneWidget);
});

View File

@@ -1,64 +1,44 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/item_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ItemWidget', () {
testWidgets('displays text with icon', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: ItemWidget(text: 'Test item'),
),
),
);
expect(find.byType(ItemWidget), findsOneWidget);
expect(find.text('Test item'), findsOneWidget);
});
testWidgets('displays reversed layout when reversed is true', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: ItemWidget(text: 'Test item', reversed: true),
),
),
);
expect(find.byType(ItemWidget), findsOneWidget);
expect(find.text('Test item'), findsOneWidget);
});
testWidgets('applies custom icon size', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: ItemWidget(text: 'Test item', iconSize: 30),
),
),
);
expect(find.byType(ItemWidget), findsOneWidget);
});
});
}
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/item_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ItemWidget 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: Scaffold(body: child),
);
}
testWidgets('renders item widget', (tester) async {
await tester.pumpWidget(wrap(const ItemWidget(text: 'Test Item')));
await tester.pumpAndSettle();
expect(find.byType(ItemWidget), findsOneWidget);
expect(find.text('Test Item'), findsOneWidget);
});
testWidgets('renders item widget reversed', (tester) async {
await tester.pumpWidget(wrap(const ItemWidget(text: 'Test Item', reversed: true)));
await tester.pumpAndSettle();
expect(find.byType(ItemWidget), findsOneWidget);
expect(find.text('Test Item'), findsOneWidget);
});
});
}

View File

@@ -1,49 +1,22 @@
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Loader', () {
testWidgets('displays loader widget', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Loader(),
),
),
);
expect(find.byType(Loader), findsOneWidget);
expect(find.byType(Center), findsOneWidget);
});
testWidgets('has correct dimensions', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Loader(),
),
),
);
final sizedBox = tester.widget<SizedBox>(find.byType(SizedBox));
expect(sizedBox.height, 42);
expect(sizedBox.width, 42);
});
});
}
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Loader smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
testWidgets('renders loader', (tester) async {
await tester.pumpWidget(wrap(const Loader()));
await tester.pump();
expect(find.byType(Loader), findsOneWidget);
});
});
}

View File

@@ -1,103 +1,37 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('MainBackgroundBouble', () {
testWidgets('displays child widget', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: MainBackgroundBouble(
child: Text('Test'),
),
),
),
);
expect(find.byType(MainBackgroundBouble), findsOneWidget);
expect(find.text('Test'), findsOneWidget);
});
testWidgets('applies custom width', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: MainBackgroundBouble(
width: 200,
child: Text('Test'),
),
),
),
);
expect(find.byType(MainBackgroundBouble), findsOneWidget);
});
testWidgets('applies shadow when enableShadow is true', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: MainBackgroundBouble(
enableShadow: true,
child: Text('Test'),
),
),
),
);
expect(find.byType(MainBackgroundBouble), findsOneWidget);
});
testWidgets('applies error border when hasError is true', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: MainBackgroundBouble(
hasError: true,
child: Text('Test'),
),
),
),
);
expect(find.byType(MainBackgroundBouble), findsOneWidget);
});
testWidgets('applies custom padding', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const Scaffold(
body: MainBackgroundBouble(
padding: EdgeInsets.all(16),
child: Text('Test'),
),
),
),
);
expect(find.byType(MainBackgroundBouble), findsOneWidget);
});
});
}
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('MainBackgroundBouble smoke', () {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
testWidgets('renders main background bouble', (tester) async {
await tester.pumpWidget(wrap(const MainBackgroundBouble(child: Text('Test'))));
await tester.pump();
expect(find.byType(MainBackgroundBouble), findsOneWidget);
expect(find.text('Test'), findsOneWidget);
});
testWidgets('renders main background bouble with error', (tester) async {
await tester.pumpWidget(wrap(const MainBackgroundBouble(hasError: true, child: Text('Test'))));
await tester.pump();
expect(find.byType(MainBackgroundBouble), findsOneWidget);
});
testWidgets('renders main background bouble with shadow', (tester) async {
await tester.pumpWidget(wrap(const MainBackgroundBouble(enableShadow: true, child: Text('Test'))));
await tester.pump();
expect(find.byType(MainBackgroundBouble), findsOneWidget);
});
});
}

View File

@@ -1,83 +1,97 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/marketing_bottom_sheet.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('MarketingBottomSheet', () {
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('displays marketing bottom sheet', (tester) async {
await tester.pumpWidget(
createTestWidget(
const MarketingBottomSheet(),
),
);
expect(find.byType(MarketingBottomSheet), findsOneWidget);
});
testWidgets('shows marketing bottom sheet via function', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Builder(
builder: (context) {
return Scaffold(
body: ElevatedButton(
onPressed: () {
showMarketingBottomSheet(context);
},
child: const Text('Show'),
),
);
},
),
),
);
await tester.tap(find.text('Show'));
await tester.pumpAndSettle();
expect(find.byType(MarketingBottomSheet), findsOneWidget);
});
});
}
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/marketing_bottom_sheet.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('MarketingBottomSheet', () {
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('displays marketing bottom sheet', (tester) async {
await tester.pumpWidget(
createTestWidget(
const MarketingBottomSheet(),
),
);
expect(find.byType(MarketingBottomSheet), findsOneWidget);
});
testWidgets('shows marketing bottom sheet via function', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Builder(
builder: (context) {
return Scaffold(
body: ElevatedButton(
onPressed: () {
showMarketingBottomSheet(context);
},
child: const Text('Show'),
),
);
},
),
),
);
await tester.tap(find.text('Show'));
await tester.pumpAndSettle();
expect(find.byType(MarketingBottomSheet), findsOneWidget);
});
});
}

View File

@@ -1,72 +1,86 @@
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)));
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)));
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)));
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)));
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)));
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)));
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)));
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)));
expect(find.byType(OptionWidget), findsOneWidget);
});
});
}

View File

@@ -1,79 +1,63 @@
import 'package:eatvio_app/core/app_router.dart';
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/success_published_diet_bottom_sheet.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
void main() {
group('SuccessPublishedDietOrPlanBottomSheet', () {
Widget createTestWidget(Widget child) {
return MaterialApp(
group('SuccessPublishedDietOrPlanBottomSheet 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'));
AppRouter.navigatorKey = GlobalKey<NavigatorState>();
});
Widget wrap(Widget child) {
return MaterialApp.router(
theme: ThemeHelper().themeData(),
routerConfig: GoRouter(
navigatorKey: AppRouter.navigatorKey,
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => Scaffold(body: child),
),
],
),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Scaffold(
body: child,
),
supportedLocales: const [Locale('ru')],
);
}
testWidgets('displays success published diet bottom sheet', (tester) async {
await tester.pumpWidget(
createTestWidget(
const SuccessPublishedDietOrPlanBottomSheet(
title: 'Test Diet',
),
),
);
testWidgets('renders success published diet bottom sheet', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedDietOrPlanBottomSheet(title: 'Test Diet', isPlan: false)));
await tester.pump();
expect(find.byType(SuccessPublishedDietOrPlanBottomSheet), findsOneWidget);
expect(find.text('Test Diet'), findsOneWidget);
});
testWidgets('displays diet published text when isPlan is false', (tester) async {
await tester.pumpWidget(
createTestWidget(
const SuccessPublishedDietOrPlanBottomSheet(
title: 'Test Diet',
isPlan: false,
),
),
);
testWidgets('renders success published plan bottom sheet', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedDietOrPlanBottomSheet(title: 'Test Plan', isPlan: true)));
await tester.pump();
expect(find.text('опубликованна'), findsOneWidget);
});
testWidgets('displays plan published text when isPlan is true', (tester) async {
await tester.pumpWidget(
createTestWidget(
const SuccessPublishedDietOrPlanBottomSheet(
title: 'Test Plan',
isPlan: true,
),
),
);
expect(find.text('опубликован'), findsOneWidget);
});
testWidgets('displays plan title', (tester) async {
await tester.pumpWidget(
createTestWidget(
const SuccessPublishedDietOrPlanBottomSheet(
title: 'My Plan',
isPlan: true,
),
),
);
expect(find.text('My Plan'), findsOneWidget);
expect(find.byType(SuccessPublishedDietOrPlanBottomSheet), findsOneWidget);
});
});
}

View File

@@ -1,68 +1,56 @@
import 'package:eatvio_app/core/app_router.dart';
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/success_published_recipe_bottom_sheet.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
void main() {
group('SuccessPublishedRecipeBottomSheet', () {
Widget createTestWidget(Widget child) {
return MaterialApp(
group('SuccessPublishedRecipeBottomSheet 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'));
AppRouter.navigatorKey = GlobalKey<NavigatorState>();
});
Widget wrap(Widget child) {
return MaterialApp.router(
theme: ThemeHelper().themeData(),
routerConfig: GoRouter(
navigatorKey: AppRouter.navigatorKey,
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => Scaffold(body: child),
),
],
),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Scaffold(
body: child,
),
supportedLocales: const [Locale('ru')],
);
}
testWidgets('displays success published recipe bottom sheet', (tester) async {
await tester.pumpWidget(
createTestWidget(
const SuccessPublishedRecipeBottomSheet(
title: 'Test Recipe',
),
),
);
await tester.pumpAndSettle();
expect(find.byType(SuccessPublishedRecipeBottomSheet), findsOneWidget);
// The widget displays: '"Test Recipe" успешно опубликован'
expect(find.textContaining('Test Recipe'), findsOneWidget);
});
testWidgets('displays recipe title', (tester) async {
await tester.pumpWidget(
createTestWidget(
const SuccessPublishedRecipeBottomSheet(
title: 'My Recipe',
),
),
);
await tester.pumpAndSettle();
// The widget displays: '"My Recipe" успешно опубликован'
expect(find.textContaining('My Recipe'), findsOneWidget);
});
testWidgets('has ok button', (tester) async {
await tester.pumpWidget(
createTestWidget(
const SuccessPublishedRecipeBottomSheet(
title: 'Test Recipe',
),
),
);
testWidgets('renders success published recipe bottom sheet', (tester) async {
await tester.pumpWidget(wrap(const SuccessPublishedRecipeBottomSheet(title: 'Test Recipe')));
await tester.pump();
expect(find.byType(SuccessPublishedRecipeBottomSheet), findsOneWidget);
});
});
}

View File

@@ -1,19 +1,29 @@
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/prefs.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/profile/repositories/profile_repository.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../fixtures/doubles/auth_doubles.dart';
import '../../../../fixtures/doubles/profile_doubles.dart';
import '../../../../fixtures/fixture_reader.dart';
import 'auth_cubit_test.mocks.dart';
@GenerateNiceMocks([MockSpec<Prefs>(), MockSpec<FirebaseApi>(), MockSpec<ProfileRepository>()])
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('AuthCubit', () {
AuthCubit? authCubit;
late FixtureAuthRepository authRepository;
late FixtureProfileRepository profileRepository;
late MockPrefs mockPrefs;
late MockFirebaseApi mockFirebaseApi;
late Map<String, dynamic> credentialsFixture;
late TokenModel tokenFixture;
@@ -22,16 +32,17 @@ void main() {
tokenFixture = TokenModel.fromJson(FixtureReader.readAsMap('auth/token_response.json'));
credentialsFixture = FixtureReader.readAsMap('auth/login_credentials.json');
authRepository = FixtureAuthRepository(tokenFixture: tokenFixture);
profileRepository = FixtureProfileRepository();
mockPrefs = MockPrefs();
mockFirebaseApi = MockFirebaseApi();
SharedPreferences.setMockInitialValues({});
// Mock Fluttertoast plugin
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('PonnamKarthik/fluttertoast'),
(MethodCall methodCall) async {
return null;
},
);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(const MethodChannel('PonnamKarthik/fluttertoast'), (
MethodCall methodCall,
) async {
return null;
});
});
tearDown(() async {
@@ -40,47 +51,34 @@ void main() {
authCubit!.close();
authCubit = null;
}
// Clear mock handlers
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('PonnamKarthik/fluttertoast'),
null,
);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(const MethodChannel('PonnamKarthik/fluttertoast'), null);
});
test('начальное состояние - AuthState', () {
authCubit = AuthCubit(authRepository: authRepository);
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
expect(authCubit!.state, const AuthState());
});
test('getTokenByCredentials - успешная авторизация', () async {
authCubit = AuthCubit(authRepository: authRepository);
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
final result = await authCubit!.getTokenByCredentials(
credentialsFixture['email'] as String,
credentialsFixture['password'] as String,
);
final result = await authCubit!.getTokenByCredentials(credentialsFixture['email'] as String, credentialsFixture['password'] as String);
expect(result, true);
await Future.delayed(const Duration(milliseconds: 100));
expect(authCubit!.state, isA<AuthStateLoaded>());
final loadedState = authCubit!.state as AuthStateLoaded;
expect(loadedState.token, tokenFixture.token);
expect(
authRepository.credentialPayloads.single,
credentialsFixture.map((key, value) => MapEntry(key, value.toString())),
);
expect(authRepository.credentialPayloads.single, credentialsFixture.map((key, value) => MapEntry(key, value.toString())));
});
test('getTokenByCredentials - переход в состояние loading', () async {
authCubit = AuthCubit(authRepository: authRepository);
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
authRepository.responseDelay = const Duration(milliseconds: 50);
final future = authCubit!.getTokenByCredentials(
credentialsFixture['email'] as String,
credentialsFixture['password'] as String,
);
final future = authCubit!.getTokenByCredentials(credentialsFixture['email'] as String, credentialsFixture['password'] as String);
await Future.delayed(const Duration(milliseconds: 10));
expect(authCubit!.state, isA<AuthStateLoading>());
@@ -88,7 +86,7 @@ void main() {
});
test('getTokenByCredentials - обработка ошибки авторизации', () async {
authCubit = AuthCubit(authRepository: authRepository);
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
authRepository.error = Exception('Неверный адрес электронной почты или пароль');
final result = await authCubit!.getTokenByCredentials('wrong@example.com', 'wrongpassword');
@@ -98,14 +96,11 @@ void main() {
expect(authCubit!.state, isA<AuthStateError>());
final errorState = authCubit!.state as AuthStateError;
expect(errorState.errorMessage, contains('Exception'));
expect(
authRepository.credentialPayloads.single,
{'email': 'wrong@example.com', 'password': 'wrongpassword'},
);
expect(authRepository.credentialPayloads.single, {'email': 'wrong@example.com', 'password': 'wrongpassword'});
});
test('getTokenByCredentials - обработка различных типов ошибок', () async {
authCubit = AuthCubit(authRepository: authRepository);
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
authRepository.error = const FormatException('Invalid credentials format');
final result = await authCubit!.getTokenByCredentials('invalid', 'pass');
@@ -118,36 +113,26 @@ void main() {
});
test('getTokenByCredentials - проверка вызова с правильными параметрами', () async {
authCubit = AuthCubit(authRepository: authRepository);
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
await authCubit!.getTokenByCredentials('user@test.com', 'securePassword123');
expect(
authRepository.credentialPayloads.single,
{'email': 'user@test.com', 'password': 'securePassword123'},
);
expect(
authRepository.credentialPayloads.where((call) => call['email'] == 'other@test.com'),
isEmpty,
);
expect(authRepository.credentialPayloads.single, {'email': 'user@test.com', 'password': 'securePassword123'});
expect(authRepository.credentialPayloads.where((call) => call['email'] == 'other@test.com'), isEmpty);
});
test('getTokenByCredentials - множественные вызовы', () async {
authCubit = AuthCubit(authRepository: authRepository);
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
await authCubit!.getTokenByCredentials('first@test.com', 'password1');
await Future.delayed(const Duration(milliseconds: 100));
await authCubit!.getTokenByCredentials('second@test.com', 'password2');
expect(authRepository.credentialPayloads.length, 2);
expect(
authRepository.credentialPayloads,
[
{'email': 'first@test.com', 'password': 'password1'},
{'email': 'second@test.com', 'password': 'password2'},
],
);
expect(authRepository.credentialPayloads, [
{'email': 'first@test.com', 'password': 'password1'},
{'email': 'second@test.com', 'password': 'password2'},
]);
});
});
}

View File

@@ -1,5 +1,8 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
@@ -185,5 +188,221 @@ void main() {
final loadedState = oauthCubit!.state as OAuthStateLoaded;
expect(loadedState.token, tokenFixture.token);
});
test('signInViaSocialMedia - обработка ошибки валидации email/phone', () async {
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
final dioException = DioException(
requestOptions: RequestOptions(path: '/api/auth/oauth/signin'),
response: Response(
statusCode: 422,
data: {
'errors': {
'email': ['Некорректный формат email'],
},
},
requestOptions: RequestOptions(path: '/api/auth/oauth/signin'),
),
);
when(
mockAuthRepository.signInViaSocialMedia(
provider: anyNamed('provider'),
accessToken: anyNamed('accessToken'),
refreshToken: anyNamed('refreshToken'),
clientId: anyNamed('clientId'),
),
).thenThrow(dioException);
await oauthCubit!.signInViaSocialMedia(
provider: signInRequestFixture['provider'] as String,
accessToken: signInRequestFixture['access_token'] as String,
refreshToken: signInRequestFixture['refresh_token'] as String,
clientId: signInRequestFixture['client_id'] as String,
);
await Future.delayed(const Duration(milliseconds: 100));
expect(oauthCubit!.state, isA<OAuthStateError>());
final errorState = oauthCubit!.state as OAuthStateError;
expect(errorState.errorMessage, 'validation_error');
});
test('signUpViaSocialMedia - обработка ошибки валидации email', () async {
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
final dioException = DioException(
requestOptions: RequestOptions(path: '/api/auth/oauth/signup'),
response: Response(
statusCode: 422,
data: {
'errors': {
'email': ['Некорректный формат email'],
},
},
requestOptions: RequestOptions(path: '/api/auth/oauth/signup'),
),
);
when(
mockAuthRepository.signUpViaSocialMedia(
provider: anyNamed('provider'),
accessToken: anyNamed('accessToken'),
id: anyNamed('id'),
refreshToken: anyNamed('refreshToken'),
firstName: anyNamed('firstName'),
lastName: anyNamed('lastName'),
email: anyNamed('email'),
phone: anyNamed('phone'),
dateOfBirth: anyNamed('dateOfBirth'),
gender: anyNamed('gender'),
weight: anyNamed('weight'),
height: anyNamed('height'),
clientId: anyNamed('clientId'),
),
).thenThrow(dioException);
final result = await oauthCubit!.signUpViaSocialMedia(
provider: signUpRequestFixture['provider'] as String,
accessToken: signUpRequestFixture['access_token'] as String,
id: signUpRequestFixture['id'] as String,
refreshToken: signUpRequestFixture['refresh_token'] as String,
firstName: signUpRequestFixture['first_name'] as String,
lastName: signUpRequestFixture['last_name'] as String,
email: signUpRequestFixture['email'] as String,
phone: signUpRequestFixture['phone'] as String,
dateOfBirth: signUpRequestFixture['date_of_birth'] as String,
gender: signUpRequestFixture['gender'] as String,
weight: signUpRequestFixture['weight'] as int,
height: signUpRequestFixture['height'] as int,
clientId: signUpRequestFixture['client_id'] as String,
);
expect(result, isNotNull);
expect(result!.length, 1);
expect(result[0], RegistrationValidationErrors.incorrectEmailFormat);
});
test('requestOAuthVerification - успешный запрос кода верификации', () async {
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
const verificationResponse = OAuthVerificationRequestResponse(email: 't***@example.com', status: 'sent');
// OAuthCubit всегда передает email: null в репозиторий, независимо от переданного параметра
when(mockAuthRepository.requestOAuthVerification(email: null, phone: null)).thenAnswer((_) async => verificationResponse);
final result = await oauthCubit!.requestOAuthVerification(email: 'test@example.com');
expect(result, isNotNull);
expect(result!.email, 't***@example.com');
expect(oauthCubit!.state, isA<OAuthState>());
});
test('requestOAuthVerification - обработка ошибки', () async {
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
when(mockAuthRepository.requestOAuthVerification(email: anyNamed('email'), phone: anyNamed('phone'))).thenThrow(Exception('Ошибка запроса'));
final result = await oauthCubit!.requestOAuthVerification(email: 'test@example.com');
expect(result, isNull);
expect(oauthCubit!.state, isA<OAuthStateError>());
});
test('confirmOAuthVerification - успешное подтверждение верификации', () async {
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
final verificationResponse = OAuthVerificationConfirmResponse(success: true, message: 'Success', token: tokenFixture.token, user: {}, isNewUser: false);
when(
mockAuthRepository.confirmOAuthVerification(
provider: anyNamed('provider'),
accessToken: anyNamed('accessToken'),
refreshToken: anyNamed('refreshToken'),
clientId: anyNamed('clientId'),
id: anyNamed('id'),
email: anyNamed('email'),
phone: anyNamed('phone'),
code: anyNamed('code'),
verificationType: anyNamed('verificationType'),
firstName: anyNamed('firstName'),
lastName: anyNamed('lastName'),
dateOfBirth: anyNamed('dateOfBirth'),
gender: anyNamed('gender'),
weight: anyNamed('weight'),
height: anyNamed('height'),
),
).thenAnswer((_) async => verificationResponse);
await oauthCubit!.confirmOAuthVerification(
provider: 'google',
accessToken: 'access_token',
refreshToken: 'refresh_token',
clientId: 'client_id',
id: 'id',
email: 'test@example.com',
phone: null,
code: '123456',
verificationType: 'email',
firstName: 'Test',
lastName: 'User',
dateOfBirth: '2000-01-01',
gender: 'male',
weight: 70,
height: 175,
);
await Future.delayed(const Duration(milliseconds: 100));
expect(oauthCubit!.state, isA<OAuthStateLoaded>());
final loadedState = oauthCubit!.state as OAuthStateLoaded;
expect(loadedState.token, tokenFixture.token);
});
test('confirmOAuthVerification - обработка ошибки', () async {
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
when(
mockAuthRepository.confirmOAuthVerification(
provider: anyNamed('provider'),
accessToken: anyNamed('accessToken'),
refreshToken: anyNamed('refreshToken'),
clientId: anyNamed('clientId'),
id: anyNamed('id'),
email: anyNamed('email'),
phone: anyNamed('phone'),
code: anyNamed('code'),
verificationType: anyNamed('verificationType'),
firstName: anyNamed('firstName'),
lastName: anyNamed('lastName'),
dateOfBirth: anyNamed('dateOfBirth'),
gender: anyNamed('gender'),
weight: anyNamed('weight'),
height: anyNamed('height'),
),
).thenThrow(Exception('Ошибка подтверждения'));
expect(
() => oauthCubit!.confirmOAuthVerification(
provider: 'google',
accessToken: 'access_token',
refreshToken: 'refresh_token',
clientId: 'client_id',
id: 'id',
email: 'test@example.com',
phone: null,
code: '123456',
verificationType: 'email',
firstName: 'Test',
lastName: 'User',
dateOfBirth: '2000-01-01',
gender: 'male',
weight: 70,
height: 175,
),
throwsException,
);
await Future.delayed(const Duration(milliseconds: 100));
expect(oauthCubit!.state, isA<OAuthStateError>());
});
});
}

View File

@@ -0,0 +1,59 @@
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/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/presentation/pages/auth_screen.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'auth_screen_smoke_test.mocks.dart';
@GenerateNiceMocks([MockSpec<AuthCubit>()])
void main() {
group('AuthScreen 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(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: child,
);
}
testWidgets('renders base auth UI', (tester) async {
final cubit = MockAuthCubit();
when(cubit.state).thenReturn(const AuthState());
when(cubit.stream).thenAnswer((_) => const Stream<AuthState>.empty());
when(cubit.close()).thenAnswer((_) async {});
sl.registerFactory<AuthCubit>(() => cubit);
await tester.pumpWidget(wrap(const AuthScreen()));
await tester.pump();
expect(find.byType(AuthScreen), findsOneWidget);
// Minimal/stable assertions.
expect(find.text(S.current.authentificate), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,65 @@
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/bloc/reset_password_cubit/reset_password_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart';
import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart';
import 'package:flutter/material.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 'forget_password_screen_smoke_test.mocks.dart';
@GenerateNiceMocks([MockSpec<ResetPasswordCubit>()])
void main() {
group('ForgetPasswordScreen 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 initial email step (isAuth=false)', (tester) async {
final cubit = MockResetPasswordCubit();
when(cubit.state).thenReturn(const ResetPasswordState.empty());
when(cubit.stream).thenAnswer((_) => const Stream<ResetPasswordState>.empty());
when(cubit.close()).thenAnswer((_) async {});
sl.registerFactory<ResetPasswordCubit>(() => cubit);
await tester.pumpWidget(wrap(const ForgetPasswordScreen(isAuth: false)));
await tester.pump();
expect(find.byType(ForgetPasswordScreen), findsOneWidget);
expect(find.text('Восстановление пароля'), findsOneWidget);
expect(find.text('E-mail'), findsOneWidget);
expect(find.text('Сбросить пароль'), findsOneWidget);
});
testWidgets('renders code + new password step', (tester) async {
final cubit = MockResetPasswordCubit();
when(cubit.state).thenReturn(const ResetPasswordState.codeSendSuccess());
when(cubit.stream).thenAnswer((_) => const Stream<ResetPasswordState>.empty());
when(cubit.close()).thenAnswer((_) async {});
sl.registerFactory<ResetPasswordCubit>(() => cubit);
await tester.pumpWidget(wrap(const ForgetPasswordScreen(isAuth: true)));
await tester.pump();
expect(find.text('Смена пароля'), findsOneWidget);
expect(find.text('Код'), findsOneWidget);
expect(find.text('Пароль'), findsOneWidget);
expect(find.text('Подтвердите пароль'), findsOneWidget);
expect(find.text('Сменить пароль'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,66 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/di/cubit_registrations.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/presentation/pages/register_via_social_screen.dart';
import 'package:flutter/material.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 'register_via_social_screen_smoke_test.mocks.dart';
@GenerateNiceMocks([MockSpec<OAuthCubit>()])
void main() {
group('RegisterViaSocialScreen smoke', () {
setUpAll(() async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
});
setUp(() async {
await sl.reset();
await CubitRegistrations.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 and pre-fills fields from AuthResultEntity', (tester) async {
final cubit = MockOAuthCubit();
when(cubit.state).thenReturn(const OAuthState());
when(cubit.stream).thenAnswer((_) => const Stream<OAuthState>.empty());
when(cubit.close()).thenAnswer((_) async {});
CubitRegistrations.sl.registerFactory<OAuthCubit>(() => cubit);
const authResult = AuthResultEntity(
accessToken: 'token',
userId: '1',
clientId: 'client',
refreshToken: 'refresh',
firstName: 'Ivan',
lastName: 'Petrov',
email: 'ivan@example.com',
phone: '+7 (999) 111 22 33',
birthDate: '1990-01-01',
gender: GenderEnum.male,
);
await tester.pumpWidget(wrap(const RegisterViaSocialScreen(authResult: authResult, provider: 'vk')));
await tester.pump();
expect(find.byType(RegisterViaSocialScreen), findsOneWidget);
expect(find.text('Регистрация'), findsOneWidget);
expect(find.text('Ivan'), findsOneWidget);
expect(find.text('Petrov'), findsOneWidget);
expect(find.text('ivan@example.com'), findsOneWidget);
});
});
}

View File

@@ -1,28 +1,42 @@
// ignore_for_file: deprecated_member_use
import 'package:eatvio_app/app_keys.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/theme/theme_helper.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/profile/repositories/profile_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../fixtures/doubles/auth_doubles.dart';
import '../../../../fixtures/doubles/profile_doubles.dart';
import '../../../../fixtures/fixture_reader.dart';
import 'basic_widget_test.mocks.dart';
@GenerateNiceMocks([MockSpec<Prefs>(), MockSpec<FirebaseApi>(), MockSpec<ProfileRepository>()])
void main() {
group('Auth Basic Widget Tests', () {
late FixtureAuthRepository authRepository;
late FixtureProfileRepository profileRepository;
late MockPrefs mockPrefs;
late MockFirebaseApi mockFirebaseApi;
late AuthCubit authCubit;
late Map<String, dynamic> credentialsFixture;
setUp(() {
setUp(() async {
credentialsFixture = FixtureReader.readAsMap('auth/login_credentials.json');
authRepository = FixtureAuthRepository(
tokenFixture: TokenModel.fromJson(FixtureReader.readAsMap('auth/token_response.json')),
);
authCubit = AuthCubit(authRepository: authRepository);
authRepository = FixtureAuthRepository(tokenFixture: TokenModel.fromJson(FixtureReader.readAsMap('auth/token_response.json')));
profileRepository = FixtureProfileRepository();
mockPrefs = MockPrefs();
mockFirebaseApi = MockFirebaseApi();
SharedPreferences.setMockInitialValues({});
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
});
tearDown(() {
@@ -41,7 +55,7 @@ void main() {
testWidgets('should create AuthCubit successfully', (WidgetTester tester) async {
// Arrange & Act
final cubit = AuthCubit(authRepository: authRepository);
final cubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
// Assert
expect(cubit, isA<AuthCubit>());
@@ -83,11 +97,7 @@ void main() {
testWidgets('should display sign in button', (WidgetTester tester) async {
// Arrange
Widget buttonWidget = FloatingActionButton.extended(
key: AppKeys.signInButtonAuthScreen,
onPressed: () {},
label: const Text('Войти'),
);
Widget buttonWidget = FloatingActionButton.extended(key: AppKeys.signInButtonAuthScreen, onPressed: () {}, label: const Text('Войти'));
// Act
await tester.pumpWidget(createTestWidget(buttonWidget));
@@ -156,11 +166,7 @@ void main() {
obscureText: true,
),
const SizedBox(height: 16),
FloatingActionButton.extended(
key: AppKeys.signInButtonAuthScreen,
onPressed: () {},
label: const Text('Войти'),
),
FloatingActionButton.extended(key: AppKeys.signInButtonAuthScreen, onPressed: () {}, label: const Text('Войти')),
],
);
@@ -227,10 +233,7 @@ void main() {
decoration: const InputDecoration(
labelText: 'Пароль',
hintText: 'Введите пароль',
suffix: IconButton(
onPressed: null,
icon: Icon(Icons.visibility_outlined),
),
suffix: IconButton(onPressed: null, icon: Icon(Icons.visibility_outlined)),
),
obscureText: true,
);
@@ -246,10 +249,7 @@ void main() {
// Arrange
Widget forgotPasswordWidget = GestureDetector(
onTap: () {},
child: const Text(
'Забыли пароль?',
style: TextStyle(decoration: TextDecoration.underline),
),
child: const Text('Забыли пароль?', style: TextStyle(decoration: TextDecoration.underline)),
);
// Act
@@ -267,10 +267,7 @@ void main() {
const Text('Нет аккаунта? '),
GestureDetector(
onTap: () {},
child: const Text(
'Зарегистрироваться',
style: TextStyle(decoration: TextDecoration.underline),
),
child: const Text('Зарегистрироваться', style: TextStyle(decoration: TextDecoration.underline)),
),
],
);
@@ -336,11 +333,7 @@ void main() {
// Arrange
Widget emailWidget = TextFormField(
key: AppKeys.emailTextFieldAuthScreen,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'Введите email',
errorText: 'Введите корректный email',
),
decoration: const InputDecoration(labelText: 'Email', hintText: 'Введите email', errorText: 'Введите корректный email'),
keyboardType: TextInputType.emailAddress,
);
@@ -371,12 +364,7 @@ void main() {
testWidgets('should display form labels correctly', (WidgetTester tester) async {
// Arrange
Widget formWidget = const Column(
children: [
Text('Email'),
Text('Пароль'),
],
);
Widget formWidget = const Column(children: [Text('Email'), Text('Пароль')]);
// Act
await tester.pumpWidget(createTestWidget(formWidget));
@@ -413,4 +401,3 @@ void main() {
});
});
}

View File

@@ -0,0 +1,99 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/auth/sdk/social_auth_facade.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('SocialAuthFacade', () {
setUpAll(() async {
dotenv.loadFromString(
envString:
'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost\nVK_CLIENT_ID_IOS=\nVK_CLIENT_ID_ANDROID=\nYANDEX_CLIENT_ID=\nYANDEX_REDIRECT_URI=',
);
});
setUp(() async {
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
test('signInWithVk returns null when context is null', () async {
// Устанавливаем navigatorKey.currentContext в null
AppRouter.navigatorKey = GlobalKey<NavigatorState>();
final result = await SocialAuthFacade.signInWithVk();
expect(result, isNull);
});
testWidgets('signInWithVkAndFetchProfile returns null when clientId is empty', (tester) async {
// Загружаем dotenv с пустыми значениями для VK
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost\nVK_CLIENT_ID_IOS=\nVK_CLIENT_ID_ANDROID=');
// Создаем простой MaterialApp для получения BuildContext
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) {
// Вызываем метод асинхронно и проверяем результат
SocialAuthFacade.signInWithVkAndFetchProfile(context: context).then((result) {
expect(result, isNull);
});
return const Scaffold(body: SizedBox());
},
),
),
);
await tester.pumpAndSettle();
});
test('signInWithOkAndFetchProfile returns null when context is null', () async {
AppRouter.navigatorKey = GlobalKey<NavigatorState>();
final result = await SocialAuthFacade.signInWithOkAndFetchProfile();
expect(result, isNull);
});
test('signInWithMailRuAndFetchProfile returns null when context is null', () async {
AppRouter.navigatorKey = GlobalKey<NavigatorState>();
final result = await SocialAuthFacade.signInWithMailRuAndFetchProfile();
expect(result, isNull);
});
test('signInWithYandex returns null when clientId is empty', () async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost\nYANDEX_CLIENT_ID=\nYANDEX_REDIRECT_URI=test-redirect');
final result = await SocialAuthFacade.signInWithYandex();
expect(result, isNull);
});
test('signInWithYandex returns null when redirectUri is empty', () async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost\nYANDEX_CLIENT_ID=test-client-id\nYANDEX_REDIRECT_URI=');
final result = await SocialAuthFacade.signInWithYandex();
expect(result, isNull);
});
test('signInWithYandex returns null when context is null', () async {
dotenv.loadFromString(
envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost\nYANDEX_CLIENT_ID=test-client-id\nYANDEX_REDIRECT_URI=test-redirect-uri',
);
AppRouter.navigatorKey = GlobalKey<NavigatorState>();
final result = await SocialAuthFacade.signInWithYandex();
expect(result, isNull);
});
});
}

View File

@@ -0,0 +1,147 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
import 'package:eatvio_app/features/barcode/repositories/barcode_repository.dart';
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'barcode_repository_test.mocks.dart';
@GenerateNiceMocks([MockSpec<BarcodeService>(), MockSpec<Dio>()])
void main() {
group('BarcodeRepository', () {
late BarcodeRepository repository;
late MockBarcodeService mockService;
late MockDio mockDio;
setUp(() {
mockService = MockBarcodeService();
mockDio = MockDio();
repository = BarcodeRepository(barcodeService: mockService, dio: mockDio);
});
test('getProductByBarcode - возвращает успешный результат (200)', () async {
const barcode = '1234567890123';
final productData = {'GTIN': barcode, 'Описание (ru)': 'Test Product'};
final response = Response(
data: productData,
statusCode: 200,
requestOptions: RequestOptions(path: '/api/gsone/barcode/$barcode'),
);
when(mockDio.get('/api/gsone/barcode/$barcode')).thenAnswer((_) async => response);
final result = await repository.getProductByBarcode(barcode);
expect(result.type, BarcodeResultType.success);
expect(result.product, isNotNull);
expect(result.product!.description, 'Test Product');
verify(mockDio.get('/api/gsone/barcode/$barcode')).called(1);
});
test('getProductByBarcode - возвращает статус обработки (202)', () async {
const barcode = '1234567890123';
final taskData = {'task_id': 'task-123', 'status': 'processing'};
final response = Response(
data: taskData,
statusCode: 202,
requestOptions: RequestOptions(path: '/api/gsone/barcode/$barcode'),
);
when(mockDio.get('/api/gsone/barcode/$barcode')).thenAnswer((_) async => response);
final result = await repository.getProductByBarcode(barcode);
expect(result.type, BarcodeResultType.processing);
expect(result.taskId, equals('task-123'));
verify(mockDio.get('/api/gsone/barcode/$barcode')).called(1);
});
test('getProductByBarcode - обрабатывает DioException с 202 статусом', () async {
const barcode = '1234567890123';
final taskData = {'task_id': 'task-456', 'status': 'processing'};
final dioException = DioException(
requestOptions: RequestOptions(path: '/api/gsone/barcode/$barcode'),
response: Response(
data: taskData,
statusCode: 202,
requestOptions: RequestOptions(path: '/api/gsone/barcode/$barcode'),
),
);
when(mockDio.get('/api/gsone/barcode/$barcode')).thenThrow(dioException);
final result = await repository.getProductByBarcode(barcode);
expect(result.type, BarcodeResultType.processing);
expect(result.taskId, equals('task-456'));
});
test('getProductByBarcode - пробрасывает исключение при другой ошибке', () async {
const barcode = '1234567890123';
final dioException = DioException(
requestOptions: RequestOptions(path: '/api/gsone/barcode/$barcode'),
response: Response(
data: {'error': 'Not found'},
statusCode: 404,
requestOptions: RequestOptions(path: '/api/gsone/barcode/$barcode'),
),
);
when(mockDio.get('/api/gsone/barcode/$barcode')).thenThrow(dioException);
expect(() => repository.getProductByBarcode(barcode), throwsException);
});
test('getTaskStatus - возвращает completed статус', () async {
const taskId = 'task-123';
final productData = {'GTIN': '1234567890123', 'Описание (ru)': 'Test Product'};
final statusModel = BarcodeTaskStatusModel(status: 'completed', data: BarcodeProductModel.fromJson(productData));
when(mockService.getTaskStatus(taskId: taskId)).thenAnswer((_) async => statusModel);
final result = await repository.getTaskStatus(taskId);
expect(result.type, BarcodeTaskStatusType.completed);
expect(result.product, isNotNull);
expect(result.product!.description, 'Test Product');
verify(mockService.getTaskStatus(taskId: taskId)).called(1);
});
test('getTaskStatus - возвращает failed статус', () async {
const taskId = 'task-123';
const statusModel = BarcodeTaskStatusModel(status: 'failed', data: null);
when(mockService.getTaskStatus(taskId: taskId)).thenAnswer((_) async => statusModel);
final result = await repository.getTaskStatus(taskId);
expect(result.type, BarcodeTaskStatusType.failed);
expect(result.product, isNull);
verify(mockService.getTaskStatus(taskId: taskId)).called(1);
});
test('getTaskStatus - возвращает processing статус', () async {
const taskId = 'task-123';
const statusModel = BarcodeTaskStatusModel(status: 'processing', data: null);
when(mockService.getTaskStatus(taskId: taskId)).thenAnswer((_) async => statusModel);
final result = await repository.getTaskStatus(taskId);
expect(result.type, BarcodeTaskStatusType.processing);
expect(result.product, isNull);
verify(mockService.getTaskStatus(taskId: taskId)).called(1);
});
test('getTaskStatus - пробрасывает исключение при ошибке', () async {
const taskId = 'task-123';
final exception = Exception('Ошибка загрузки');
when(mockService.getTaskStatus(taskId: taskId)).thenThrow(exception);
expect(() => repository.getTaskStatus(taskId), throwsException);
});
});
}

View File

@@ -0,0 +1,109 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'blog_post_screen_smoke_test.mocks.dart';
@GenerateNiceMocks([MockSpec<BlogPostCubit>(), MockSpec<FeedCubit>(), MockSpec<CommentsCubit>()])
void main() {
group('BlogPostScreen smoke', () {
setUpAll(() async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
});
setUp(() {
FlutterError.onError = (FlutterErrorDetails details) {
if (details.exception is FlutterError && details.exception.toString().contains('overflowed')) {
return;
}
FlutterError.presentError(details);
};
});
setUp(() async {
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
void registerMocks(BlogPostCubit blogPostCubit, CommentsCubit commentsCubit) {
sl.registerLazySingleton<BlogPostCubit>(() => blogPostCubit);
sl.registerLazySingleton<CommentsCubit>(() => commentsCubit);
}
Widget wrap(BlogPostCubit blogPostCubit, FeedCubit feedCubit, CommentsCubit commentsCubit, Widget child) {
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => MediaQuery(
data: const MediaQueryData(size: Size(2000, 1200)),
child: MultiBlocProvider(
providers: [
BlocProvider<BlogPostCubit>.value(value: blogPostCubit),
BlocProvider<FeedCubit>.value(value: feedCubit),
BlocProvider<CommentsCubit>.value(value: commentsCubit),
],
child: child,
),
),
),
GoRoute(path: '/feed-screen', builder: (_, _) => const SizedBox.shrink()),
],
);
return MaterialApp.router(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
routerConfig: router,
);
}
testWidgets('renders loading state', (tester) async {
final blogPostCubit = MockBlogPostCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
final state = BlogPostState(isLoading: true, blogPostModel: null, error: null);
when(blogPostCubit.state).thenReturn(state);
when(blogPostCubit.stream).thenAnswer((_) => const Stream<BlogPostState>.empty());
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

@@ -6,6 +6,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart';
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
@@ -57,27 +58,21 @@ void main() {
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const Scaffold(
body: Center(child: Text('Home')),
),
builder: (context, state) => const Scaffold(body: Center(child: Text('Home'))),
),
GoRoute(
path: '/blog-post-create',
builder: (context, state) => MultiBlocProvider(
providers: [
BlocProvider<BlogPostCreateCubit>.value(
value: blogPostCreateCubit,
),
BlocProvider<FeedCubit>.value(
value: mockFeedCubit,
),
BlocProvider<BlogPostCreateCubit>.value(value: blogPostCreateCubit),
BlocProvider<FeedCubit>.value(value: mockFeedCubit),
],
child: BlogPostCreateScreen(feedBlogPostModel: feedBlogPostModel),
),
),
],
);
return MaterialApp.router(
routerConfig: router,
theme: ThemeHelper().themeData(),
@@ -113,12 +108,7 @@ void main() {
videoUrl: 'https://youtube.com/watch?v=test',
image: '', // Changed: removed image so video field is visible
state: 'published',
author: const Author(
authorSlug: 'test-author',
authorName: 'Test Author',
authorProfileIcon: 'avatar.jpg',
authorIsMe: true,
),
author: const Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: 'avatar.jpg', authorIsMe: true),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
totalLikes: 0,
@@ -136,7 +126,6 @@ void main() {
final descriptionField = tester.widget<CustomTextFormField>(find.byKey(AppKeys.blogPostCreateScreenContentTextField));
expect(descriptionField.controller?.text, 'Test Description');
// Only check video field if it's visible (when image is null)
final videoFieldFinder = find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField);
if (videoFieldFinder.evaluate().isNotEmpty) {
final videoField = tester.widget<CustomTextFormField>(videoFieldFinder);
@@ -183,12 +172,7 @@ void main() {
videoUrl: 'https://youtube.com/watch?v=test',
image: '',
state: 'published',
author: const Author(
authorSlug: 'test-author',
authorName: 'Test Author',
authorProfileIcon: 'avatar.jpg',
authorIsMe: true,
),
author: const Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: 'avatar.jpg', authorIsMe: true),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
totalLikes: 0,
@@ -256,16 +240,13 @@ void main() {
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Test Description');
await tester.pump();
// Tap create button - find by text and scroll if needed
final createButton = find.text('Создать');
await tester.ensureVisible(createButton);
await tester.tap(createButton);
await tester.pump();
// Check for Loader widget (not CircularProgressIndicator)
expect(find.byType(Loader), findsWidgets);
// Wait for timer to complete
await tester.pump(const Duration(milliseconds: 150));
await tester.pumpAndSettle();
});
@@ -280,11 +261,11 @@ void main() {
final titleField = find.byKey(AppKeys.blogPostCreateScreenTitleTextField);
final contentField = find.byKey(AppKeys.blogPostCreateScreenContentTextField);
await tester.ensureVisible(titleField);
await tester.ensureVisible(contentField);
await tester.pumpAndSettle();
await tester.enterText(titleField, 'Test Title');
await tester.enterText(contentField, 'Test Description');
await tester.pump();
@@ -294,7 +275,6 @@ void main() {
await tester.tap(createButton, warnIfMissed: false);
await tester.pump();
// Wait for async operation
await tester.pump(const Duration(milliseconds: 200));
await tester.pumpAndSettle();
@@ -333,28 +313,19 @@ void main() {
final titleField = find.byKey(AppKeys.blogPostCreateScreenTitleTextField);
expect(titleField, findsOneWidget);
await tester.enterText(titleField, 'Updated Title');
await tester.pump();
// Find and tap create button - ensure it's visible
final createButton = find.text('Создать');
await tester.ensureVisible(createButton);
await tester.tap(createButton, warnIfMissed: false);
await tester.pump();
// Wait for async operation
await tester.pump(const Duration(milliseconds: 200));
await tester.pumpAndSettle();
verify(mockBlogPostRepository.updateBlogPost(
any,
any,
any,
any,
any,
any,
)).called(1);
verify(mockBlogPostRepository.updateBlogPost(any, any, any, any, any, any)).called(1);
});
});
@@ -371,6 +342,24 @@ void main() {
expect(find.byType(SingleChildScrollView), findsOneWidget);
});
testWidgets('renders error state', (tester) async {
blogPostCreateCubit.emit(const BlogPostCreateState.error(errorMessage: 'Error creating post'));
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
expect(find.byType(BlogPostCreateScreen), findsOneWidget);
});
testWidgets('renders loading state', (tester) async {
blogPostCreateCubit.emit(const BlogPostCreateState.loading());
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
expect(find.byType(BlogPostCreateScreen), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,147 @@
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/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.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';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:network_image_mock/network_image_mock.dart';
import 'bottom_navigation_bar_widget_test.mocks.dart';
@GenerateNiceMocks([MockSpec<NotificationsCubit>(), MockSpec<ChatsCubit>()])
void main() {
group('BottomNavigationBarWidget 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'));
});
const mockUserProfile = ProfileModel(
id: 1,
name: 'Test User',
email: 'test@example.com',
phoneConfirmationState: 'confirmed',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
slug: 'test-user',
profilePhotoUrl: 'https://example.com/avatar.jpg',
roles: ['user'],
badges: [],
subscriptions: [],
subscribers: [],
);
const mockConsultantProfile = ProfileModel(
id: 1,
name: 'Test Consultant',
email: 'consultant@example.com',
phoneConfirmationState: 'confirmed',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
slug: 'test-consultant',
profilePhotoUrl: 'https://example.com/avatar.jpg',
roles: ['consultant'],
badges: [],
subscriptions: [],
subscribers: [],
);
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: child,
);
}
testWidgets('renders bottom navigation bar for user', (tester) async {
mockNetworkImagesFor(() async {
final notificationsCubit = MockNotificationsCubit();
final chatsCubit = MockChatsCubit();
when(notificationsCubit.state).thenReturn(const NotificationsState());
when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty());
when(notificationsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(chatsCubit.state).thenReturn(ChatsState());
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(
MultiBlocProvider(
providers: [
BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
BlocProvider<ChatsCubit>.value(value: chatsCubit),
],
child: wrap(
const Scaffold(
body: BottomNavigationBarWidget(profileModel: mockUserProfile, child: Text('Test Content')),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(BottomNavigationBarWidget), findsOneWidget);
});
});
testWidgets('renders bottom navigation bar for consultant', (tester) async {
mockNetworkImagesFor(() async {
final notificationsCubit = MockNotificationsCubit();
final chatsCubit = MockChatsCubit();
when(notificationsCubit.state).thenReturn(const NotificationsState());
when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty());
when(notificationsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(chatsCubit.state).thenReturn(ChatsState());
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(
MultiBlocProvider(
providers: [
BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
BlocProvider<ChatsCubit>.value(value: chatsCubit),
],
child: wrap(
const Scaffold(
body: BottomNavigationBarWidget(profileModel: mockConsultantProfile, child: Text('Test Content')),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(BottomNavigationBarWidget), findsOneWidget);
});
});
});
}

View File

@@ -0,0 +1,85 @@
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/calendar/models/calendar_model.dart';
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'calendar_screen_smoke_test.mocks.dart';
@GenerateNiceMocks([MockSpec<CalendarRepository>()])
void main() {
group('CalendarScreen smoke', () {
setUpAll(() async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
await initializeDateFormatting('ru', null);
});
setUp(() async {
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
final mockCalendarList = [
const CalendarModel(date: '2025-01-01', isCurrentDayIncludedInTheChosenDietProgression: true, diary: <String, dynamic>{'id': 1, 'state': 'finished'}),
];
Widget wrap(Widget child) {
return MaterialApp(theme: ThemeHelper().themeData(), home: child);
}
testWidgets('renders loading state', (tester) async {
final calendarRepository = MockCalendarRepository();
final cubit = CalendarCubit(calendarRepository: calendarRepository);
when(calendarRepository.getCalendar()).thenAnswer((_) async => mockCalendarList);
sl.registerLazySingleton<CalendarRepository>(() => calendarRepository);
sl.registerLazySingleton<CalendarCubit>(() => cubit);
await tester.pumpWidget(wrap(const CalendarScreen()));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(CalendarScreen), findsOneWidget);
});
testWidgets('renders loaded state with calendar list', (tester) async {
final calendarRepository = MockCalendarRepository();
final cubit = CalendarCubit(calendarRepository: calendarRepository);
when(calendarRepository.getCalendar()).thenAnswer((_) async => mockCalendarList);
sl.registerLazySingleton<CalendarRepository>(() => calendarRepository);
sl.registerLazySingleton<CalendarCubit>(() => cubit);
await tester.pumpWidget(wrap(const CalendarScreen()));
await tester.pumpAndSettle();
expect(find.text('Календарь'), findsOneWidget);
});
testWidgets('renders error state', (tester) async {
final calendarRepository = MockCalendarRepository();
final cubit = CalendarCubit(calendarRepository: calendarRepository);
when(calendarRepository.getCalendar()).thenThrow(Exception('Error loading calendar'));
sl.registerLazySingleton<CalendarRepository>(() => calendarRepository);
sl.registerLazySingleton<CalendarCubit>(() => cubit);
await tester.pumpWidget(wrap(const CalendarScreen()));
await tester.pumpAndSettle();
expect(find.textContaining('Error loading calendar'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,199 @@
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/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_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.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';
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:visibility_detector/visibility_detector.dart';
import 'chat_screen_loaded_smoke_test.mocks.dart';
@GenerateNiceMocks([MockSpec<ChatRoomCubit>(), MockSpec<ChatsCubit>()])
void main() {
group('ChatScreen loaded smoke', () {
setUpAll(() async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
await initializeDateFormatting('ru', null);
await initializeDateFormatting('ru_RU', null);
await initializeDateFormatting('RU', null);
VisibilityDetectorController.instance.updateInterval = Duration.zero;
});
setUp(() async {
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
const fakeSvg = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
final fakeBundle = _FakeAssetBundle({'assets/icons/error_icon.svg': fakeSvg});
Widget wrap(Widget child, ChatsCubit chatsCubit) {
return DefaultAssetBundle(
bundle: fakeBundle,
child: MaterialApp(
theme: ThemeHelper().themeData(),
home: BlocProvider<ChatsCubit>.value(value: chatsCubit, child: child),
),
);
}
testWidgets('renders loaded header (chosenPlan null)', (tester) async {
final chatRoomCubit = MockChatRoomCubit();
final chatsCubit = MockChatsCubit();
final now = DateTime(2025, 1, 2, 10, 30);
final yesterday = DateTime(2025, 1, 1, 9, 15);
const chatRoom = ChatRoomModel(
id: '1',
chatRoomSettings: ChatRoomSettingsModel(name: 'room', state: 'active'),
users: Users(
sender: ChatUserModel(id: 1, name: 'Me', slug: 'me', profilePhotoUrl: null),
receiver: ChatUserModel(id: 2, name: 'User Two', slug: 'user-two', profilePhotoUrl: null),
),
chosenPlanId: null,
chosenPlan: null,
);
final state = ChatRoomState.loaded(
chatRoom: chatRoom,
chatMessages: [
ChatMessageModel(
id: 2,
data: 'hello',
mediaData: const [],
authorId: 1, // current user (sender)
updatedAt: now,
isRead: true,
isCurrentUserAuthor: true,
roomId: '1',
messageState: MessageState.sent,
uuid: 'u2',
),
ChatMessageModel(
id: 4,
data: '',
mediaData: const ['chat_files/file.pdf'],
authorId: 1, // current user
updatedAt: now.subtract(const Duration(minutes: 1)),
isRead: true,
isCurrentUserAuthor: true,
roomId: '1',
messageState: MessageState.sent,
uuid: 'u4',
),
ChatMessageModel(
id: 3,
data: 'sending...',
mediaData: const [],
authorId: 1, // current user
updatedAt: now.subtract(const Duration(minutes: 2)),
isRead: true,
isCurrentUserAuthor: true,
roomId: '1',
messageState: MessageState.loading,
uuid: 'u3',
),
ChatMessageModel(
id: 5,
data: 'tap to retry',
mediaData: const [],
authorId: 1, // current user
updatedAt: now.subtract(const Duration(minutes: 3)),
isRead: true,
isCurrentUserAuthor: true,
roomId: '1',
messageState: MessageState.error,
uuid: 'u5',
),
ChatMessageModel(
id: 1,
data: 'hi',
mediaData: const [],
authorId: 2, // other user (receiver)
updatedAt: yesterday,
isRead: true,
isCurrentUserAuthor: false,
roomId: '1',
messageState: MessageState.sent,
uuid: 'u1',
),
],
hasCard: false,
isTyping: false,
isLoading: false,
);
when(chatRoomCubit.state).thenReturn(state);
when(chatRoomCubit.stream).thenAnswer((_) => const Stream<ChatRoomState>.empty());
when(chatRoomCubit.close()).thenAnswer((_) async => Future<void>.value());
when(chatRoomCubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'))).thenAnswer((_) async => Future<void>.value());
when(chatRoomCubit.setTimer()).thenReturn(null);
sl.registerFactory<ChatRoomCubit>(() => chatRoomCubit);
when(chatsCubit.state).thenReturn(ChatsState());
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(wrap(const ChatScreen(uuid: 'uuid', sendInitialMessage: false), chatsCubit));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(find.byType(ChatScreen), findsOneWidget);
expect(find.text('User Two'), findsOneWidget);
expect(find.text('hello'), findsOneWidget);
expect(find.text('hi'), findsOneWidget);
expect(find.text('sending...'), findsOneWidget);
expect(find.text('tap to retry'), findsOneWidget);
expect(find.text('file.pdf'), findsOneWidget);
VisibilityDetectorController.instance.notifyNow();
await tester.pump();
});
});
}
class _FakeAssetBundle extends CachingAssetBundle {
_FakeAssetBundle(this.assets);
final Map<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) {
throw FlutterError('Asset not found: $key');
}
return value;
}
@override
Future<ByteData> load(String key) async {
final value = assets[key];
if (value == null) {
throw FlutterError('Asset not found: $key');
}
final bytes = Uint8List.fromList(value.codeUnits);
return ByteData.sublistView(bytes);
}
}

View File

@@ -0,0 +1,47 @@
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/loader.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
import 'package:flutter/material.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 'chat_screen_loading_smoke_test.mocks.dart';
@GenerateNiceMocks([MockSpec<ChatRoomCubit>()])
void main() {
group('ChatScreen 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) => MaterialApp(theme: ThemeHelper().themeData(), home: child);
testWidgets('renders loading scaffold', (tester) async {
final cubit = MockChatRoomCubit();
when(cubit.state).thenReturn(const ChatRoomState.loading());
when(cubit.stream).thenAnswer((_) => const Stream<ChatRoomState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'))).thenAnswer((_) async => Future<void>.value());
when(cubit.setTimer()).thenReturn(null);
sl.registerFactory<ChatRoomCubit>(() => cubit);
await tester.pumpWidget(wrap(const ChatScreen(uuid: 'uuid', sendInitialMessage: false)));
await tester.pump();
expect(find.byType(ChatScreen), findsOneWidget);
expect(find.byType(Loader), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,53 @@
import 'package:eatvio_app/core/enums/base_state.dart';
import 'package:eatvio_app/core/theme/theme_helper.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';
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'chats_screen_smoke_test.mocks.dart';
@GenerateNiceMocks([MockSpec<ChatsCubit>()])
void main() {
group('ChatsScreen smoke', () {
Widget wrap(Widget child, ChatsCubit cubit) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: BlocProvider<ChatsCubit>.value(value: cubit, child: child),
);
}
testWidgets('renders success state (empty list)', (tester) async {
final cubit = MockChatsCubit();
when(cubit.state).thenReturn(ChatsState(state: BaseState.success, chatRooms: const []));
when(cubit.stream).thenAnswer((_) => const Stream<ChatsState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(wrap(const ChatsScreen(), cubit));
await tester.pump();
expect(find.byType(ChatsScreen), findsOneWidget);
});
testWidgets('renders error state', (tester) async {
final cubit = MockChatsCubit();
when(cubit.state).thenReturn(ChatsState(state: BaseState.error, errorMessage: 'err'));
when(cubit.stream).thenAnswer((_) => const Stream<ChatsState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(wrap(const ChatsScreen(), cubit));
await tester.pump();
expect(find.byType(ChatsScreen), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,40 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:network_image_mock/network_image_mock.dart';
void main() {
group('EmptyChatScreen smoke', () {
Widget wrap(Widget child) => MaterialApp(theme: ThemeHelper().themeData(), home: child);
testWidgets('renders', (tester) async {
const user = ProfileModel(
id: 1,
name: 'User',
email: 'u@e.com',
phoneConfirmationState: 'confirmed',
slug: 'user',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: 'http://localhost/avatar.png',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(const EmptyChatScreen(user: user)));
await tester.pump();
});
expect(find.byType(EmptyChatScreen), findsOneWidget);
expect(find.text('Сообщение'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,102 @@
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/chat/presentation/pages/empty_chat_screen.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_localizations/flutter_localizations.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 'package:network_image_mock/network_image_mock.dart';
import 'empty_chat_screen_test.mocks.dart';
@GenerateNiceMocks([MockSpec<BlockedUsersCubit>()])
@GenerateMocks([ChatRepository])
void main() {
group('EmptyChatScreen 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'));
});
tearDown(() async {
await sl.reset();
});
Widget wrap(Widget child) {
return MaterialApp.router(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
routerConfig: GoRouter(
initialLocation: '/',
routes: [
GoRoute(path: '/', builder: (context, state) => child),
GoRoute(path: '/profile/:slug', name: 'profileScreen', builder: (context, state) => const SizedBox.shrink()),
GoRoute(path: '/chat/:uuid', name: 'chatScreen', builder: (context, state) => const SizedBox.shrink()),
],
),
);
}
testWidgets('renders empty chat screen', (tester) async {
final blockedUsersCubit = MockBlockedUsersCubit();
final chatRepository = MockChatRepository();
const blockedState = BlockedUsersStateLoaded(profilesList: []);
when(blockedUsersCubit.state).thenReturn(blockedState);
when(blockedUsersCubit.stream).thenAnswer((_) => Stream.value(blockedState));
when(blockedUsersCubit.close()).thenAnswer((_) async => Future<void>.value());
when(blockedUsersCubit.getBlockedUsers()).thenAnswer((_) async => Future<void>.value());
when(chatRepository.sendMessage(data: anyNamed('data'), slug: anyNamed('slug'))).thenAnswer((_) async => throw UnimplementedError());
when(chatRepository.getChatByUserSlug(any)).thenAnswer((_) async => 'test-uuid');
sl.registerLazySingleton<BlockedUsersCubit>(() => blockedUsersCubit);
sl.registerLazySingleton<ChatRepository>(() => chatRepository);
const user = 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/photo.jpg',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(const EmptyChatScreen(user: user)));
await tester.pump();
});
expect(find.byType(EmptyChatScreen), findsOneWidget);
expect(find.text('Test User'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,152 @@
import 'dart:async';
import 'dart:io';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
class _FakeHttpClient implements HttpClient {
bool _autoUncompress = true;
@override
bool get autoUncompress => _autoUncompress;
@override
set autoUncompress(bool value) {
_autoUncompress = value;
}
@override
Future<HttpClientRequest> getUrl(Uri url) async => _FakeHttpClientRequest();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientRequest implements HttpClientRequest {
@override
Future<HttpClientResponse> close() async => _FakeHttpClientResponse();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientResponse extends Stream<List<int>> implements HttpClientResponse {
static final List<int> _png = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
0x42,
0x60,
0x82,
];
@override
int get statusCode => 200;
@override
int get contentLength => _png.length;
@override
HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed;
@override
StreamSubscription<List<int>> listen(void Function(List<int>)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) {
return Stream<List<int>>.fromIterable([_png]).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
group('PhotoViewScreen 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'));
});
testWidgets('renders with a network image url', (tester) async {
await HttpOverrides.runZoned(() async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeHelper().themeData(),
home: const PhotoViewScreen(imageUrls: ['https://example.com/a.png'], index: 0),
),
);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(find.byType(PhotoViewScreen), findsOneWidget);
}, createHttpClient: (context) => _FakeHttpClient());
});
});
}

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