Merge pull request 'implement OAuth verification request and confirmation flow with error handling and UI updates' (#811) from mso7nueh/eatvio_app:main into main

Reviewed-on: eatvio/eatvio_app#811
This commit is contained in:
mso7nueh
2025-12-17 12:42:26 +00:00
60 changed files with 2226 additions and 39 deletions

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

@@ -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,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<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,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<String, dynamic> json) => _$OAuthVerificationRequestFromJson(json);
}

View File

@@ -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<String, dynamic> json) => _$OAuthVerificationRequestResponseFromJson(json);
}

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,69 @@ 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: 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<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

@@ -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,
@@ -501,6 +546,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 +558,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 +570,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 (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,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<TokenModel> signInViaSocialMedia({
required String provider,
required String accessToken,
@@ -73,4 +77,60 @@ class AuthRepository {
rethrow;
}
}
Future<OAuthVerificationRequestResponse> requestOAuthVerification({String? email, String? phone}) async {
try {
return await authService.requestOAuthVerification(
body: OAuthVerificationRequest(email: email, phone: phone),
);
} 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,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<TokenModel> signInViaSocialMedia({@Body() required OAuthLoginRequest body});
@POST('/api/oauth/verification/request')
Future<OAuthVerificationRequestResponse> requestOAuthVerification({@Body() required OAuthVerificationRequest body});
@POST('/api/oauth/verification/confirm')
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({@Body() required OAuthVerificationConfirmRequest body});
}

View File

@@ -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:

View File

@@ -136,3 +136,7 @@ void main() {

View File

@@ -71,3 +71,7 @@ void main() {

View File

@@ -85,3 +85,7 @@ void main() {

View File

@@ -34,3 +34,7 @@ void main() {

View File

@@ -30,3 +30,7 @@ void main() {

View File

@@ -27,3 +27,7 @@ void main() {

View File

@@ -31,3 +31,7 @@ void main() {

View File

@@ -110,3 +110,7 @@ void main() {

View File

@@ -34,3 +34,7 @@ void main() {

View File

@@ -28,3 +28,7 @@ void main() {

View File

@@ -64,3 +64,7 @@ void main() {

View File

@@ -116,3 +116,7 @@ void main() {

View File

@@ -53,3 +53,7 @@ void main() {

View File

@@ -135,3 +135,7 @@ void main() {

View File

@@ -96,3 +96,7 @@ void main() {

View File

@@ -164,3 +164,7 @@ void main() {

View File

@@ -57,3 +57,7 @@ void main() {

View File

@@ -42,3 +42,7 @@ void main() {

View File

@@ -96,3 +96,7 @@ void main() {

View File

@@ -76,3 +76,7 @@ void main() {

View File

@@ -65,3 +65,7 @@ void main() {

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,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<ChatsCubit>()])
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<ChatsCubit>(
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
});
});
}

View File

@@ -0,0 +1,89 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/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>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
Widget createTestWidget({
required List<dynamic> 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
});
});
}

View File

@@ -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', () {

View File

@@ -71,3 +71,7 @@ void main() {

View File

@@ -58,3 +58,7 @@ void main() {

View File

@@ -90,3 +90,7 @@ void main() {

View File

@@ -108,3 +108,7 @@ void main() {

View File

@@ -138,3 +138,7 @@ void main() {

View File

@@ -50,3 +50,7 @@ void main() {

View File

@@ -51,3 +51,7 @@ void main() {

View File

@@ -60,3 +60,7 @@ void main() {
});
});
}

View File

@@ -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>(() => 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);
});
});
}

View File

@@ -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>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
mockProfileRepository = MockProfileRepository();
subCubit = SubCubit(profileRepository: mockProfileRepository);
sl.registerLazySingleton<SubCubit>(() => 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);
});
});
}

View File

@@ -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<RegistrationCubit>(
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<TextFormField>(textFields.first);
expect(firstField.controller?.text, '100');
final secondField = tester.widget<TextFormField>(textFields.at(1));
expect(secondField.controller?.text, '80');
final thirdField = tester.widget<TextFormField>(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<TextFormField>(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));
});
});
}

View File

@@ -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<RegistrationCubit>(
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);
});
});
}

View File

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

View File

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

View File

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

View File

@@ -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>(() => 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);
});
});
}

View File

@@ -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<Map<String, Object?>> signUpViaSocialMedia({required OAuthModel body}) {
throw UnimplementedError('Not implemented in fixture.');
}
@override
Future<OAuthVerificationRequestResponse> requestOAuthVerification({required OAuthVerificationRequest body}) {
throw UnimplementedError('Not implemented in fixture.');
}
@override
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({required OAuthVerificationConfirmRequest body}) {
throw UnimplementedError('Not implemented in fixture.');
}
}

View File

@@ -7,9 +7,6 @@ import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/profile/services/profile_service.dart';
import 'package:retrofit/dio.dart' show HttpResponse;
import '../../fixtures/fixture_reader.dart';
@@ -133,16 +130,6 @@ class FixtureCreateDietRepository extends CreateDietRepository {
}
}
class FixtureProfileRepository extends ProfileRepository {
FixtureProfileRepository() : super(profileService: _NoopProfileService());
@override
Future<ProfileModel> getMyProfile() async {
final profileFixture = FixtureReader.readAsMap('profile/my_profile_response.json');
return ProfileModel.fromJson(profileFixture);
}
}
class FixtureGlossaryRepository extends GlossaryRepository {
FixtureGlossaryRepository() : super(glossaryService: _NoopGlossaryService());
@@ -190,11 +177,6 @@ class _NoopCreateDietService implements CreateDietService {
}
}
class _NoopProfileService implements ProfileService {
@override
noSuchMethod(Invocation invocation) => throw UnimplementedError('ProfileService call ${invocation.memberName}');
}
class _NoopGlossaryService implements GlossaryService {
@override
noSuchMethod(Invocation invocation) => throw UnimplementedError('GlossaryService call ${invocation.memberName}');