merge upstream

This commit is contained in:
2026-01-13 09:25:36 +00:00
285 changed files with 25664 additions and 9577 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

@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 87;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -539,7 +539,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 87;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -578,7 +578,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 87;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>87</string>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -48,8 +48,33 @@ platform :ios do
UI.message "Configuring Flutter for release mode..."
sh("cd .. && flutter build ios --config-only --release")
# Read build number from Flutter Generated.xcconfig
generated_xcconfig_path = File.join("..", "Flutter", "Generated.xcconfig")
flutter_build_number = nil
if File.exist?(generated_xcconfig_path)
File.foreach(generated_xcconfig_path) do |line|
matches = line.match(/FLUTTER_BUILD_NUMBER=(.*)/)
if matches
flutter_build_number = matches[1].strip
break
end
end
end
if flutter_build_number.nil?
UI.user_error!("Could not find FLUTTER_BUILD_NUMBER in #{generated_xcconfig_path}")
end
UI.message "Flutter build number: #{flutter_build_number}"
match(type: "appstore", readonly: true)
increment_build_number(xcodeproj: "Runner.xcodeproj")
# Set build number explicitly instead of incrementing
increment_build_number(
xcodeproj: "Runner.xcodeproj",
build_number: flutter_build_number
)
build_app(
workspace: "Runner.xcworkspace",
@@ -74,7 +99,7 @@ platform :ios do
skip_submission: true,
distribute_external: false,
notify_external_testers: false,
changelog: "TestFlight build #{get_build_number(xcodeproj: 'Runner.xcodeproj')}",
changelog: "TestFlight build #{flutter_build_number}",
verbose: true
)
rescue => ex

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

@@ -0,0 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_verification_confirm_request.freezed.dart';
part 'oauth_verification_confirm_request.g.dart';
@freezed
abstract class OAuthVerificationConfirmRequest with _$OAuthVerificationConfirmRequest {
const factory OAuthVerificationConfirmRequest({
@JsonKey(name: 'access_token') required String accessToken,
@JsonKey(name: 'refresh_token') required String refreshToken,
@JsonKey(name: 'client_id') required String clientId,
required String provider,
required String id,
String? email,
String? phone,
required String code,
@JsonKey(name: 'verification_type') required String verificationType,
@JsonKey(name: 'first_name') required String firstName,
@JsonKey(name: 'last_name') required String lastName,
@JsonKey(name: 'date_of_birth') required String dateOfBirth,
required String gender,
required int weight,
required int height,
}) = _OAuthVerificationConfirmRequest;
factory OAuthVerificationConfirmRequest.fromJson(Map<String, dynamic> json) => _$OAuthVerificationConfirmRequestFromJson(json);
}

View File

@@ -0,0 +1,36 @@
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

@@ -0,0 +1,30 @@
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

@@ -3,6 +3,7 @@ import 'package:dio/dio.dart';
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.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_request_response.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -28,6 +29,19 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
log('OAuthCubit: Got token model: ${tokenModel.token}');
TokenStorage.saveTokenInStorage(tokenModel.token);
emit(OAuthState.loaded(token: tokenModel.token));
} on DioException catch (e) {
log('OAuthCubit: Error occurred: $e');
// Проверяем, является ли ошибка ошибкой валидации email/phone
final errors = e.response?.data['errors'] as Map<String, dynamic>?;
final emailErrors = errors?['email'] as List<dynamic>?;
final phoneErrors = errors?['phone'] as List<dynamic>?;
if ((emailErrors != null && emailErrors.isNotEmpty) || (phoneErrors != null && phoneErrors.isNotEmpty)) {
// Это ошибка валидации - нужно показать экран подтверждения
emit(const OAuthState.error(errorMessage: 'validation_error'));
} else {
emit(OAuthState.error(errorMessage: e.toString()));
}
} catch (e) {
log('OAuthCubit: Error occurred: $e');
emit(OAuthState.error(errorMessage: e.toString()));
@@ -77,16 +91,22 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
final dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
final errorsList = <RegistrationValidationErrors>[];
if (emailErrors != null && emailErrors.isNotEmpty) {
if (emailErrors.contains('Некорректный формат email')) {
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
}
if (emailErrors.contains('Такой Эл. почта уже существует.')) {
errorsList.add(RegistrationValidationErrors.emailExist);
for (final error in emailErrors) {
final errorStr = error.toString();
if (errorStr.contains('Некорректный формат email')) {
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
}
if (errorStr.contains('почта уже существует') || errorStr.contains('Эл. почта уже существует')) {
errorsList.add(RegistrationValidationErrors.emailExist);
}
}
}
if (phoneErrors != null && phoneErrors.isNotEmpty) {
if (phoneErrors.contains('Такой phone уже существует.')) {
errorsList.add(RegistrationValidationErrors.phoneExist);
for (final error in phoneErrors) {
final errorStr = error.toString();
if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) {
errorsList.add(RegistrationValidationErrors.phoneExist);
}
}
}
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
@@ -101,6 +121,71 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
return null;
}
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
try {
emit(const OAuthState.loading());
log('OAuthCubit: Requesting verification code for email: $email, phone: $phone');
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;
} catch (e) {
log('OAuthCubit: Error requesting verification code: $e');
emit(OAuthState.error(errorMessage: e.toString()));
return null;
}
}
Future<void> confirmOAuthVerification({
required String provider,
required String accessToken,
required String refreshToken,
required String clientId,
required String id,
String? email,
String? phone,
required String code,
required String verificationType,
required String firstName,
required String lastName,
required String dateOfBirth,
required String gender,
required int weight,
required int height,
}) async {
try {
emit(const OAuthState.loading());
log('OAuthCubit: Confirming OAuth verification - provider: $provider, verificationType: $verificationType, code: $code');
final response = await authRepository.confirmOAuthVerification(
provider: provider,
accessToken: accessToken,
refreshToken: refreshToken,
clientId: clientId,
id: id,
email: email,
phone: phone,
code: code,
verificationType: verificationType,
firstName: firstName,
lastName: lastName,
dateOfBirth: dateOfBirth,
gender: gender,
weight: weight,
height: height,
);
log('OAuthCubit: OAuth verification confirmed successfully, token received');
TokenStorage.saveTokenInStorage(response.token);
emit(OAuthState.loaded(token: response.token));
} catch (e) {
log('OAuthCubit: Error confirming OAuth verification: $e');
emit(OAuthState.error(errorMessage: e.toString()));
rethrow; // Пробрасываем ошибку дальше, чтобы UI мог её обработать
}
}
}
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }

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

