forked from eatvio/eatvio_app
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:
@@ -388,7 +388,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 87;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -539,7 +539,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 87;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -578,7 +578,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 87;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>87</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
12
lib/features/auth/models/oauth_verification_request.dart
Normal file
12
lib/features/auth/models/oauth_verification_request.dart
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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/mixins/registration_date_cache_mixin.dart';
|
||||||
import 'package:eatvio_app/core/prefs.dart';
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.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/presentation/bloc/oauth_cubit/oauth_state.dart';
|
||||||
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
|
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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}');
|
log('OAuthCubit: Got token model: ${tokenModel.token}');
|
||||||
TokenStorage.saveTokenInStorage(tokenModel.token);
|
TokenStorage.saveTokenInStorage(tokenModel.token);
|
||||||
emit(OAuthState.loaded(token: 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) {
|
} catch (e) {
|
||||||
log('OAuthCubit: Error occurred: $e');
|
log('OAuthCubit: Error occurred: $e');
|
||||||
emit(OAuthState.error(errorMessage: e.toString()));
|
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 dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
|
||||||
final errorsList = <RegistrationValidationErrors>[];
|
final errorsList = <RegistrationValidationErrors>[];
|
||||||
if (emailErrors != null && emailErrors.isNotEmpty) {
|
if (emailErrors != null && emailErrors.isNotEmpty) {
|
||||||
if (emailErrors.contains('Некорректный формат email')) {
|
for (final error in emailErrors) {
|
||||||
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
final errorStr = error.toString();
|
||||||
}
|
if (errorStr.contains('Некорректный формат email')) {
|
||||||
if (emailErrors.contains('Такой Эл. почта уже существует.')) {
|
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
||||||
errorsList.add(RegistrationValidationErrors.emailExist);
|
}
|
||||||
|
if (errorStr.contains('почта уже существует') || errorStr.contains('Эл. почта уже существует')) {
|
||||||
|
errorsList.add(RegistrationValidationErrors.emailExist);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
||||||
if (phoneErrors.contains('Такой phone уже существует.')) {
|
for (final error in phoneErrors) {
|
||||||
errorsList.add(RegistrationValidationErrors.phoneExist);
|
final errorStr = error.toString();
|
||||||
|
if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) {
|
||||||
|
errorsList.add(RegistrationValidationErrors.phoneExist);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
|
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
|
||||||
@@ -101,6 +121,69 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
|
|
||||||
return null;
|
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 }
|
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/const.dart';
|
import 'package:eatvio_app/core/const.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
@@ -49,6 +50,8 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
bool weightError = false;
|
bool weightError = false;
|
||||||
bool heightError = false;
|
bool heightError = false;
|
||||||
bool incorrectEmailFormatError = false;
|
bool incorrectEmailFormatError = false;
|
||||||
|
bool showVerificationButton = false;
|
||||||
|
String? verificationType; // 'email' or 'phone'
|
||||||
|
|
||||||
// Состояние для выбора пола
|
// Состояние для выбора пола
|
||||||
bool isMaleSelected = true;
|
bool isMaleSelected = true;
|
||||||
@@ -221,7 +224,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
hasError: emailExistError || incorrectEmailFormatError,
|
hasError: emailExistError || incorrectEmailFormatError,
|
||||||
onChanged: (newValue) {},
|
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')],
|
if (incorrectEmailFormatError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат email')],
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text('Номер телефона'),
|
const Text('Номер телефона'),
|
||||||
@@ -235,7 +259,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
hasError: phoneExistError,
|
hasError: phoneExistError,
|
||||||
onChanged: (newValue) {},
|
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),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -501,6 +546,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
clientId: clientID,
|
clientId: clientID,
|
||||||
);
|
);
|
||||||
if (errors != null) {
|
if (errors != null) {
|
||||||
|
bool hasEmailExist = false;
|
||||||
|
bool hasPhoneExist = false;
|
||||||
|
|
||||||
|
// Сначала определяем, какие ошибки присутствуют
|
||||||
for (final error in errors) {
|
for (final error in errors) {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case RegistrationValidationErrors.incorrectEmailFormat:
|
case RegistrationValidationErrors.incorrectEmailFormat:
|
||||||
@@ -509,14 +558,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case RegistrationValidationErrors.emailExist:
|
case RegistrationValidationErrors.emailExist:
|
||||||
setState(() {
|
hasEmailExist = true;
|
||||||
emailExistError = true;
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case RegistrationValidationErrors.phoneExist:
|
case RegistrationValidationErrors.phoneExist:
|
||||||
setState(() {
|
hasPhoneExist = true;
|
||||||
phoneExistError = true;
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case RegistrationValidationErrors.incorrectDateOfBirthFormat:
|
case RegistrationValidationErrors.incorrectDateOfBirthFormat:
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -525,8 +570,236 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
break;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
// Регистрация успешна только если нет ошибок
|
||||||
showSuccessToast('Регистрация успешна');
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'package:eatvio_app/features/auth/models/credentials_model.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_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_login_request.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/models/token_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/services/auth_service.dart';
|
import 'package:eatvio_app/features/auth/services/auth_service.dart';
|
||||||
|
|
||||||
@@ -19,7 +24,6 @@ class AuthRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<TokenModel> signInViaSocialMedia({
|
Future<TokenModel> signInViaSocialMedia({
|
||||||
required String provider,
|
required String provider,
|
||||||
required String accessToken,
|
required String accessToken,
|
||||||
@@ -73,4 +77,60 @@ class AuthRepository {
|
|||||||
rethrow;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/credentials_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_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_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/models/token_model.dart';
|
||||||
import 'package:retrofit/retrofit.dart';
|
import 'package:retrofit/retrofit.dart';
|
||||||
|
|
||||||
@@ -19,4 +23,10 @@ abstract class AuthService {
|
|||||||
|
|
||||||
@POST('/api/oauth/login')
|
@POST('/api/oauth/login')
|
||||||
Future<TokenModel> signInViaSocialMedia({@Body() required OAuthLoginRequest body});
|
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});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: eatvio_app
|
name: eatvio_app
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
version: 1.3.7+87
|
version: 1.3.8+88
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -136,3 +136,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -71,3 +71,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -85,3 +85,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,3 +34,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -60,3 +60,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,3 +30,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,3 +27,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,3 +31,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -110,3 +110,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,3 +34,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,3 +28,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -64,3 +64,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -116,3 +116,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,3 +53,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -135,3 +135,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -96,3 +96,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -164,3 +164,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -57,3 +57,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,3 +42,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -96,3 +96,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -76,3 +76,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -65,3 +65,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
112
test/features/chat/presentation/pages/chats_screen_test.dart
Normal file
112
test/features/chat/presentation/pages/chats_screen_test.dart
Normal 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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../../fixtures/doubles/create_diet_doubles.dart';
|
import '../../fixtures/doubles/create_diet_doubles.dart';
|
||||||
|
import '../../fixtures/doubles/profile_doubles.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('Diet Creation Basic Widget Tests', () {
|
group('Diet Creation Basic Widget Tests', () {
|
||||||
|
|||||||
@@ -71,3 +71,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,3 +80,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -58,3 +58,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,3 +53,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -90,3 +90,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,3 +42,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -108,3 +108,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -138,3 +138,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -50,3 +50,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -58,3 +58,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -51,3 +51,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1028,3 +1028,7 @@ void main() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -60,3 +60,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
118
test/features/profile/presentation/pages/subs_screen_test.dart
Normal file
118
test/features/profile/presentation/pages/subs_screen_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
14
test/fixtures/doubles/auth_doubles.dart
vendored
14
test/fixtures/doubles/auth_doubles.dart
vendored
@@ -1,6 +1,10 @@
|
|||||||
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
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_login_request.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_model.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/models/token_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
|
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
|
||||||
import 'package:eatvio_app/features/auth/services/auth_service.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}) {
|
Future<Map<String, Object?>> signUpViaSocialMedia({required OAuthModel body}) {
|
||||||
throw UnimplementedError('Not implemented in fixture.');
|
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.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
test/fixtures/doubles/create_diet_doubles.dart
vendored
18
test/fixtures/doubles/create_diet_doubles.dart
vendored
@@ -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/models/glossary_model.dart';
|
||||||
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.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/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 'package:retrofit/dio.dart' show HttpResponse;
|
||||||
|
|
||||||
import '../../fixtures/fixture_reader.dart';
|
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 {
|
class FixtureGlossaryRepository extends GlossaryRepository {
|
||||||
FixtureGlossaryRepository() : super(glossaryService: _NoopGlossaryService());
|
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 {
|
class _NoopGlossaryService implements GlossaryService {
|
||||||
@override
|
@override
|
||||||
noSuchMethod(Invocation invocation) => throw UnimplementedError('GlossaryService call ${invocation.memberName}');
|
noSuchMethod(Invocation invocation) => throw UnimplementedError('GlossaryService call ${invocation.memberName}');
|
||||||
|
|||||||
Reference in New Issue
Block a user