diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index e06b26ab..ad156380 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -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 = (
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 94c063d6..182a74af 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -21,7 +21,7 @@
CFBundleSignature
????
CFBundleVersion
- 87
+ $(FLUTTER_BUILD_NUMBER)
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/lib/features/auth/models/oauth_verification_confirm_request.dart b/lib/features/auth/models/oauth_verification_confirm_request.dart
new file mode 100644
index 00000000..88101ab2
--- /dev/null
+++ b/lib/features/auth/models/oauth_verification_confirm_request.dart
@@ -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 json) => _$OAuthVerificationConfirmRequestFromJson(json);
+}
diff --git a/lib/features/auth/models/oauth_verification_confirm_response.dart b/lib/features/auth/models/oauth_verification_confirm_response.dart
new file mode 100644
index 00000000..9333a03d
--- /dev/null
+++ b/lib/features/auth/models/oauth_verification_confirm_response.dart
@@ -0,0 +1,18 @@
+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 user,
+ @JsonKey(name: 'is_new_user') required bool isNewUser,
+ }) = _OAuthVerificationConfirmResponse;
+
+ factory OAuthVerificationConfirmResponse.fromJson(Map json) => _$OAuthVerificationConfirmResponseFromJson(json);
+}
+
diff --git a/lib/features/auth/models/oauth_verification_request.dart b/lib/features/auth/models/oauth_verification_request.dart
new file mode 100644
index 00000000..5d9c8408
--- /dev/null
+++ b/lib/features/auth/models/oauth_verification_request.dart
@@ -0,0 +1,12 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'oauth_verification_request.freezed.dart';
+part 'oauth_verification_request.g.dart';
+
+@freezed
+abstract class OAuthVerificationRequest with _$OAuthVerificationRequest {
+ const factory OAuthVerificationRequest({String? email, String? phone}) = _OAuthVerificationRequest;
+
+ factory OAuthVerificationRequest.fromJson(Map json) => _$OAuthVerificationRequestFromJson(json);
+}
+
diff --git a/lib/features/auth/models/oauth_verification_request_response.dart b/lib/features/auth/models/oauth_verification_request_response.dart
new file mode 100644
index 00000000..32787602
--- /dev/null
+++ b/lib/features/auth/models/oauth_verification_request_response.dart
@@ -0,0 +1,12 @@
+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 json) => _$OAuthVerificationRequestResponseFromJson(json);
+}
+
diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart
index b4f78b4a..22f65f42 100644
--- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart
+++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart
@@ -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 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?;
+ final emailErrors = errors?['email'] as List?;
+ final phoneErrors = errors?['phone'] as List?;
+
+ 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 with RegistrationDateCacheMixin {
final dateOfBirth = errors?['date_of_birth'] as List?;
final errorsList = [];
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,69 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin {
return null;
}
+
+ Future 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: email, phone: phone);
+ 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 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 }
diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart
index 1791e24e..2663dac2 100644
--- a/lib/features/auth/presentation/pages/register_via_social_screen.dart
+++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart
@@ -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 {
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 {
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 {
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,
@@ -501,6 +546,10 @@ class _RegisterViaSocialScreenState extends State {
clientId: clientID,
);
if (errors != null) {
+ bool hasEmailExist = false;
+ bool hasPhoneExist = false;
+
+ // Сначала определяем, какие ошибки присутствуют
for (final error in errors) {
switch (error) {
case RegistrationValidationErrors.incorrectEmailFormat:
@@ -509,14 +558,10 @@ class _RegisterViaSocialScreenState extends State {
});
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 +570,236 @@ class _RegisterViaSocialScreenState extends State {
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 _handleVerificationRequest(BuildContext context, String type) async {
+ try {
+ // Используем service locator для доступа к OAuthCubit
+ final oauthCubit = CubitRegistrations.sl();
+ // Всегда передаем и 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 (context.mounted) {
+ showErrorToast('Не удалось отправить код верификации');
+ }
+ }
+ } catch (e) {
+ if (context.mounted) {
+ showErrorToast('Ошибка: ${e.toString()}');
+ }
+ }
+ }
+
+ void _showVerificationCodeDialog(BuildContext context, String verificationType, String displayText) {
+ final List focusNodes = List.generate(4, (_) => FocusNode());
+ final List 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 _handleVerificationConfirm(
+ BuildContext widgetContext,
+ BuildContext dialogContext,
+ String verificationType,
+ String code,
+ List controllers,
+ List focusNodes,
+ ) async {
+ try {
+ // Используем service locator для доступа к OAuthCubit, так как контекст из диалога может не иметь доступа к провайдеру
+ final oauthCubit = CubitRegistrations.sl();
+ 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();
+ }
+ }
+ }
}
diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart
index a65f309c..da80bf68 100644
--- a/lib/features/auth/repositories/auth_repository.dart
+++ b/lib/features/auth/repositories/auth_repository.dart
@@ -1,6 +1,11 @@
+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.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 +24,6 @@ class AuthRepository {
}
}
-
Future signInViaSocialMedia({
required String provider,
required String accessToken,
@@ -73,4 +77,60 @@ class AuthRepository {
rethrow;
}
}
+
+ Future requestOAuthVerification({String? email, String? phone}) async {
+ try {
+ return await authService.requestOAuthVerification(
+ body: OAuthVerificationRequest(email: email, phone: phone),
+ );
+ } catch (e) {
+ rethrow;
+ }
+ }
+
+ Future 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;
+ }
+ }
}
diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart
index 41d35da7..34e624c0 100644
--- a/lib/features/auth/services/auth_service.dart
+++ b/lib/features/auth/services/auth_service.dart
@@ -2,6 +2,10 @@ 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.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 +23,10 @@ abstract class AuthService {
@POST('/api/oauth/login')
Future signInViaSocialMedia({@Body() required OAuthLoginRequest body});
+
+ @POST('/api/oauth/verification/request')
+ Future requestOAuthVerification({@Body() required OAuthVerificationRequest body});
+
+ @POST('/api/oauth/verification/confirm')
+ Future confirmOAuthVerification({@Body() required OAuthVerificationConfirmRequest body});
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 23aad87a..96bab2f8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
-version: 1.3.7+87
+version: 1.3.8+88
publish_to: 'none'
environment:
diff --git a/test/core/constants/app_constants_test.dart b/test/core/constants/app_constants_test.dart
index 012ea54f..8c10cae7 100644
--- a/test/core/constants/app_constants_test.dart
+++ b/test/core/constants/app_constants_test.dart
@@ -136,3 +136,7 @@ void main() {
+
+
+
+
diff --git a/test/core/cubit/state_utils_test.dart b/test/core/cubit/state_utils_test.dart
index bc2c0d2d..63c632ca 100644
--- a/test/core/cubit/state_utils_test.dart
+++ b/test/core/cubit/state_utils_test.dart
@@ -71,3 +71,7 @@ void main() {
+
+
+
+
diff --git a/test/core/data/selection_popup_model_test.dart b/test/core/data/selection_popup_model_test.dart
index 6f158b38..72b9e03d 100644
--- a/test/core/data/selection_popup_model_test.dart
+++ b/test/core/data/selection_popup_model_test.dart
@@ -85,3 +85,7 @@ void main() {
+
+
+
+
diff --git a/test/core/enums/prefs_keys_test.dart b/test/core/enums/prefs_keys_test.dart
index 05314dbf..d9394d38 100644
--- a/test/core/enums/prefs_keys_test.dart
+++ b/test/core/enums/prefs_keys_test.dart
@@ -34,3 +34,7 @@ void main() {
+
+
+
+
diff --git a/test/core/internet_connection_cubit/internet_connection_state_test.dart b/test/core/internet_connection_cubit/internet_connection_state_test.dart
index 85cd955b..68dac160 100644
--- a/test/core/internet_connection_cubit/internet_connection_state_test.dart
+++ b/test/core/internet_connection_cubit/internet_connection_state_test.dart
@@ -60,3 +60,7 @@ void main() {
+
+
+
+
diff --git a/test/core/prefs_keys_test.dart b/test/core/prefs_keys_test.dart
index 8881b395..e1962106 100644
--- a/test/core/prefs_keys_test.dart
+++ b/test/core/prefs_keys_test.dart
@@ -30,3 +30,7 @@ void main() {
+
+
+
+
diff --git a/test/core/routing/main_navigation_routes_test.dart b/test/core/routing/main_navigation_routes_test.dart
index 558aa397..fd115acb 100644
--- a/test/core/routing/main_navigation_routes_test.dart
+++ b/test/core/routing/main_navigation_routes_test.dart
@@ -27,3 +27,7 @@ void main() {
+
+
+
+
diff --git a/test/core/routing/other_routes_test.dart b/test/core/routing/other_routes_test.dart
index 42ffe188..19b4747a 100644
--- a/test/core/routing/other_routes_test.dart
+++ b/test/core/routing/other_routes_test.dart
@@ -31,3 +31,7 @@ void main() {
+
+
+
+
diff --git a/test/core/routing/route_constants_test.dart b/test/core/routing/route_constants_test.dart
index b49f6836..66b3306b 100644
--- a/test/core/routing/route_constants_test.dart
+++ b/test/core/routing/route_constants_test.dart
@@ -110,3 +110,7 @@ void main() {
+
+
+
+
diff --git a/test/core/routing/settings_routes_test.dart b/test/core/routing/settings_routes_test.dart
index abf21fc9..70e0af0e 100644
--- a/test/core/routing/settings_routes_test.dart
+++ b/test/core/routing/settings_routes_test.dart
@@ -34,3 +34,7 @@ void main() {
+
+
+
+
diff --git a/test/core/views/cubit/views_state_test.dart b/test/core/views/cubit/views_state_test.dart
index 3493fed2..7214e4fd 100644
--- a/test/core/views/cubit/views_state_test.dart
+++ b/test/core/views/cubit/views_state_test.dart
@@ -28,3 +28,7 @@ void main() {
+
+
+
+
diff --git a/test/core/widgets/add_item_button_test.dart b/test/core/widgets/add_item_button_test.dart
index c25013b3..f5235cb7 100644
--- a/test/core/widgets/add_item_button_test.dart
+++ b/test/core/widgets/add_item_button_test.dart
@@ -64,3 +64,7 @@ void main() {
+
+
+
+
diff --git a/test/core/widgets/animated_option_button_test.dart b/test/core/widgets/animated_option_button_test.dart
index 0b9f73da..62809f4d 100644
--- a/test/core/widgets/animated_option_button_test.dart
+++ b/test/core/widgets/animated_option_button_test.dart
@@ -116,3 +116,7 @@ void main() {
+
+
+
+
diff --git a/test/core/widgets/base_button_test.dart b/test/core/widgets/base_button_test.dart
index 89e8b2aa..b0d3a80a 100644
--- a/test/core/widgets/base_button_test.dart
+++ b/test/core/widgets/base_button_test.dart
@@ -53,3 +53,7 @@ void main() {
+
+
+
+
diff --git a/test/core/widgets/bottom_sheet_with_blur_test.dart b/test/core/widgets/bottom_sheet_with_blur_test.dart
index d1b41d46..bcd64961 100644
--- a/test/core/widgets/bottom_sheet_with_blur_test.dart
+++ b/test/core/widgets/bottom_sheet_with_blur_test.dart
@@ -135,3 +135,7 @@ void main() {
+
+
+
+
diff --git a/test/core/widgets/cpfc_container_test.dart b/test/core/widgets/cpfc_container_test.dart
index 12fc023f..cf80ae48 100644
--- a/test/core/widgets/cpfc_container_test.dart
+++ b/test/core/widgets/cpfc_container_test.dart
@@ -96,3 +96,7 @@ void main() {
+
+
+
+
diff --git a/test/core/widgets/custom_search_bar_test.dart b/test/core/widgets/custom_search_bar_test.dart
index 5a83efea..3e7ea5a7 100644
--- a/test/core/widgets/custom_search_bar_test.dart
+++ b/test/core/widgets/custom_search_bar_test.dart
@@ -164,3 +164,7 @@ void main() {
+
+
+
+
diff --git a/test/core/widgets/item_widget_test.dart b/test/core/widgets/item_widget_test.dart
index 691c89e6..e827b8f6 100644
--- a/test/core/widgets/item_widget_test.dart
+++ b/test/core/widgets/item_widget_test.dart
@@ -57,3 +57,7 @@ void main() {
+
+
+
+
diff --git a/test/core/widgets/loader_test.dart b/test/core/widgets/loader_test.dart
index 3dc68c30..5d5bf3a6 100644
--- a/test/core/widgets/loader_test.dart
+++ b/test/core/widgets/loader_test.dart
@@ -42,3 +42,7 @@ void main() {
+
+
+
+
diff --git a/test/core/widgets/main_background_bouble_test.dart b/test/core/widgets/main_background_bouble_test.dart
index 42e66afc..60df496e 100644
--- a/test/core/widgets/main_background_bouble_test.dart
+++ b/test/core/widgets/main_background_bouble_test.dart
@@ -96,3 +96,7 @@ void main() {
+
+
+
+
diff --git a/test/core/widgets/marketing_bottom_sheet_test.dart b/test/core/widgets/marketing_bottom_sheet_test.dart
index 438dd648..3f79c376 100644
--- a/test/core/widgets/marketing_bottom_sheet_test.dart
+++ b/test/core/widgets/marketing_bottom_sheet_test.dart
@@ -76,3 +76,7 @@ void main() {
+
+
+
+
diff --git a/test/core/widgets/option_widget_test.dart b/test/core/widgets/option_widget_test.dart
index ae29f427..fdaf84db 100644
--- a/test/core/widgets/option_widget_test.dart
+++ b/test/core/widgets/option_widget_test.dart
@@ -65,3 +65,7 @@ void main() {
+
+
+
+
diff --git a/test/features/auth/presentation/pages/forget_password_screen_test.dart b/test/features/auth/presentation/pages/forget_password_screen_test.dart
new file mode 100644
index 00000000..4af21add
--- /dev/null
+++ b/test/features/auth/presentation/pages/forget_password_screen_test.dart
@@ -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);
+ });
+
+ 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);
+ });
+ });
+}
+
diff --git a/test/features/chat/presentation/pages/chats_screen_test.dart b/test/features/chat/presentation/pages/chats_screen_test.dart
new file mode 100644
index 00000000..4a2ec4a6
--- /dev/null
+++ b/test/features/chat/presentation/pages/chats_screen_test.dart
@@ -0,0 +1,112 @@
+import 'package:eatvio_app/core/enums/base_state.dart';
+import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
+import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
+import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
+import 'package:eatvio_app/generated/l10n.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+
+import 'chats_screen_test.mocks.dart';
+
+@GenerateNiceMocks([MockSpec()])
+void main() {
+ group('ChatsScreen', () {
+ late MockChatsCubit mockChatsCubit;
+
+ setUp(() {
+ mockChatsCubit = MockChatsCubit();
+ when(mockChatsCubit.state).thenReturn(
+ ChatsState(
+ chatRooms: [],
+ state: BaseState.loading,
+ isLoadingMoreChats: false,
+ ),
+ );
+ when(mockChatsCubit.stream).thenAnswer((_) => Stream.value(
+ ChatsState(
+ chatRooms: [],
+ state: BaseState.loading,
+ isLoadingMoreChats: false,
+ ),
+ ));
+ });
+
+ Widget createTestWidget() {
+ return MaterialApp(
+ theme: ThemeHelper().themeData(),
+ localizationsDelegates: const [
+ S.delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: const [Locale('ru')],
+ home: BlocProvider(
+ create: (context) => mockChatsCubit,
+ child: const ChatsScreen(),
+ ),
+ );
+ }
+
+ testWidgets('displays screen', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pump();
+
+ expect(find.byType(ChatsScreen), findsOneWidget);
+ });
+
+ testWidgets('displays search field', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pump();
+
+ expect(find.text('Поиск'), findsOneWidget);
+ });
+
+ testWidgets('displays loading state', (WidgetTester tester) async {
+ when(mockChatsCubit.state).thenReturn(
+ ChatsState(
+ chatRooms: [],
+ state: BaseState.loading,
+ isLoadingMoreChats: false,
+ ),
+ );
+
+ await tester.pumpWidget(createTestWidget());
+ await tester.pump();
+
+ // Screen should be displayed
+ expect(find.byType(ChatsScreen), findsOneWidget);
+ });
+
+ testWidgets('displays error state', (WidgetTester tester) async {
+ when(mockChatsCubit.state).thenReturn(
+ ChatsState(
+ chatRooms: [],
+ state: BaseState.error,
+ isLoadingMoreChats: false,
+ ),
+ );
+
+ await tester.pumpWidget(createTestWidget());
+ await tester.pump();
+
+ // Screen should be displayed
+ expect(find.byType(ChatsScreen), findsOneWidget);
+ });
+
+ testWidgets('displays screen correctly', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pump();
+
+ // Screen should be displayed
+ expect(find.byType(ChatsScreen), findsOneWidget);
+ // Note: getChatRooms is called in initState, which is hard to test without proper setup
+ });
+ });
+}
+
diff --git a/test/features/chat/presentation/pages/photo_view_screen_test.dart b/test/features/chat/presentation/pages/photo_view_screen_test.dart
new file mode 100644
index 00000000..5b4f1347
--- /dev/null
+++ b/test/features/chat/presentation/pages/photo_view_screen_test.dart
@@ -0,0 +1,89 @@
+import 'package:eatvio_app/core/app_config.dart';
+import 'package:eatvio_app/core/app_initializer.dart';
+import 'package:eatvio_app/core/locator_service.dart';
+import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_dotenv/flutter_dotenv.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ group('PhotoViewScreen', () {
+ setUp(() async {
+ await sl.reset();
+ dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
+ sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
+ });
+
+ Widget createTestWidget({
+ required List imageUrls,
+ required int index,
+ bool isLocal = false,
+ }) {
+ return MaterialApp(
+ theme: ThemeHelper().themeData(),
+ home: PhotoViewScreen(
+ imageUrls: imageUrls,
+ index: index,
+ isLocal: isLocal,
+ ),
+ );
+ }
+
+ testWidgets('creates PhotoViewScreen with image URLs', (WidgetTester tester) async {
+ const imageUrls = [
+ 'https://example.com/image1.jpg',
+ 'https://example.com/image2.jpg',
+ ];
+
+ await tester.pumpWidget(createTestWidget(imageUrls: imageUrls, index: 0));
+ await tester.pump();
+
+ // Screen should be created
+ expect(find.byType(PhotoViewScreen), findsOneWidget);
+ });
+
+ testWidgets('initializes with correct index', (WidgetTester tester) async {
+ const imageUrls = [
+ 'https://example.com/image1.jpg',
+ 'https://example.com/image2.jpg',
+ 'https://example.com/image3.jpg',
+ ];
+
+ await tester.pumpWidget(createTestWidget(imageUrls: imageUrls, index: 1));
+ await tester.pump();
+
+ // Screen should be created with index 1
+ expect(find.byType(PhotoViewScreen), findsOneWidget);
+ });
+
+ testWidgets('handles single image', (WidgetTester tester) async {
+ const imageUrls = ['https://example.com/image1.jpg'];
+
+ await tester.pumpWidget(createTestWidget(imageUrls: imageUrls, index: 0));
+ await tester.pump();
+
+ expect(find.byType(PhotoViewScreen), findsOneWidget);
+ });
+
+ testWidgets('handles multiple images', (WidgetTester tester) async {
+ const imageUrls = [
+ 'https://example.com/image1.jpg',
+ 'https://example.com/image2.jpg',
+ 'https://example.com/image3.jpg',
+ ];
+
+ await tester.pumpWidget(createTestWidget(imageUrls: imageUrls, index: 0));
+ await tester.pump();
+
+ expect(find.byType(PhotoViewScreen), findsOneWidget);
+ });
+
+ testWidgets('creates screen with isLocal flag', (WidgetTester tester) async {
+ // For local files, we need File objects, but in tests we'll skip this test
+ // as it requires actual file system access
+ // This test is skipped to avoid File type errors
+ });
+ });
+}
+
diff --git a/test/features/create_diet/basic_widget_test.dart b/test/features/create_diet/basic_widget_test.dart
index 27dd8faf..6812d414 100644
--- a/test/features/create_diet/basic_widget_test.dart
+++ b/test/features/create_diet/basic_widget_test.dart
@@ -9,6 +9,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../fixtures/doubles/create_diet_doubles.dart';
+import '../../fixtures/doubles/profile_doubles.dart';
void main() {
group('Diet Creation Basic Widget Tests', () {
diff --git a/test/features/create_diet/presentation/widgets/error_widget_test.dart b/test/features/create_diet/presentation/widgets/error_widget_test.dart
index 752f4f44..36d0a184 100644
--- a/test/features/create_diet/presentation/widgets/error_widget_test.dart
+++ b/test/features/create_diet/presentation/widgets/error_widget_test.dart
@@ -71,3 +71,7 @@ void main() {
+
+
+
+
diff --git a/test/features/diary/presentation/widgets/chosen_diet_widget_content_test.dart b/test/features/diary/presentation/widgets/chosen_diet_widget_content_test.dart
index 0db0bdd4..15ef7bd4 100644
--- a/test/features/diary/presentation/widgets/chosen_diet_widget_content_test.dart
+++ b/test/features/diary/presentation/widgets/chosen_diet_widget_content_test.dart
@@ -80,3 +80,7 @@ void main() {
+
+
+
+
diff --git a/test/features/diary/presentation/widgets/comment_container_test.dart b/test/features/diary/presentation/widgets/comment_container_test.dart
index 0d80e7ff..dd6296da 100644
--- a/test/features/diary/presentation/widgets/comment_container_test.dart
+++ b/test/features/diary/presentation/widgets/comment_container_test.dart
@@ -58,3 +58,7 @@ void main() {
+
+
+
+
diff --git a/test/features/diary/presentation/widgets/finish_day_button_widget_test.dart b/test/features/diary/presentation/widgets/finish_day_button_widget_test.dart
index 74164fa7..52a1d8ba 100644
--- a/test/features/diary/presentation/widgets/finish_day_button_widget_test.dart
+++ b/test/features/diary/presentation/widgets/finish_day_button_widget_test.dart
@@ -53,3 +53,7 @@ void main() {
+
+
+
+
diff --git a/test/features/diary/presentation/widgets/meal_summary_widget_test.dart b/test/features/diary/presentation/widgets/meal_summary_widget_test.dart
index 83007e51..2625403e 100644
--- a/test/features/diary/presentation/widgets/meal_summary_widget_test.dart
+++ b/test/features/diary/presentation/widgets/meal_summary_widget_test.dart
@@ -90,3 +90,7 @@ void main() {
+
+
+
+
diff --git a/test/features/diary/presentation/widgets/product_loader_widget_test.dart b/test/features/diary/presentation/widgets/product_loader_widget_test.dart
index 6729bf76..0dc02252 100644
--- a/test/features/diary/presentation/widgets/product_loader_widget_test.dart
+++ b/test/features/diary/presentation/widgets/product_loader_widget_test.dart
@@ -42,3 +42,7 @@ void main() {
+
+
+
+
diff --git a/test/features/diets/presentation/widgets/info_bouble_test.dart b/test/features/diets/presentation/widgets/info_bouble_test.dart
index 40a7abe9..1c5889ef 100644
--- a/test/features/diets/presentation/widgets/info_bouble_test.dart
+++ b/test/features/diets/presentation/widgets/info_bouble_test.dart
@@ -108,3 +108,7 @@ void main() {
+
+
+
+
diff --git a/test/features/feed/presentation/widgets/choice_chip_widget_test.dart b/test/features/feed/presentation/widgets/choice_chip_widget_test.dart
index 0abecbaf..0c54328f 100644
--- a/test/features/feed/presentation/widgets/choice_chip_widget_test.dart
+++ b/test/features/feed/presentation/widgets/choice_chip_widget_test.dart
@@ -138,3 +138,7 @@ void main() {
+
+
+
+
diff --git a/test/features/feed/presentation/widgets/filter_badge_test.dart b/test/features/feed/presentation/widgets/filter_badge_test.dart
index 72cb3c7d..e0023ab4 100644
--- a/test/features/feed/presentation/widgets/filter_badge_test.dart
+++ b/test/features/feed/presentation/widgets/filter_badge_test.dart
@@ -50,3 +50,7 @@ void main() {
+
+
+
+
diff --git a/test/features/finances_and_consultations_archive/presentation/widgets/status_widget_test.dart b/test/features/finances_and_consultations_archive/presentation/widgets/status_widget_test.dart
index c6c90373..ca12ebef 100644
--- a/test/features/finances_and_consultations_archive/presentation/widgets/status_widget_test.dart
+++ b/test/features/finances_and_consultations_archive/presentation/widgets/status_widget_test.dart
@@ -58,3 +58,7 @@ void main() {
+
+
+
+
diff --git a/test/features/glossary/repositories/glossary_repository_test.dart b/test/features/glossary/repositories/glossary_repository_test.dart
index d9a77f84..5cb09ffb 100644
--- a/test/features/glossary/repositories/glossary_repository_test.dart
+++ b/test/features/glossary/repositories/glossary_repository_test.dart
@@ -51,3 +51,7 @@ void main() {
+
+
+
+
diff --git a/test/features/notifications/presentation/widgets/notification_widget_test.dart b/test/features/notifications/presentation/widgets/notification_widget_test.dart
index cef6a0d2..11b00faa 100644
--- a/test/features/notifications/presentation/widgets/notification_widget_test.dart
+++ b/test/features/notifications/presentation/widgets/notification_widget_test.dart
@@ -1028,3 +1028,7 @@ void main() {
+
+
+
+
diff --git a/test/features/notifications/service/notifications_service_test.dart b/test/features/notifications/service/notifications_service_test.dart
index 388a16c1..a05cf8f0 100644
--- a/test/features/notifications/service/notifications_service_test.dart
+++ b/test/features/notifications/service/notifications_service_test.dart
@@ -60,3 +60,7 @@ void main() {
});
});
}
+
+
+
+
diff --git a/test/features/order_info/presentation/info_by_order_screen_test.dart b/test/features/order_info/presentation/info_by_order_screen_test.dart
new file mode 100644
index 00000000..589d0b2d
--- /dev/null
+++ b/test/features/order_info/presentation/info_by_order_screen_test.dart
@@ -0,0 +1,100 @@
+import 'package:eatvio_app/core/locator_service.dart';
+import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
+import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
+import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
+import 'package:eatvio_app/features/profile/repositories/profile_repository.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';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+
+import 'info_by_order_screen_test.mocks.dart';
+
+@GenerateMocks([ChatRepository, ProfileRepository])
+void main() {
+ group('InfoByOrderScreen', () {
+ late MockChatRepository mockChatRepository;
+ late MockProfileRepository mockProfileRepository;
+ late OrderInfoCubit orderInfoCubit;
+
+ setUp(() async {
+ await sl.reset();
+ mockChatRepository = MockChatRepository();
+ mockProfileRepository = MockProfileRepository();
+ orderInfoCubit = OrderInfoCubit(
+ chatRepository: mockChatRepository,
+ profileRepository: mockProfileRepository,
+ );
+ sl.registerLazySingleton(() => orderInfoCubit);
+ });
+
+ tearDown(() {
+ orderInfoCubit.close();
+ });
+
+ Widget createTestWidget({required int planId, bool? successfulPayment, String? customerSlug}) {
+ return MaterialApp(
+ theme: ThemeHelper().themeData(),
+ localizationsDelegates: const [
+ S.delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: const [Locale('ru')],
+ home: InfoByOrderScreen(
+ planId: planId,
+ successfulPayment: successfulPayment,
+ customerSlug: customerSlug,
+ ),
+ );
+ }
+
+ testWidgets('displays screen with loading state initially', (WidgetTester tester) async {
+ when(mockChatRepository.getChosenPlan(id: anyNamed('id')))
+ .thenThrow(Exception('Not implemented'));
+
+ await tester.pumpWidget(createTestWidget(planId: 1));
+ await tester.pump();
+
+ // Screen should be displayed (Loader widget is used, not CircularProgressIndicator directly)
+ expect(find.byType(Scaffold), findsOneWidget);
+ });
+
+ testWidgets('calls getChosenPlan when screen is created', (WidgetTester tester) async {
+ when(mockChatRepository.getChosenPlan(id: anyNamed('id')))
+ .thenThrow(Exception('Not implemented'));
+
+ await tester.pumpWidget(createTestWidget(planId: 123));
+ await tester.pump();
+
+ verify(mockChatRepository.getChosenPlan(id: 123)).called(1);
+ });
+
+ testWidgets('calls getChosenPlan when customerSlug is provided', (WidgetTester tester) async {
+ when(mockChatRepository.getChosenPlan(id: anyNamed('id')))
+ .thenThrow(Exception('Not implemented'));
+
+ await tester.pumpWidget(createTestWidget(planId: 123, customerSlug: 'test-user'));
+ await tester.pump();
+
+ verify(mockChatRepository.getChosenPlan(id: 123)).called(1);
+ // Note: getProfileBySlug is only called if getChosenPlan succeeds
+ });
+
+ testWidgets('displays screen with back button', (WidgetTester tester) async {
+ when(mockChatRepository.getChosenPlan(id: anyNamed('id')))
+ .thenThrow(Exception('Not implemented'));
+
+ await tester.pumpWidget(createTestWidget(planId: 1));
+ await tester.pump();
+
+ // Check for back button
+ expect(find.byIcon(Icons.arrow_back), findsOneWidget);
+ });
+ });
+}
+
diff --git a/test/features/profile/presentation/pages/subs_screen_test.dart b/test/features/profile/presentation/pages/subs_screen_test.dart
new file mode 100644
index 00000000..f1cd34a5
--- /dev/null
+++ b/test/features/profile/presentation/pages/subs_screen_test.dart
@@ -0,0 +1,118 @@
+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/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
+import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
+import 'package:eatvio_app/features/profile/repositories/profile_repository.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 'subs_screen_test.mocks.dart';
+
+@GenerateMocks([ProfileRepository])
+void main() {
+ group('SubsScreen', () {
+ late MockProfileRepository mockProfileRepository;
+ late SubCubit subCubit;
+
+ setUp(() async {
+ await sl.reset();
+ dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
+ sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
+ mockProfileRepository = MockProfileRepository();
+ subCubit = SubCubit(profileRepository: mockProfileRepository);
+ sl.registerLazySingleton(() => subCubit);
+ });
+
+ tearDown(() {
+ subCubit.close();
+ });
+
+ Widget createTestWidget({required SubType subType, required String slug}) {
+ return MaterialApp(
+ theme: ThemeHelper().themeData(),
+ home: SubsScreen(subType: subType, slug: slug),
+ );
+ }
+
+ testWidgets('displays screen with correct title for subscriptions', (WidgetTester tester) async {
+ when(mockProfileRepository.getSubscriptionsBySlug(any, any))
+ .thenAnswer((_) async => []);
+
+ await tester.pumpWidget(createTestWidget(subType: SubType.subscriptions, slug: 'test-user'));
+ await tester.pump(); // Use pump instead of pumpAndSettle
+
+ expect(find.text('Подписки'), findsOneWidget);
+ });
+
+ testWidgets('displays screen with correct title for subscribers', (WidgetTester tester) async {
+ when(mockProfileRepository.getSubscribersBySlug(any, any))
+ .thenAnswer((_) async => []);
+
+ await tester.pumpWidget(createTestWidget(subType: SubType.subscribers, slug: 'test-user'));
+ await tester.pump(); // Use pump instead of pumpAndSettle
+
+ expect(find.text('Подписчики'), findsOneWidget);
+ });
+
+ testWidgets('displays search field', (WidgetTester tester) async {
+ when(mockProfileRepository.getSubscriptionsBySlug(any, any))
+ .thenAnswer((_) async => []);
+
+ await tester.pumpWidget(createTestWidget(subType: SubType.subscriptions, slug: 'test-user'));
+ await tester.pump();
+
+ expect(find.text('Поиск'), findsOneWidget);
+ });
+
+ testWidgets('displays loading state', (WidgetTester tester) async {
+ when(mockProfileRepository.getSubscriptionsBySlug(any, any))
+ .thenAnswer((_) async => []);
+
+ await tester.pumpWidget(createTestWidget(subType: SubType.subscriptions, slug: 'test-user'));
+ await tester.pump();
+
+ // Check for loader widget (Loader widget is used, not CircularProgressIndicator directly)
+ // The screen should be displayed even in loading state
+ expect(find.text('Подписки'), findsOneWidget);
+ });
+
+ testWidgets('displays error state', (WidgetTester tester) async {
+ when(mockProfileRepository.getSubscriptionsBySlug(any, any))
+ .thenThrow(Exception('Error loading subscriptions'));
+
+ await tester.pumpWidget(createTestWidget(subType: SubType.subscriptions, slug: 'test-user'));
+ await tester.pump();
+ await tester.pump(); // Wait for error state
+
+ // Error message should be displayed
+ expect(find.textContaining('Error'), findsWidgets);
+ });
+
+
+ testWidgets('calls getSubscriptions when subType is subscriptions', (WidgetTester tester) async {
+ when(mockProfileRepository.getSubscriptionsBySlug(any, any))
+ .thenAnswer((_) async => []);
+
+ await tester.pumpWidget(createTestWidget(subType: SubType.subscriptions, slug: 'test-user'));
+ await tester.pump();
+
+ verify(mockProfileRepository.getSubscriptionsBySlug('test-user', null)).called(1);
+ });
+
+ testWidgets('calls getSubscribers when subType is subscribers', (WidgetTester tester) async {
+ when(mockProfileRepository.getSubscribersBySlug(any, any))
+ .thenAnswer((_) async => []);
+
+ await tester.pumpWidget(createTestWidget(subType: SubType.subscribers, slug: 'test-user'));
+ await tester.pump();
+
+ verify(mockProfileRepository.getSubscribersBySlug('test-user', null)).called(1);
+ });
+ });
+}
+
diff --git a/test/features/registration/presentation/pages/body_circumferences_screen_test.dart b/test/features/registration/presentation/pages/body_circumferences_screen_test.dart
new file mode 100644
index 00000000..d6a5de73
--- /dev/null
+++ b/test/features/registration/presentation/pages/body_circumferences_screen_test.dart
@@ -0,0 +1,393 @@
+import 'package:eatvio_app/core/prefs.dart';
+import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
+import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
+import 'package:eatvio_app/features/registration/presentation/pages/body_circumferences_screen.dart';
+import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
+import 'package:eatvio_app/generated/l10n.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+
+import 'body_circumferences_screen_test.mocks.dart';
+
+@GenerateMocks([RegistrationRepository, Prefs])
+void main() {
+ group('BodyCircumferencesScreen', () {
+ late MockRegistrationRepository mockRegistrationRepository;
+ late RegistrationCubit registrationCubit;
+
+ setUp(() {
+ mockRegistrationRepository = MockRegistrationRepository();
+ registrationCubit = RegistrationCubit(
+ registrationRepository: mockRegistrationRepository,
+ prefs: MockPrefs(),
+ );
+ });
+
+ tearDown(() {
+ registrationCubit.close();
+ });
+
+ Widget createTestWidget(UserRegistrationSheet sheet) {
+ return MaterialApp(
+ theme: ThemeHelper().themeData(),
+ localizationsDelegates: const [
+ S.delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: const [Locale('ru')],
+ home: Scaffold(
+ body: BlocProvider(
+ create: (context) => registrationCubit,
+ child: BodyCircumferencesScreen(sheet: sheet),
+ ),
+ ),
+ );
+ }
+
+ testWidgets('displays screen with initial circumferences from sheet', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: 'Moscow',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 100,
+ waistCircumference: 80,
+ hipCircumference: 90,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pumpAndSettle();
+
+ // Check that text fields contain the values
+ final textFields = find.byType(TextFormField);
+ expect(textFields, findsNWidgets(3));
+
+ // Verify values in text fields
+ final firstField = tester.widget(textFields.first);
+ expect(firstField.controller?.text, '100');
+
+ final secondField = tester.widget(textFields.at(1));
+ expect(secondField.controller?.text, '80');
+
+ final thirdField = tester.widget(textFields.at(2));
+ expect(thirdField.controller?.text, '90');
+ });
+
+ testWidgets('displays empty fields when circumferences are 0', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: 'Moscow',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 0,
+ waistCircumference: 0,
+ hipCircumference: 0,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pumpAndSettle();
+
+ final textFields = find.byType(TextFormField);
+ expect(textFields, findsNWidgets(3));
+ });
+
+ testWidgets('displays hint texts', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: 'Moscow',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 0,
+ waistCircumference: 0,
+ hipCircumference: 0,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pumpAndSettle();
+
+ // Check hint texts are displayed
+ final textFields = find.byType(TextFormField);
+ expect(textFields, findsNWidgets(3));
+
+ // Hint texts should be visible when fields are empty
+ expect(find.text('90'), findsWidgets); // Appears as hint for chest and hip
+ expect(find.text('60'), findsOneWidget); // Appears as hint for waist
+ });
+
+ testWidgets('updates registration cubit when chest circumference changes', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: 'Moscow',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 100,
+ waistCircumference: 80,
+ hipCircumference: 90,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pumpAndSettle();
+
+ final textFields = find.byType(TextFormField);
+ expect(textFields, findsNWidgets(3));
+
+ await tester.enterText(textFields.first, '105');
+ await tester.pump();
+
+ // Verify that the cubit state was updated
+ expect(registrationCubit.state.sheet.chestCircumference, 105);
+ });
+
+ testWidgets('updates registration cubit when waist circumference changes', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: 'Moscow',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 100,
+ waistCircumference: 80,
+ hipCircumference: 90,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pumpAndSettle();
+
+ final textFields = find.byType(TextFormField);
+ expect(textFields, findsNWidgets(3));
+
+ await tester.enterText(textFields.at(1), '85');
+ await tester.pump();
+
+ // Verify that the cubit state was updated
+ expect(registrationCubit.state.sheet.waistCircumference, 85);
+ });
+
+ testWidgets('updates registration cubit when hip circumference changes', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: 'Moscow',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 100,
+ waistCircumference: 80,
+ hipCircumference: 90,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pumpAndSettle();
+
+ final textFields = find.byType(TextFormField);
+ expect(textFields, findsNWidgets(3));
+
+ await tester.enterText(textFields.at(2), '95');
+ await tester.pump();
+
+ // Verify that the cubit state was updated
+ expect(registrationCubit.state.sheet.hipCircumference, 95);
+ });
+
+ testWidgets('only accepts digits input', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: 'Moscow',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 0,
+ waistCircumference: 0,
+ hipCircumference: 0,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pumpAndSettle();
+
+ final textFields = find.byType(TextFormField);
+ expect(textFields, findsNWidgets(3));
+
+ // Try to enter non-digit characters
+ await tester.enterText(textFields.first, 'abc');
+ await tester.pump();
+
+ // The text field should only accept digits (filtered by inputFormatters)
+ final textFieldWidget = tester.widget(textFields.first);
+ // Since digitsOnly formatter filters non-digits, the text should be empty
+ expect(textFieldWidget.controller?.text, isEmpty);
+ });
+
+ testWidgets('displays labels for all circumference fields', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: 'Moscow',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 0,
+ waistCircumference: 0,
+ hipCircumference: 0,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pumpAndSettle();
+
+ // Check that labels are displayed (they contain "(не обязательно)")
+ expect(find.textContaining('не обязательно'), findsNWidgets(3));
+ });
+ });
+}
+
diff --git a/test/features/registration/presentation/pages/city_of_living_screen_test.dart b/test/features/registration/presentation/pages/city_of_living_screen_test.dart
new file mode 100644
index 00000000..cedf6025
--- /dev/null
+++ b/test/features/registration/presentation/pages/city_of_living_screen_test.dart
@@ -0,0 +1,275 @@
+import 'package:eatvio_app/core/prefs.dart';
+import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/features/registration/models/location_model.dart';
+import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
+import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
+import 'package:eatvio_app/features/registration/presentation/pages/city_of_living_screen.dart';
+import 'package:eatvio_app/features/registration/repositories/registration_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:mockito/mockito.dart';
+
+import 'city_of_living_screen_test.mocks.dart';
+
+@GenerateMocks([RegistrationRepository, Prefs])
+void main() {
+ group('CityOfLivingScreen', () {
+ late MockRegistrationRepository mockRegistrationRepository;
+ late RegistrationCubit registrationCubit;
+
+ setUp(() {
+ mockRegistrationRepository = MockRegistrationRepository();
+ registrationCubit = RegistrationCubit(
+ registrationRepository: mockRegistrationRepository,
+ prefs: MockPrefs(),
+ );
+ // Mock getLocationsListByQuery to return empty list by default
+ when(mockRegistrationRepository.getLocationsListByQuery(any))
+ .thenAnswer((_) async => const LocationModel(locations: [], responseBody: '{}'));
+ });
+
+ tearDown(() {
+ registrationCubit.close();
+ });
+
+ Widget createTestWidget(UserRegistrationSheet sheet) {
+ return MaterialApp(
+ theme: ThemeHelper().themeData(),
+ home: Scaffold(
+ body: BlocProvider(
+ create: (context) => registrationCubit,
+ child: CityOfLivingScreen(sheet: sheet),
+ ),
+ ),
+ );
+ }
+
+ testWidgets('displays screen with initial location from sheet', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: 'Moscow',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 100,
+ waistCircumference: 80,
+ hipCircumference: 90,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pump(); // Use pump instead of pumpAndSettle to avoid timeout
+
+ expect(find.text('Moscow'), findsOneWidget);
+ });
+
+ testWidgets('displays empty field when location is empty', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: '',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 100,
+ waistCircumference: 80,
+ hipCircumference: 90,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pump(); // Use pump instead of pumpAndSettle to avoid timeout
+
+ final textField = find.byType(TextFormField);
+ expect(textField, findsOneWidget);
+ });
+
+ testWidgets('displays hint text', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: '',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 100,
+ waistCircumference: 80,
+ hipCircumference: 90,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pump(); // Use pump instead of pumpAndSettle to avoid timeout
+
+ expect(find.text('Москва'), findsOneWidget);
+ });
+
+ testWidgets('displays location label', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: '',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 100,
+ waistCircumference: 80,
+ hipCircumference: 90,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pump(); // Use pump instead of pumpAndSettle to avoid timeout
+
+ expect(find.text('Местоположение'), findsOneWidget);
+ });
+
+ testWidgets('updates registration cubit when location changes', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: 'Moscow',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 100,
+ waistCircumference: 80,
+ hipCircumference: 90,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pump(); // Use pump instead of pumpAndSettle to avoid timeout
+
+ final textField = find.byType(TextFormField);
+ expect(textField, findsOneWidget);
+
+ await tester.enterText(textField, 'Saint Petersburg');
+ await tester.pump();
+
+ // Verify that the cubit state was updated
+ expect(registrationCubit.state.sheet.location, 'Saint Petersburg');
+ });
+
+ testWidgets('displays TypeAheadField widget', (WidgetTester tester) async {
+ const sheet = UserRegistrationSheet(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ password: 'password123',
+ passwordConfirmation: 'password123',
+ birthdate: '1990-01-01',
+ gender: 'male',
+ location: '',
+ responseBody: '{}',
+ lang: 'ru',
+ weight: 75,
+ height: 180,
+ phone: '+1234567890',
+ chestCircumference: 100,
+ waistCircumference: 80,
+ hipCircumference: 90,
+ bloodPressureUp: 120,
+ bloodPressureDown: 80,
+ hasHypo: 0,
+ hasHyper: 0,
+ aboutAllergy: 'None',
+ aboutTakingPils: 'None',
+ aboutSportHobbies: 'Running',
+ favoriteFoods: 'Pizza',
+ unfavoriteFoods: 'Broccoli',
+ additionalInfo: 'No additional info',
+ );
+
+ await tester.pumpWidget(createTestWidget(sheet));
+ await tester.pump(); // Use pump instead of pumpAndSettle to avoid timeout
+
+ final textField = find.byType(TextFormField);
+ expect(textField, findsOneWidget);
+ });
+ });
+}
+
diff --git a/test/features/settings/presentation/pages/other_info_screen_test.dart b/test/features/settings/presentation/pages/other_info_screen_test.dart
new file mode 100644
index 00000000..8310f644
--- /dev/null
+++ b/test/features/settings/presentation/pages/other_info_screen_test.dart
@@ -0,0 +1,100 @@
+import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
+import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
+import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart';
+import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.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';
+import 'package:go_router/go_router.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../../../fixtures/settings_fixtures.dart';
+import 'other_info_screen_test.mocks.dart';
+
+@GenerateMocks([SettingsCubit])
+void main() {
+ group('OtherInfoScreen', () {
+ late MockSettingsCubit mockSettingsCubit;
+ late SettingsEntity settingsEntity;
+
+ setUp(() {
+ mockSettingsCubit = MockSettingsCubit();
+ final profileDetails = SettingsFixtures.profileDetailsModel();
+ final profileSettings = SettingsFixtures.profileSettingsModel();
+
+ when(mockSettingsCubit.state).thenReturn(
+ SettingsStateLoaded(
+ profile: SettingsFixtures.profileModel(),
+ profileSettings: profileSettings,
+ profileDetails: profileDetails,
+ hasActiveConsultation: false,
+ ),
+ );
+
+ settingsEntity = SettingsEntity(
+ profileSettingsModel: profileSettings,
+ profileDetailsModel: profileDetails,
+ settingsCubit: mockSettingsCubit,
+ );
+ });
+
+ Widget createTestWidget() {
+ final router = GoRouter(
+ initialLocation: '/',
+ routes: [
+ GoRoute(
+ path: '/',
+ builder: (context, state) => OtherInfoScreen(settingsEntity: settingsEntity),
+ ),
+ ],
+ );
+
+ return MaterialApp.router(
+ theme: ThemeHelper().themeData(),
+ routerConfig: router,
+ localizationsDelegates: const [
+ S.delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: const [Locale('ru')],
+ );
+ }
+
+ testWidgets('displays screen with correct title', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ expect(find.text('Прочая информаиця'), findsOneWidget);
+ });
+
+ testWidgets('displays all input fields', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // Check for input fields
+ expect(find.byType(TextFormField), findsWidgets);
+ });
+
+ testWidgets('displays save button', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ expect(find.text('Сохранить'), findsOneWidget);
+ });
+
+ testWidgets('initializes with existing values', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // The controllers should be initialized with existing values
+ final textFields = find.byType(TextFormField);
+ expect(textFields, findsWidgets);
+ });
+ });
+}
+
diff --git a/test/features/settings/presentation/pages/physical_form_screen_test.dart b/test/features/settings/presentation/pages/physical_form_screen_test.dart
new file mode 100644
index 00000000..0500a341
--- /dev/null
+++ b/test/features/settings/presentation/pages/physical_form_screen_test.dart
@@ -0,0 +1,111 @@
+import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
+import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
+import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart';
+import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.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';
+import 'package:go_router/go_router.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../../../fixtures/settings_fixtures.dart';
+import 'physical_form_screen_test.mocks.dart';
+
+@GenerateMocks([SettingsCubit])
+void main() {
+ group('PhysicalFormScreen', () {
+ late MockSettingsCubit mockSettingsCubit;
+ late SettingsEntity settingsEntity;
+
+ setUp(() {
+ mockSettingsCubit = MockSettingsCubit();
+ final profileDetails = SettingsFixtures.profileDetailsModel();
+ final profileSettings = SettingsFixtures.profileSettingsModel();
+
+ when(mockSettingsCubit.state).thenReturn(
+ SettingsStateLoaded(
+ profile: SettingsFixtures.profileModel(),
+ profileSettings: profileSettings,
+ profileDetails: profileDetails,
+ hasActiveConsultation: false,
+ ),
+ );
+
+ settingsEntity = SettingsEntity(
+ profileSettingsModel: profileSettings,
+ profileDetailsModel: profileDetails,
+ settingsCubit: mockSettingsCubit,
+ );
+ });
+
+ Widget createTestWidget() {
+ final router = GoRouter(
+ initialLocation: '/',
+ routes: [
+ GoRoute(
+ path: '/',
+ builder: (context, state) => PhysicalFormScreen(settingsEntity: settingsEntity),
+ ),
+ ],
+ );
+
+ return MaterialApp.router(
+ theme: ThemeHelper().themeData(),
+ routerConfig: router,
+ localizationsDelegates: const [
+ S.delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: const [Locale('ru')],
+ );
+ }
+
+ testWidgets('displays screen with correct title', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ expect(find.text('Физическая форма'), findsOneWidget);
+ });
+
+ testWidgets('displays all input fields', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // Check for input fields by hint text
+ expect(find.text('см'), findsWidgets);
+ expect(find.text('кг'), findsOneWidget);
+ });
+
+ testWidgets('displays save button', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ expect(find.text('Сохранить'), findsOneWidget);
+ });
+
+ testWidgets('initializes with existing values', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // The controllers should be initialized with existing values
+ // We can verify this by checking that the fields are not empty
+ final chestField = find.byType(TextFormField).first;
+ expect(chestField, findsOneWidget);
+ });
+
+ testWidgets('displays save button', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // Verify save button is displayed
+ expect(find.text('Сохранить'), findsOneWidget);
+ // Note: Testing button tap requires proper navigation setup
+ });
+ });
+}
+
diff --git a/test/features/settings/presentation/pages/pressure_screen_test.dart b/test/features/settings/presentation/pages/pressure_screen_test.dart
new file mode 100644
index 00000000..bd0ca3cb
--- /dev/null
+++ b/test/features/settings/presentation/pages/pressure_screen_test.dart
@@ -0,0 +1,108 @@
+import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
+import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
+import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart';
+import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.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';
+import 'package:go_router/go_router.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../../../fixtures/settings_fixtures.dart';
+import 'pressure_screen_test.mocks.dart';
+
+@GenerateMocks([SettingsCubit])
+void main() {
+ group('PressureScreen', () {
+ late MockSettingsCubit mockSettingsCubit;
+ late SettingsEntity settingsEntity;
+
+ setUp(() {
+ mockSettingsCubit = MockSettingsCubit();
+ final profileDetails = SettingsFixtures.profileDetailsModel();
+ final profileSettings = SettingsFixtures.profileSettingsModel();
+
+ when(mockSettingsCubit.state).thenReturn(
+ SettingsStateLoaded(
+ profile: SettingsFixtures.profileModel(),
+ profileSettings: profileSettings,
+ profileDetails: profileDetails,
+ hasActiveConsultation: false,
+ ),
+ );
+
+ settingsEntity = SettingsEntity(
+ profileSettingsModel: profileSettings,
+ profileDetailsModel: profileDetails,
+ settingsCubit: mockSettingsCubit,
+ );
+ });
+
+ Widget createTestWidget() {
+ final router = GoRouter(
+ initialLocation: '/',
+ routes: [
+ GoRoute(
+ path: '/',
+ builder: (context, state) => PressureScreen(settingsEntity: settingsEntity),
+ ),
+ ],
+ );
+
+ return MaterialApp.router(
+ theme: ThemeHelper().themeData(),
+ routerConfig: router,
+ localizationsDelegates: const [
+ S.delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: const [Locale('ru')],
+ );
+ }
+
+ testWidgets('displays screen with correct title', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ expect(find.text('Давление'), findsOneWidget);
+ });
+
+ testWidgets('displays input fields for pressure', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // Check for input fields by hint text
+ expect(find.text('90'), findsWidgets);
+ });
+
+ testWidgets('displays form fields', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // Check that screen displays correctly with input fields
+ expect(find.byType(TextFormField), findsWidgets);
+ });
+
+ testWidgets('displays save button', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ expect(find.text('Сохранить'), findsOneWidget);
+ });
+
+ testWidgets('displays save button', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // Verify save button is displayed
+ expect(find.text('Сохранить'), findsOneWidget);
+ // Note: Testing button tap requires proper navigation setup
+ });
+ });
+}
+
diff --git a/test/features/support/presentation/pages/support_screen_test.dart b/test/features/support/presentation/pages/support_screen_test.dart
new file mode 100644
index 00000000..ba6a44d3
--- /dev/null
+++ b/test/features/support/presentation/pages/support_screen_test.dart
@@ -0,0 +1,71 @@
+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/support/presentation/pages/support_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';
+
+void main() {
+ group('SupportScreen', () {
+ setUp(() async {
+ await sl.reset();
+ dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
+ sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
+ });
+
+ Widget createTestWidget() {
+ return MaterialApp(
+ theme: ThemeHelper().themeData(),
+ localizationsDelegates: const [
+ S.delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: const [Locale('ru')],
+ home: const SupportScreen(),
+ );
+ }
+
+ testWidgets('displays screen with correct title', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ expect(find.text('Поддержка'), findsOneWidget);
+ });
+
+ testWidgets('displays support information', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // Check for support text
+ expect(find.textContaining('Если у вас есть вопросы'), findsOneWidget);
+ });
+
+ testWidgets('displays legal documents section', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ expect(find.text('Правовые документы'), findsOneWidget);
+ });
+
+ testWidgets('displays privacy policy link', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ expect(find.text('Политика конфиденциальности'), findsOneWidget);
+ });
+
+ testWidgets('displays company details link', (WidgetTester tester) async {
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ expect(find.text('О компании'), findsOneWidget);
+ });
+ });
+}
+
diff --git a/test/fixtures/doubles/auth_doubles.dart b/test/fixtures/doubles/auth_doubles.dart
index d8ba584f..ad61fedd 100644
--- a/test/fixtures/doubles/auth_doubles.dart
+++ b/test/fixtures/doubles/auth_doubles.dart
@@ -1,6 +1,10 @@
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
+import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart';
+import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart';
+import 'package:eatvio_app/features/auth/models/oauth_verification_request.dart';
+import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
@@ -62,5 +66,15 @@ class _NoopAuthService implements AuthService {
Future