@@ -1,166 +1,124 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class ForgetPasswordScreen extends StatefulWidget {
final bool isAuth;
const ForgetPasswordScreen({super.key, required this.isAuth});
@override
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
}
class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
final emailController = TextEditingController();
final codeController = TextEditingController();
final passwordController = TextEditingController();
final passwordConfirmController = TextEditingController();
@override
Widget build(BuildContext context) {
return BlocProvider<ResetPasswordCubit>(
create: (BuildContext context) => sl<ResetPasswordCubit>(),
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
builder: (BuildContext context, ResetPasswordState state) {
return Scaffold(
appBar: AppBar(
title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'),
centerTitle: false,
),
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
? const Center(
child: CircularProgressIndicator(),
)
: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
Text(
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 24),
const Text(
'E-mail',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
CustomTextFormField(
controller: emailController,
hintText: 'Введите e-mail',
),
],
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
const Text(
'Код',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
CustomTextFormField(
controller: codeController,
hintText: 'Введите код',
),
const SizedBox(height: 12),
const Text(
'Пароль',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
CustomTextFormField(
controller: passwordController,
hintText: 'Введите новый пароль',
),
const SizedBox(height: 12),
const Text(
'Подтвердите пароль',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
CustomTextFormField(
controller: passwordConfirmController,
hintText: 'Введите новый пароль',
),
],
const Spacer(),
InkWell(
onTap: () {
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
context.read<ResetPasswordCubit>().tryResetPassword(
emailController.text,
codeController.text,
passwordController.text,
passwordConfirmController.text,
);
} else {
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
}
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: Center(
child: Text(
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
? 'Сменить пароль'
: 'Сбросить пароль',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
],
),
),
);
},
listener: (BuildContext context, ResetPasswordState state) {
if (state is ResetPasswordStateCodeSendSuccess) {
showSuccessToast('Код на почту успешно отправлен');
}
if (state is ResetPasswordStateCodeSendError) {
showErrorToast('Ошибка отправки кода. Проверьте правильность ввёдной почты');
}
if (state is ResetPasswordStateResetPasswordError) {
showErrorToast('Ошибка смены пароля. Проверьте правильность ввёднного кода и совпадение пароля');
}
if (state is ResetPasswordStateResetPassowrdSuccess) {
GoRouter.of(context).pop();
showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}');
}
},
),
);
}
}
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.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:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class ForgetPasswordScreen extends StatefulWidget {
final bool isAuth;
const ForgetPasswordScreen({super.key, required this.isAuth});
@override
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
}
class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
final emailController = TextEditingController();
final codeController = TextEditingController();
final passwordController = TextEditingController();
final passwordConfirmController = TextEditingController();
@override
Widget build(BuildContext context) {
return BlocProvider<ResetPasswordCubit>(
create: (BuildContext context) => sl<ResetPasswordCubit>(),
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
builder: (BuildContext context, ResetPasswordState state) {
return Scaffold(
appBar: AppBar(title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'), centerTitle: false),
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
? const Center(child: CircularProgressIndicator())
: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
Text(
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 24),
const Text('E-mail', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
CustomTextFormField(controller: emailController, hintText: 'Введите e-mail'),
],
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
const Text('Код', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
CustomTextFormField(controller: codeController, hintText: 'Введите код'),
const SizedBox(height: 12),
const Text('Пароль', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
CustomTextFormField(
controller: passwordController,
hintText: 'Введите новый пароль',
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
),
const SizedBox(height: 12),
const Text('Подтвердите пароль', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
CustomTextFormField(
controller: passwordConfirmController,
hintText: 'Введите новый пароль',
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
),
],
const Spacer(),
InkWell(
onTap: () {
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
context.read<ResetPasswordCubit>().tryResetPassword(
emailController.text,
codeController.text,
passwordController.text,
passwordConfirmController.text,
);
} else {
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
}
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: Center(
child: Text(
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
? 'Сменить пароль'
: 'Сбросить пароль',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
),
),
),
),
],
),
),
);
},
listener: (BuildContext context, ResetPasswordState state) {
if (state is ResetPasswordStateCodeSendSuccess) {
showSuccessToast('Код на почту успешно отправлен');
}
if (state is ResetPasswordStateCodeSendError) {
showErrorToast('Ошибка отправки кода. Проверьте правильность ввёдной почты');
}
if (state is ResetPasswordStateResetPasswordError) {
showErrorToast('Ошибка смены пароля. Проверьте правильность ввёднного кода и совпадение пароля');
}
if (state is ResetPasswordStateResetPassowrdSuccess) {
GoRouter.of(context).pop();
showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}');
}
},
),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:developer';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -49,6 +50,8 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
bool weightError = false;
bool heightError = false;
bool incorrectEmailFormatError = false;
bool showVerificationButton = false;
String? verificationType; // 'email' or 'phone'
// Состояние для выбора пола
bool isMaleSelected = true;
@@ -221,7 +224,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
hasError: emailExistError || incorrectEmailFormatError,
onChanged: (newValue) {},
),
if (emailExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такая почта уже существует.')],
if (emailExistError) ...[
const SizedBox(height: 4),
const ValidationErrorWidget(error: 'Такая почта уже существует.'),
if (showVerificationButton && verificationType == 'email') ...[
const SizedBox(height: 12),
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => _handleVerificationRequest(context, 'email'),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
child: const Center(
child: Text(
'Да, это мой аккаунт, подтвердить',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white),
),
),
),
),
],
],
if (incorrectEmailFormatError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат email')],
const SizedBox(height: 12),
const Text('Номер телефона'),
@@ -235,7 +259,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
hasError: phoneExistError,
onChanged: (newValue) {},
),
if (phoneExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такой номер телефона уже существует.')],
if (phoneExistError) ...[
const SizedBox(height: 4),
const ValidationErrorWidget(error: 'Такой номер телефона уже существует.'),
if (showVerificationButton && verificationType == 'phone') ...[
const SizedBox(height: 12),
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => _handleVerificationRequest(context, 'phone'),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
child: const Center(
child: Text(
'Да, это мой аккаунт, подтвердить',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white),
),
),
),
),
],
],
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -293,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 кг')],
],
),
@@ -306,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 см')],
],
),
@@ -501,6 +558,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
clientId: clientID,
);
if (errors != null) {
bool hasEmailExist = false;
bool hasPhoneExist = false;
// Сначала определяем, какие ошибки присутствуют
for (final error in errors) {
switch (error) {
case RegistrationValidationErrors.incorrectEmailFormat:
@@ -509,14 +570,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
});
break;
case RegistrationValidationErrors.emailExist:
setState(() {
emailExistError = true;
});
hasEmailExist = true;
break;
case RegistrationValidationErrors.phoneExist:
setState(() {
phoneExistError = true;
});
hasPhoneExist = true;
break;
case RegistrationValidationErrors.incorrectDateOfBirthFormat:
setState(() {
@@ -525,8 +582,236 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
break;
}
}
// Устанавливаем verificationType согласно правилам:
// - Если оба (email и phone) существуют -> 'email'
// - Если только email -> 'email'
// - Если только phone -> 'phone'
if (hasEmailExist || hasPhoneExist) {
setState(() {
emailExistError = hasEmailExist;
phoneExistError = hasPhoneExist;
showVerificationButton = true;
if (hasEmailExist && hasPhoneExist) {
verificationType = 'email'; // Если оба существуют, используем email
} else if (hasEmailExist) {
verificationType = 'email'; // Только email
} else {
verificationType = 'phone'; // Только phone
}
});
}
// Если есть ошибки, возвращаемся и не показываем toast об успехе
return;
}
// Регистрация успешна только если нет ошибок
showSuccessToast('Регистрация успешна');
}
Future<void> _handleVerificationRequest(BuildContext context, String type) async {
try {
// Используем service locator для доступа к OAuthCubit
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
// Всегда передаем и email и phone
final String email = emailController.text;
final String phone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), '');
final response = await oauthCubit.requestOAuthVerification(email: email, phone: phone);
if (response != null && context.mounted) {
// Показываем диалог для ввода кода
final displayText = response.email;
_showVerificationCodeDialog(context, type, displayText);
} else if ((emailController.text.isEmpty || phoneController.text.isEmpty) && context.mounted) {
showErrorToast('Пожалуйста, заполните все поля');
} else if (context.mounted) {
showErrorToast('Не удалось отправить код верификации');
}
} catch (e) {
if (context.mounted) {
showErrorToast('Ошибка: ${e.toString()}');
}
}
}
void _showVerificationCodeDialog(BuildContext context, String verificationType, String displayText) {
final List<FocusNode> focusNodes = List.generate(4, (_) => FocusNode());
final List<TextEditingController> controllers = List.generate(4, (_) => TextEditingController());
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => AlertDialog(
title: const Text('Подтверждение аккаунта'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Код отправлен на $displayText'),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(4, (index) {
return SizedBox(
width: 50,
child: TextField(
controller: controllers[index],
focusNode: focusNodes[index],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
decoration: const InputDecoration(counterText: '', border: OutlineInputBorder()),
onChanged: (value) {
if (value.isNotEmpty && index < 3) {
focusNodes[index + 1].requestFocus();
}
if (value.isEmpty && index > 0) {
focusNodes[index - 1].requestFocus();
}
// Проверяем, заполнен ли весь код
final code = controllers.map((c) => c.text).join();
if (code.length == 4) {
// Небольшая задержка перед отправкой, чтобы пользователь увидел последний символ
Future.delayed(const Duration(milliseconds: 100), () async {
if (dialogContext.mounted && context.mounted) {
await _handleVerificationConfirm(context, dialogContext, verificationType, code, controllers, focusNodes);
}
});
}
},
),
);
}),
),
],
),
),
actions: [
TextButton(
onPressed: () {
for (var controller in controllers) {
controller.dispose();
}
for (var node in focusNodes) {
node.dispose();
}
Navigator.of(dialogContext).pop();
},
child: const Text('Отмена'),
),
],
),
);
}
Future<void> _handleVerificationConfirm(
BuildContext widgetContext,
BuildContext dialogContext,
String verificationType,
String code,
List<TextEditingController> controllers,
List<FocusNode> focusNodes,
) async {
try {
// Используем service locator для доступа к OAuthCubit, так как контекст из диалога может не иметь доступа к провайдеру
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
final authResult = widget.authResult;
if (authResult == null) {
showErrorToast('Ошибка: данные авторизации не найдены');
return;
}
// Всегда передаем и email и phone (полный набор данных от провайдера)
// Используем данные из формы, если они есть, иначе из authResult
final String email = emailController.text.isNotEmpty ? emailController.text : (authResult.email ?? '');
final String phone = phoneController.text.isNotEmpty ? phoneController.text.replaceAll(RegExp(r'[^\d+]'), '') : (authResult.phone ?? '');
// Проверяем, что данные есть
if (email.isEmpty && phone.isEmpty) {
log('OAuth Verification Confirm: Both email and phone are empty');
showErrorToast('Ошибка: не указан email или телефон');
return;
}
// Проверяем, что все необходимые данные от провайдера есть
if (authResult.accessToken.isEmpty || authResult.clientId.isEmpty || authResult.userId.isEmpty) {
log('OAuth Verification Confirm: Missing required OAuth data');
showErrorToast('Ошибка: неполные данные авторизации');
return;
}
// Проверяем, что код не пустой
if (code.isEmpty || code.length != 4) {
showErrorToast('Введите полный код');
return;
}
// Получаем данные профиля из формы
final String firstName = firstNameController.text.isNotEmpty ? firstNameController.text : (authResult.firstName ?? '');
final String lastName = lastNameController.text.isNotEmpty ? lastNameController.text : (authResult.lastName ?? '');
final String dateOfBirth = dateOfBirthController.text.isNotEmpty ? dateOfBirthController.text : (authResult.birthDate ?? '');
final String gender = isMaleSelected ? 'male' : 'female';
final int weight = weightController.text.isNotEmpty ? (int.tryParse(weightController.text) ?? 0) : 0;
final int height = heightController.text.isNotEmpty ? (int.tryParse(heightController.text) ?? 0) : 0;
// Проверяем, что все обязательные поля заполнены
if (firstName.isEmpty || lastName.isEmpty || dateOfBirth.isEmpty || weight == 0 || height == 0) {
log('OAuth Verification Confirm: Missing required profile data');
showErrorToast('Ошибка: заполните все обязательные поля');
return;
}
// Логируем перед отправкой запроса
log('OAuth Verification Confirm: provider=${widget.provider}, verificationType=$verificationType, code=$code, email=$email, phone=$phone');
// Отправляем запрос на подтверждение с полным набором данных от провайдера
await oauthCubit.confirmOAuthVerification(
provider: widget.provider ?? 'social',
accessToken: authResult.accessToken,
refreshToken: authResult.refreshToken ?? '',
clientId: authResult.clientId,
id: authResult.userId,
email: email,
phone: phone,
code: code,
verificationType: verificationType,
firstName: firstName,
lastName: lastName,
dateOfBirth: dateOfBirth,
gender: gender,
weight: weight,
height: height,
);
// Закрываем диалог
for (var controller in controllers) {
controller.dispose();
}
for (var node in focusNodes) {
node.dispose();
}
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
}
// Перезагружаем веб-сокет и переходим на главный экран
if (widgetContext.mounted) {
_reconnectWebSocket(widgetContext);
}
} catch (e, stackTrace) {
log('OAuth Verification Confirm Error: $e', error: e, stackTrace: stackTrace);
if (widgetContext.mounted) {
showErrorToast('Неверный код. Попробуйте снова.');
}
// Очищаем поля
for (var controller in controllers) {
controller.clear();
}
if (focusNodes.isNotEmpty) {
focusNodes[0].requestFocus();
}
}
}
}

View File

@@ -1,6 +1,10 @@
import 'dart:developer';
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
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_response.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
@@ -19,7 +23,6 @@ class AuthRepository {
}
}
Future<TokenModel> signInViaSocialMedia({
required String provider,
required String accessToken,
@@ -73,4 +76,67 @@ class AuthRepository {
rethrow;
}
}
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
try {
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;
}
}
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({
required String provider,
required String accessToken,
required String refreshToken,
required String clientId,
required String id,
String? email,
String? phone,
required String code,
required String verificationType,
required String firstName,
required String lastName,
required String dateOfBirth,
required String gender,
required int weight,
required int height,
}) async {
try {
final request = OAuthVerificationConfirmRequest(
provider: provider,
accessToken: accessToken,
refreshToken: refreshToken,
clientId: clientId,
id: id,
email: email,
phone: phone,
code: code,
verificationType: verificationType,
firstName: firstName,
lastName: lastName,
dateOfBirth: dateOfBirth,
gender: gender,
weight: weight,
height: height,
);
// Логируем запрос перед отправкой
log(
'AuthRepository: Sending OAuth verification confirm request: provider=$provider, verificationType=$verificationType, code=$code, email=$email, phone=$phone',
);
return await authService.confirmOAuthVerification(body: request);
} catch (e) {
log('AuthRepository: Error in confirmOAuthVerification: $e');
rethrow;
}
}
}

View File

@@ -2,6 +2,9 @@ import 'package:dio/dio.dart';
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
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_response.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:retrofit/retrofit.dart';
@@ -19,4 +22,10 @@ abstract class AuthService {
@POST('/api/oauth/login')
Future<TokenModel> signInViaSocialMedia({@Body() required OAuthLoginRequest body});
@POST('/api/oauth/verification/request')
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,24 @@
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

@@ -100,10 +100,10 @@ class MealSummaryWidget extends StatelessWidget {
fats = (caloriesInfo.fats * portion.quantity) / 100;
carbohydrates = (caloriesInfo.carbohydrates * portion.quantity) / 100;
} else {
calories = caloriesInfo.energyOnPortion;
proteins = caloriesInfo.proteinsOnPortion;
fats = caloriesInfo.fatsOnPortion;
carbohydrates = caloriesInfo.carbohydratesOnPortion;
calories = caloriesInfo.energyOnPortion * portion.quantity;
proteins = caloriesInfo.proteinsOnPortion * portion.quantity;
fats = caloriesInfo.fatsOnPortion * portion.quantity;
carbohydrates = caloriesInfo.carbohydratesOnPortion * portion.quantity;
}
totalCalories += calories;

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

@@ -1,378 +1,388 @@
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart';
import 'package:eatvio_app/features/registration/presentation/pages/additional_information_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/allergies_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/blood_pressure_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/body_circumferences_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/city_of_living_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/credentials_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/date_of_birth_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/favorite_foods_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/favorite_sports_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/gender_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/height_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/medicine_intake_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/name_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/phone_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/weight_screen.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:validators/validators.dart';
class RegistrationScreen extends StatefulWidget {
const RegistrationScreen({super.key});
@override
State<RegistrationScreen> createState() => _RegistrationScreenState();
}
class _RegistrationScreenState extends State<RegistrationScreen> {
double progressValue = 1 / 15;
int step = 1;
final pageController = PageController();
final RegistrationCubit registrationCubit = sl<RegistrationCubit>();
@override
void initState() {
super.initState();
RegistrationAnalyticsService.reportRegistrationStep(step);
}
@override
Widget build(BuildContext context) {
return BlocProvider<RegistrationCubit>(
create: (BuildContext context) => registrationCubit,
child: BlocListener<RegistrationCubit, RegistrationState>(
listener: (context, state) {
if (state.error != null && state.error!.isNotEmpty) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка при регистрации'),
content: Text(state.error!),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
}
},
child: BlocBuilder<RegistrationCubit, RegistrationState>(
builder: (BuildContext context, RegistrationState state) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.2,
padding: const EdgeInsets.only(left: 16, right: 4),
child: FloatingActionButton.extended(
heroTag: 'backButtonRegistration',
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
},
label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400),
),
),
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: appTheme.blueA400,
onPressed: () async {
if (await canGoNext(state, context)) {
setState(() {
pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
}
},
label: Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
),
],
),
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(icon: const Icon(Icons.close_rounded), onPressed: () => GoRouter.of(context).pop()),
),
centerTitle: true,
title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 48, fit: BoxFit.contain),
actions: [Padding(padding: const EdgeInsets.only(right: 16), child: Text('шаг $step из 15'))],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
padding: const EdgeInsets.all(16),
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(begin: 1 / 15, end: progressValue),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
minHeight: 8,
backgroundColor: Colors.grey.shade100,
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
),
),
),
),
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: (index) {
setState(() {
step = index + 1;
progressValue = step / 15;
});
// Report analytics for step completion
RegistrationAnalyticsService.reportRegistrationStep(step);
},
children: [
// LanguageSelectScreen(sheet: state.sheet),
PhoneScreen(sheet: state.sheet),
CredentialsScreen(sheet: state.sheet),
NameScreen(sheet: state.sheet),
GenderScreen(sheet: state.sheet),
DateOfBirthScreen(sheet: state.sheet),
CityOfLivingScreen(sheet: state.sheet),
BodyCircumferencesScreen(sheet: state.sheet),
WeightScreen(sheet: state.sheet),
HeightScreen(sheet: state.sheet),
BloodPressureScreen(sheet: state.sheet),
AllergiesScreen(sheet: state.sheet),
MedicineIntakeScreen(sheet: state.sheet),
FavoriteSportsScreen(sheet: state.sheet),
FavoriteFoodsScreen(sheet: state.sheet),
AdditionalInformationScreen(sheet: state.sheet),
],
),
);
},
),
),
);
}
Future<bool> canGoNext(RegistrationState state, BuildContext context) async {
switch (step) {
case 1:
if (state.sheet.phone.length != 18) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Введите корректный номер телефона'),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
} else {
return true;
}
case 2:
List<String> errors = validateCredentialsFields(state.sheet.email, state.sheet.password, state.sheet.passwordConfirmation, context);
if (errors.isEmpty) {
return true;
} else {
String errorMessage = '';
for (var error in errors) {
errorMessage += '$error\n\n';
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: Text(errorMessage),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 3:
List<String> errors = validateNameFields(state.sheet.firstName.toString(), state.sheet.lastName.toString(), context);
if (errors.isEmpty) {
return true;
} else {
String errorMessage = '';
for (var error in errors) {
errorMessage += '$error\n\n';
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: Text(errorMessage),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 4:
if (state.sheet.gender.isNotEmpty) {
return true;
} else {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Выберете пол'),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 6:
if (state.sheet.location.isNotEmpty) {
return true;
} else {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Заполните все поля'),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 7:
List<String> errors = validateCircumferencesFields(
state.sheet.chestCircumference.toString(),
state.sheet.waistCircumference.toString(),
state.sheet.hipCircumference.toString(),
context,
);
if (errors.isEmpty) {
return true;
} else {
String errorMessage = '';
for (var error in errors) {
errorMessage += '$error\n\n';
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: Text(errorMessage),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 15:
try {
showDialog(
barrierDismissible: false,
useSafeArea: false,
context: context,
builder: (context) => const PopScope(canPop: false, child: Loader()),
);
await registrationCubit.tryRegistration();
if (context.mounted) {
Navigator.pop(context);
}
} catch (e) {
if (context.mounted) {
Navigator.pop(context);
showDialog(
context: context,
builder: (context) => const AlertDialog(title: Text('Ошибка при попытке регистрации')),
);
}
}
return false;
default:
return true;
}
}
List<String> validateCircumferencesFields(String chestCircumference, String waistCircumference, String hipCircumference, BuildContext context) {
final s = S.of(context);
List<String> errors = [];
if (chestCircumference.isEmpty | waistCircumference.isEmpty | hipCircumference.isEmpty) {
errors.add(s.circumference_empty);
}
if (![chestCircumference, waistCircumference, hipCircumference].every((circumference) => int.tryParse(circumference) != null)) {
errors.add(s.circumference_is_not_a_number);
}
return errors;
}
List<String> validateCredentialsFields(String email, String password, String passwordConfirmation, BuildContext context) {
final s = S.of(context);
List<String> errors = [];
if (!isEmail(email)) {
errors.add(s.email_invalid);
}
if (email.isEmpty) {
errors.add(s.email_empty);
}
if (password.length < 6) {
errors.add(s.password_short);
}
if (password != passwordConfirmation) {
errors.add(s.password_confirmation_mismatch);
}
return errors;
}
List<String> validateNameFields(String firstName, String lastName, BuildContext context) {
List<String> errors = [];
if (firstName.isEmpty) {
errors.add('First name cannot be empty');
}
if (firstName.isEmpty) {
errors.add('First name is too short');
}
if (lastName.isEmpty) {
errors.add('Last name cannot be empty');
}
if (lastName.isEmpty) {
errors.add('Last name is too short');
}
return errors;
}
}
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart';
import 'package:eatvio_app/features/registration/presentation/pages/additional_information_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/allergies_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/blood_pressure_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/body_circumferences_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/city_of_living_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/credentials_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/date_of_birth_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/favorite_foods_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/favorite_sports_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/gender_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/height_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/medicine_intake_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/name_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/phone_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/weight_screen.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:validators/validators.dart';
class RegistrationScreen extends StatefulWidget {
const RegistrationScreen({super.key});
@override
State<RegistrationScreen> createState() => _RegistrationScreenState();
}
class _RegistrationScreenState extends State<RegistrationScreen> {
double progressValue = 1 / 15;
int step = 1;
final pageController = PageController();
final RegistrationCubit registrationCubit = sl<RegistrationCubit>();
@override
void initState() {
super.initState();
RegistrationAnalyticsService.reportRegistrationStep(step);
}
@override
Widget build(BuildContext context) {
return BlocProvider<RegistrationCubit>(
create: (BuildContext context) => registrationCubit,
child: BlocListener<RegistrationCubit, RegistrationState>(
listener: (context, state) {
if (state.error != null && state.error!.isNotEmpty) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка при регистрации'),
content: Text(state.error!),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
}
},
child: BlocBuilder<RegistrationCubit, RegistrationState>(
builder: (BuildContext context, RegistrationState state) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.2,
padding: const EdgeInsets.only(left: 16, right: 4),
child: FloatingActionButton.extended(
heroTag: 'backButtonRegistration',
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
},
label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400),
),
),
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: appTheme.blueA400,
onPressed: () async {
if (await canGoNext(state, context)) {
setState(() {
pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
}
},
label: Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
),
],
),
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(icon: const Icon(Icons.close_rounded), onPressed: () => GoRouter.of(context).pop()),
),
centerTitle: true,
title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 48, fit: BoxFit.contain),
actions: [Padding(padding: const EdgeInsets.only(right: 16), child: Text('шаг $step из 15'))],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
padding: const EdgeInsets.all(16),
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(begin: 1 / 15, end: progressValue),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
minHeight: 8,
backgroundColor: Colors.grey.shade100,
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
),
),
),
),
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: (index) {
setState(() {
step = index + 1;
progressValue = step / 15;
});
// Report analytics for step completion
RegistrationAnalyticsService.reportRegistrationStep(step);
},
children: [
// LanguageSelectScreen(sheet: state.sheet),
PhoneScreen(sheet: state.sheet),
CredentialsScreen(sheet: state.sheet),
NameScreen(sheet: state.sheet),
GenderScreen(sheet: state.sheet),
DateOfBirthScreen(sheet: state.sheet),
CityOfLivingScreen(sheet: state.sheet),
BodyCircumferencesScreen(sheet: state.sheet),
WeightScreen(sheet: state.sheet),
HeightScreen(sheet: state.sheet),
BloodPressureScreen(sheet: state.sheet),
AllergiesScreen(sheet: state.sheet),
MedicineIntakeScreen(sheet: state.sheet),
FavoriteSportsScreen(sheet: state.sheet),
FavoriteFoodsScreen(sheet: state.sheet),
AdditionalInformationScreen(sheet: state.sheet),
],
),
);
},
),
),
);
}
Future<bool> canGoNext(RegistrationState state, BuildContext context) async {
switch (step) {
case 1:
if (state.sheet.phone.length != 18) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Введите корректный номер телефона'),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
} else {
return true;
}
case 2:
List<String> errors = validateCredentialsFields(state.sheet.email, state.sheet.password, state.sheet.passwordConfirmation, context);
if (errors.isEmpty) {
return true;
} else {
String errorMessage = '';
for (var error in errors) {
errorMessage += '$error\n\n';
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: Text(errorMessage),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 3:
List<String> errors = validateNameFields(state.sheet.firstName.toString(), state.sheet.lastName.toString(), context);
if (errors.isEmpty) {
return true;
} else {
String errorMessage = '';
for (var error in errors) {
errorMessage += '$error\n\n';
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: Text(errorMessage),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 4:
if (state.sheet.gender.isNotEmpty) {
return true;
} else {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Выберете пол'),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 6:
if (state.sheet.location.isNotEmpty) {
return true;
} else {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Заполните все поля'),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 7:
List<String> errors = validateCircumferencesFields(
state.sheet.chestCircumference.toString(),
state.sheet.waistCircumference.toString(),
state.sheet.hipCircumference.toString(),
context,
);
if (errors.isEmpty) {
return true;
} else {
String errorMessage = '';
for (var error in errors) {
errorMessage += '$error\n\n';
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: Text(errorMessage),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 15:
try {
showDialog(
barrierDismissible: false,
useSafeArea: false,
context: context,
builder: (context) => const PopScope(canPop: false, child: Loader()),
);
await registrationCubit.tryRegistration();
if (context.mounted) {
Navigator.pop(context);
}
} catch (e) {
if (context.mounted) {
Navigator.pop(context);
String errorMessage = '';
if (e is String) {
errorMessage = e;
} else {
errorMessage = e.toString().replaceFirst('Exception: ', '').replaceFirst('Error: ', '');
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка при регистрации'),
content: Text(errorMessage.isNotEmpty ? errorMessage : 'Регистрация не удалась'),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
}
}
return false;
default:
return true;
}
}
List<String> validateCircumferencesFields(String chestCircumference, String waistCircumference, String hipCircumference, BuildContext context) {
final s = S.of(context);
List<String> errors = [];
if (chestCircumference.isEmpty | waistCircumference.isEmpty | hipCircumference.isEmpty) {
errors.add(s.circumference_empty);
}
if (![chestCircumference, waistCircumference, hipCircumference].every((circumference) => int.tryParse(circumference) != null)) {
errors.add(s.circumference_is_not_a_number);
}
return errors;
}
List<String> validateCredentialsFields(String email, String password, String passwordConfirmation, BuildContext context) {
final s = S.of(context);
List<String> errors = [];
if (!isEmail(email)) {
errors.add(s.email_invalid);
}
if (email.isEmpty) {
errors.add(s.email_empty);
}
if (password.length < 6) {
errors.add(s.password_short);
}
if (password != passwordConfirmation) {
errors.add(s.password_confirmation_mismatch);
}
return errors;
}
List<String> validateNameFields(String firstName, String lastName, BuildContext context) {
List<String> errors = [];
if (firstName.isEmpty) {
errors.add('First name cannot be empty');
}
if (firstName.isEmpty) {
errors.add('First name is too short');
}
if (lastName.isEmpty) {
errors.add('Last name cannot be empty');
}
if (lastName.isEmpty) {
errors.add('Last name is too short');
}
return errors;
}
}

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,238 +1,266 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:eatvio_app/features/registration_v2/models/registration_model.dart';
import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gpassword/gpassword.dart';
class RegistrationCubitV2 extends Cubit<RegistrationStateV2> {
final RegistrationRepositoryV2 registrationRepository;
RegistrationCubitV2({required this.registrationRepository}) : super(RegistrationStateV2());
void setFirstName(String firstName) {
emit(state.copyWith(firstName: firstName));
validateFirstName();
}
void setLastName(String lastName) {
emit(state.copyWith(lastName: lastName));
validateLastName();
}
void setEmail(String email) {
emit(state.copyWith(email: email));
validateEmail();
}
void setPhone(String phone) {
emit(state.copyWith(phone: phone));
validatePhone();
}
void setBirthday(DateTime birthday) {
emit(state.copyWith(birthday: birthday));
validateBirthday();
}
void setGender(String gender) {
emit(state.copyWith(gender: gender));
validateGender();
}
void setWeight(int weight) {
emit(state.copyWith(weight: weight));
validateWeight();
}
void setHeight(int height) {
emit(state.copyWith(height: height));
validateHeight();
}
void setActivityCoefficient(double activityCoefficient) {
emit(state.copyWith(activityCoefficient: activityCoefficient));
validateActivityCoefficient();
}
void setPassword(String password) {
emit(state.copyWith(password: password));
}
bool validateFirstName() {
if (state.firstName.isEmpty) {
emit(state.copyWith(isFirstNameValid: false));
return false;
}
emit(state.copyWith(isFirstNameValid: true));
return true;
}
bool validateLastName() {
if (state.lastName.isEmpty) {
emit(state.copyWith(isLastNameValid: false));
return false;
}
emit(state.copyWith(isLastNameValid: true));
return true;
}
bool validateEmail() {
if (state.email.isEmpty) {
emit(state.copyWith(isEmailValid: false));
return false;
}
emit(state.copyWith(isEmailValid: true));
return true;
}
bool validatePhone() {
if (state.phone.isEmpty) {
emit(state.copyWith(isPhoneValid: false));
return false;
}
emit(state.copyWith(isPhoneValid: true));
return true;
}
bool validateGender() {
if (state.gender.isEmpty) {
emit(state.copyWith(isGenderValid: false));
return false;
}
emit(state.copyWith(isGenderValid: true));
return true;
}
bool validateWeight() {
if (state.weight == 0) {
emit(state.copyWith(isWeightValid: false));
return false;
}
emit(state.copyWith(isWeightValid: true));
return true;
}
bool validateHeight() {
if (state.height == 0) {
emit(state.copyWith(isHeightValid: false));
return false;
}
emit(state.copyWith(isHeightValid: true));
return true;
}
bool validateActivityCoefficient() {
if (state.activityCoefficient == 0) {
emit(state.copyWith(isActivityCoefficientValid: false));
return false;
}
emit(state.copyWith(isActivityCoefficientValid: true));
return true;
}
bool validateBirthday() {
if (state.birthday == null) {
emit(state.copyWith(isBirthdayValid: false));
return false;
}
emit(state.copyWith(isBirthdayValid: true));
return true;
}
void nextStep() {
emit(state.copyWith(step: 1));
}
void prevStep() {
emit(state.copyWith(step: 0));
}
bool validateAllFields() {
if (!validateFirstName() ||
!validateLastName() ||
!validateEmail() ||
!validatePhone() ||
!validateGender() ||
!validateWeight() ||
!validateHeight() ||
!validateActivityCoefficient() ||
!validateBirthday()) {
return false;
}
return true;
}
Future<(bool, bool)> register() async {
if (!validateAllFields()) {
return (false, true);
}
final birthday = state.birthday?.format('yyyy-MM-dd');
if (birthday == null) {
validateBirthday();
return (false, true);
}
final password = await _generatePassword();
final registrationModel = RegistrationModel(
firstName: state.firstName,
lastName: state.lastName,
email: state.email,
phone: state.phone,
gender: state.gender,
password: password,
confirmPassword: password,
weight: state.weight,
height: state.height,
activityCoefficient: state.activityCoefficient,
birthday: birthday,
);
bool didApiComplainOnValidation = false;
try {
final token = await registrationRepository.register(registrationModel: registrationModel);
if (token != null) {
TokenStorage.saveTokenInStorage(token);
return (true, false);
}
} on DioException catch (e) {
final errors = e.response?.data['errors'] as Map<String, dynamic>?;
final emailErrors = errors?['email'] as List<dynamic>?;
final phoneErrors = errors?['phone'] as List<dynamic>?;
final dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
if (emailErrors != null && emailErrors.isNotEmpty) {
if (emailErrors.contains('Некорректный формат email')) {
emit(state.copyWith(isEmailValid: false, emailDisplayError: 'Некорректный формат email'));
didApiComplainOnValidation = true;
}
if (emailErrors.contains('Такой Эл. почта уже существует.')) {
emit(state.copyWith(isEmailValid: false, emailDisplayError: 'Такая Эл. почта уже существует.'));
didApiComplainOnValidation = true;
}
}
if (phoneErrors != null && phoneErrors.isNotEmpty) {
if (phoneErrors.contains('Такой phone уже существует.')) {
emit(state.copyWith(isPhoneValid: false, phoneDisplayError: 'Такой номер телефона уже существует.'));
didApiComplainOnValidation = true;
}
}
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
if (dateOfBirth.contains('Некорректный формат даты рождения')) {
emit(state.copyWith(isBirthdayValid: false, emailDisplayError: 'Некорректный формат даты рождения'));
didApiComplainOnValidation = true;
}
}
} catch (e) {
log(e.toString());
}
return (false, didApiComplainOnValidation);
}
Future<String> _generatePassword() async {
final generator = GPassword();
final password = generator.generate(passwordLength: 10);
emit(state.copyWith(password: password));
return password;
}
}
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:eatvio_app/features/registration_v2/models/registration_model.dart';
import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gpassword/gpassword.dart';
class RegistrationCubitV2 extends Cubit<RegistrationStateV2> {
final RegistrationRepositoryV2 registrationRepository;
RegistrationCubitV2({required this.registrationRepository}) : super(const RegistrationStateV2());
void setFirstName(String firstName) {
emit(state.copyWith(firstName: firstName));
validateFirstName();
}
void setLastName(String lastName) {
emit(state.copyWith(lastName: lastName));
validateLastName();
}
void setEmail(String email) {
emit(state.copyWith(email: email));
validateEmail();
}
void setPhone(String phone) {
emit(state.copyWith(phone: phone));
validatePhone();
}
void setBirthday(DateTime birthday) {
emit(state.copyWith(birthday: birthday));
validateBirthday();
}
void setGender(String gender) {
emit(state.copyWith(gender: gender));
validateGender();
}
void setWeight(int weight) {
emit(state.copyWith(weight: weight));
validateWeight();
}
void setHeight(int height) {
emit(state.copyWith(height: height));
validateHeight();
}
void setActivityCoefficient(double activityCoefficient) {
emit(state.copyWith(activityCoefficient: activityCoefficient));
validateActivityCoefficient();
}
void setPassword(String password) {
emit(state.copyWith(password: password));
}
bool validateFirstName() {
if (state.firstName.isEmpty) {
emit(state.copyWith(isFirstNameValid: false));
return false;
}
emit(state.copyWith(isFirstNameValid: true));
return true;
}
bool validateLastName() {
if (state.lastName.isEmpty) {
emit(state.copyWith(isLastNameValid: false));
return false;
}
emit(state.copyWith(isLastNameValid: true));
return true;
}
bool validateEmail() {
if (state.email.isEmpty) {
emit(state.copyWith(isEmailValid: false));
return false;
}
emit(state.copyWith(isEmailValid: true));
return true;
}
bool validatePhone() {
if (state.phone.isEmpty) {
emit(state.copyWith(isPhoneValid: false));
return false;
}
emit(state.copyWith(isPhoneValid: true));
return true;
}
bool validateGender() {
if (state.gender.isEmpty) {
emit(state.copyWith(isGenderValid: false));
return false;
}
emit(state.copyWith(isGenderValid: true));
return true;
}
bool validateWeight() {
if (state.weight == 0) {
emit(state.copyWith(isWeightValid: false));
return false;
}
emit(state.copyWith(isWeightValid: true));
return true;
}
bool validateHeight() {
if (state.height == 0) {
emit(state.copyWith(isHeightValid: false));
return false;
}
emit(state.copyWith(isHeightValid: true));
return true;
}
bool validateActivityCoefficient() {
if (state.activityCoefficient == 0) {
emit(state.copyWith(isActivityCoefficientValid: false));
return false;
}
emit(state.copyWith(isActivityCoefficientValid: true));
return true;
}
bool validateBirthday() {
if (state.birthday == null) {
emit(state.copyWith(isBirthdayValid: false));
return false;
}
emit(state.copyWith(isBirthdayValid: true));
return true;
}
void nextStep() {
emit(state.copyWith(step: 1));
}
void prevStep() {
emit(state.copyWith(step: 0));
}
bool validateAllFields() {
if (!validateFirstName() ||
!validateLastName() ||
!validateEmail() ||
!validatePhone() ||
!validateGender() ||
!validateWeight() ||
!validateHeight() ||
!validateActivityCoefficient() ||
!validateBirthday()) {
return false;
}
return true;
}
Future<(bool, bool)> register() async {
if (!validateAllFields()) {
return (false, true);
}
final birthday = state.birthday?.format('yyyy-MM-dd');
if (birthday == null) {
validateBirthday();
return (false, true);
}
final password = await _generatePassword();
final registrationModel = RegistrationModel(
firstName: state.firstName,
lastName: state.lastName,
email: state.email,
phone: state.phone,
gender: state.gender,
password: password,
confirmPassword: password,
weight: state.weight,
height: state.height,
activityCoefficient: state.activityCoefficient,
birthday: birthday,
);
bool didApiComplainOnValidation = false;
String errorMessage = '';
try {
final token = await registrationRepository.register(registrationModel: registrationModel);
if (token != null) {
TokenStorage.saveTokenInStorage(token);
emit(state.copyWith(registrationError: null));
return (true, false);
}
} on DioException catch (e) {
final errors = e.response?.data['errors'] as Map<String, dynamic>?;
final emailErrors = errors?['email'] as List<dynamic>?;
final phoneErrors = errors?['phone'] as List<dynamic>?;
final dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
// Собираем все ошибки для отображения в диалоге
List<String> allErrors = [];
if (emailErrors != null && emailErrors.isNotEmpty) {
for (var error in emailErrors) {
allErrors.add(error.toString());
if (error.toString() == 'Некорректный формат email') {
emit(state.copyWith(isEmailValid: false, emailDisplayError: 'Некорректный формат email'));
didApiComplainOnValidation = true;
}
if (error.toString() == 'Такой Эл. почта уже существует.') {
emit(state.copyWith(isEmailValid: false, emailDisplayError: 'Такая Эл. почта уже существует.'));
didApiComplainOnValidation = true;
}
}
}
if (phoneErrors != null && phoneErrors.isNotEmpty) {
for (var error in phoneErrors) {
allErrors.add(error.toString());
if (error.toString() == 'Такой phone уже существует.') {
emit(state.copyWith(isPhoneValid: false, phoneDisplayError: 'Такой номер телефона уже существует.'));
didApiComplainOnValidation = true;
}
}
}
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
for (var error in dateOfBirth) {
allErrors.add(error.toString());
if (error.toString() == 'Некорректный формат даты рождения') {
emit(state.copyWith(isBirthdayValid: false, dateOfBirthDisplayError: 'Некорректный формат даты рождения'));
didApiComplainOnValidation = true;
}
}
}
// Сохраняем все ошибки для отображения в диалоге
if (allErrors.isNotEmpty) {
errorMessage = allErrors.join('\n');
emit(state.copyWith(registrationError: errorMessage));
} else {
// Если нет конкретных ошибок, берем общее сообщение
final message = e.response?.data['message'] as String?;
errorMessage = message ?? 'Ошибка при регистрации';
emit(state.copyWith(registrationError: errorMessage));
}
} catch (e) {
log(e.toString());
errorMessage = e.toString();
emit(state.copyWith(registrationError: errorMessage));
}
return (false, didApiComplainOnValidation);
}
Future<String> _generatePassword() async {
final generator = GPassword();
final password = generator.generate(passwordLength: 10);
emit(state.copyWith(password: password));
return password;
}
}

View File

@@ -1,32 +1,33 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'registration_state.freezed.dart';
@freezed
abstract class RegistrationStateV2 with _$RegistrationStateV2 {
factory RegistrationStateV2({
@Default('') String firstName,
@Default('') String lastName,
@Default('') String email,
@Default('') String phone,
@Default('') String gender,
@Default('') String password,
@Default(0) int weight,
@Default(0) int height,
@Default(1.2) double activityCoefficient,
DateTime? birthday,
@Default(true) bool isFirstNameValid,
@Default(true) bool isLastNameValid,
@Default(true) bool isEmailValid,
@Default(true) bool isPhoneValid,
@Default(true) bool isGenderValid,
@Default(true) bool isWeightValid,
@Default(true) bool isHeightValid,
@Default(true) bool isActivityCoefficientValid,
@Default(true) bool isBirthdayValid,
@Default(0) int step,
@Default('') String emailDisplayError,
@Default('') String phoneDisplayError,
@Default('') String dateOfBirthDisplayError,
}) = _RegistrationStateV2;
}
import 'package:freezed_annotation/freezed_annotation.dart';
part 'registration_state.freezed.dart';
@freezed
abstract class RegistrationStateV2 with _$RegistrationStateV2 {
const factory RegistrationStateV2({
@Default('') String firstName,
@Default('') String lastName,
@Default('') String email,
@Default('') String phone,
@Default('') String gender,
@Default('') String password,
@Default(0) int weight,
@Default(0) int height,
@Default(1.2) double activityCoefficient,
DateTime? birthday,
@Default(true) bool isFirstNameValid,
@Default(true) bool isLastNameValid,
@Default(true) bool isEmailValid,
@Default(true) bool isPhoneValid,
@Default(true) bool isGenderValid,
@Default(true) bool isWeightValid,
@Default(true) bool isHeightValid,
@Default(true) bool isActivityCoefficientValid,
@Default(true) bool isBirthdayValid,
@Default(0) int step,
@Default('') String emailDisplayError,
@Default('') String phoneDisplayError,
@Default('') String dateOfBirthDisplayError,
String? registrationError,
}) = _RegistrationStateV2;
}

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),
@@ -125,7 +137,8 @@ class _SecondStepContentState extends State<SecondStepContent> {
setState(() {
isLoading = false;
});
if (context.mounted && !hasValidationError) {
if (context.mounted) {
// Показываем bottom sheet всегда: при успехе или ошибке
showRegistrationBottomSheet(context, cubit, commonResult);
}
},
@@ -145,7 +158,7 @@ class _SecondStepContentState extends State<SecondStepContent> {
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: 'Нажимая “Зарегистрироваться”, Вы соглашаетесь \n',
text: 'Нажимая "Зарегистрироваться", Вы соглашаетесь \n',
style: const TextStyle(fontSize: 12, color: Colors.black),
children: [
const TextSpan(text: 'c '),

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),
@@ -96,7 +96,10 @@ class _RegistrationBottomSheetState extends State<RegistrationBottomSheet> {
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Text(s.error_desc, style: TextStyles.textMMediumAuto.copyWith(color: appTheme.darkGrey)),
child: Text(
widget.cubit.state.registrationError?.isNotEmpty == true ? widget.cubit.state.registrationError! : s.error_desc,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.darkGrey),
),
),
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.7+87
version: 1.3.11+92
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,138 +1,160 @@
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,73 +1,95 @@
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,87 +1,109 @@
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,36 +1,58 @@
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,62 +1,84 @@
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,32 +1,54 @@
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,29 +1,51 @@
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,22 +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,112 +1,134 @@
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,36 +1,58 @@
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,30 +1,52 @@
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,66 +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,118 +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,55 +1,77 @@
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,137 +1,159 @@
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,98 +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,166 +1,163 @@
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,59 +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,44 +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,98 +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,78 +1,100 @@
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,67 +1,89 @@
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,78 @@
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/pages/forget_password_screen.dart';
import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'forget_password_screen_test.mocks.dart';
@GenerateMocks([ResetPasswordRepository])
void main() {
group('ForgetPasswordScreen', () {
late MockResetPasswordRepository mockResetPasswordRepository;
late ResetPasswordCubit resetPasswordCubit;
setUp(() async {
await sl.reset();
mockResetPasswordRepository = MockResetPasswordRepository();
resetPasswordCubit = ResetPasswordCubit(
resetPasswordRepository: mockResetPasswordRepository,
);
sl.registerLazySingleton<ResetPasswordCubit>(() => resetPasswordCubit);
});
tearDown(() {
resetPasswordCubit.close();
});
Widget createTestWidget({required bool isAuth}) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: ForgetPasswordScreen(isAuth: isAuth),
);
}
testWidgets('displays screen with correct title when isAuth is false', (WidgetTester tester) async {
await tester.pumpWidget(createTestWidget(isAuth: false));
await tester.pumpAndSettle();
expect(find.text('Восстановление пароля'), findsOneWidget);
});
testWidgets('displays screen with correct title when isAuth is true', (WidgetTester tester) async {
await tester.pumpWidget(createTestWidget(isAuth: true));
await tester.pumpAndSettle();
expect(find.text('Смена пароля'), findsOneWidget);
});
testWidgets('displays email field in initial state', (WidgetTester tester) async {
await tester.pumpWidget(createTestWidget(isAuth: false));
await tester.pumpAndSettle();
expect(find.text('E-mail'), findsOneWidget);
expect(find.text('Введите e-mail'), findsOneWidget);
});
testWidgets('displays email field in initial state', (WidgetTester tester) async {
await tester.pumpWidget(createTestWidget(isAuth: false));
await tester.pump(); // Use pump instead of pumpAndSettle
// Check that email field exists
final textFields = find.byType(TextFormField);
expect(textFields, findsWidgets);
});
testWidgets('displays button in initial state', (WidgetTester tester) async {
await tester.pumpWidget(createTestWidget(isAuth: false));
await tester.pump(); // Use pump instead of pumpAndSettle
// Check that button exists (either "Сбросить пароль" or "Сменить пароль")
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);
});
});
}

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