diff --git a/test/features/profile/presentation/basic_widget_test.dart b/test/features/profile/presentation/basic_widget_test.dart new file mode 100644 index 00000000..e92f2906 --- /dev/null +++ b/test/features/profile/presentation/basic_widget_test.dart @@ -0,0 +1,447 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../fixtures/doubles/profile_doubles.dart'; +import '../../../fixtures/fixture_reader.dart'; + +void main() { + group('Profile Basic Widget Tests', () { + late FixtureProfileRepository profileRepository; + late FixtureChatRepository chatRepository; + late FixtureConsultantRepository consultantRepository; + late ProfileCubit profileCubit; + late Map weightFixture; + late Map circumferenceFixture; + + setUp(() { + weightFixture = FixtureReader.readAsMap('profile/add_weight_request.json'); + circumferenceFixture = FixtureReader.readAsMap('profile/add_circumference_request.json'); + profileRepository = FixtureProfileRepository(); + chatRepository = FixtureChatRepository(); + consultantRepository = FixtureConsultantRepository(); + profileCubit = ProfileCubit(profileRepository: profileRepository, chatRepository: chatRepository, consultantRepository: consultantRepository); + }); + + tearDown(() { + profileCubit.close(); + }); + + Widget createTestWidget(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: BlocProvider( + create: (context) => profileCubit, + child: Scaffold(body: SingleChildScrollView(child: child)), + ), + ); + } + + testWidgets('should create ProfileCubit successfully', (WidgetTester tester) async { + // Arrange & Act + final cubit = ProfileCubit(profileRepository: profileRepository, chatRepository: chatRepository, consultantRepository: consultantRepository); + + // Assert + expect(cubit, isA()); + expect(cubit.state, isA()); + + cubit.close(); + }); + + testWidgets('should display weight input field', (WidgetTester tester) async { + // Arrange + Widget weightWidget = TextFormField( + key: const Key('weightField'), + decoration: const InputDecoration(labelText: 'Вес', hintText: 'Введите вес в кг'), + keyboardType: TextInputType.number, + ); + + // Act + await tester.pumpWidget(createTestWidget(weightWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byKey(const Key('weightField')), findsOneWidget); + expect(find.text('Вес'), findsOneWidget); + expect(find.text('Введите вес в кг'), findsOneWidget); + }); + + testWidgets('should update weight when user types in weight field', (WidgetTester tester) async { + // Arrange + Widget weightWidget = TextFormField( + key: const Key('weightField'), + decoration: const InputDecoration(labelText: 'Вес'), + keyboardType: TextInputType.number, + ); + + await tester.pumpWidget(createTestWidget(weightWidget)); + await tester.pumpAndSettle(); + + final testWeight = weightFixture['weight'] as double; + final testWeightString = testWeight.toString(); + + // Act + await tester.enterText(find.byKey(const Key('weightField')), testWeightString); + await tester.pump(); + + // Assert + expect(find.text(testWeightString), findsOneWidget); + }); + + testWidgets('should display circumference input fields', (WidgetTester tester) async { + // Arrange + Widget circumferenceWidget = Column( + children: [ + TextFormField( + key: const Key('chestCircumferenceField'), + decoration: const InputDecoration(labelText: 'Обхват груди (см)'), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + TextFormField( + key: const Key('waistCircumferenceField'), + decoration: const InputDecoration(labelText: 'Обхват талии (см)'), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + TextFormField( + key: const Key('hipCircumferenceField'), + decoration: const InputDecoration(labelText: 'Обхват бедер (см)'), + keyboardType: TextInputType.number, + ), + ], + ); + + // Act + await tester.pumpWidget(createTestWidget(circumferenceWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byKey(const Key('chestCircumferenceField')), findsOneWidget); + expect(find.byKey(const Key('waistCircumferenceField')), findsOneWidget); + expect(find.byKey(const Key('hipCircumferenceField')), findsOneWidget); + expect(find.text('Обхват груди (см)'), findsOneWidget); + expect(find.text('Обхват талии (см)'), findsOneWidget); + expect(find.text('Обхват бедер (см)'), findsOneWidget); + }); + + testWidgets('should update circumference values when user types', (WidgetTester tester) async { + // Arrange + Widget circumferenceWidget = Column( + children: [ + TextFormField( + key: const Key('chestCircumferenceField'), + decoration: const InputDecoration(labelText: 'Обхват груди (см)'), + keyboardType: TextInputType.number, + ), + TextFormField( + key: const Key('waistCircumferenceField'), + decoration: const InputDecoration(labelText: 'Обхват талии (см)'), + keyboardType: TextInputType.number, + ), + TextFormField( + key: const Key('hipCircumferenceField'), + decoration: const InputDecoration(labelText: 'Обхват бедер (см)'), + keyboardType: TextInputType.number, + ), + ], + ); + + await tester.pumpWidget(createTestWidget(circumferenceWidget)); + await tester.pumpAndSettle(); + + final chestValue = (circumferenceFixture['chest_circumference'] as int).toString(); + final waistValue = (circumferenceFixture['waist_circumference'] as int).toString(); + final hipValue = (circumferenceFixture['hip_circumference'] as int).toString(); + + // Act + await tester.enterText(find.byKey(const Key('chestCircumferenceField')), chestValue); + await tester.enterText(find.byKey(const Key('waistCircumferenceField')), waistValue); + await tester.enterText(find.byKey(const Key('hipCircumferenceField')), hipValue); + await tester.pump(); + + // Assert + expect(find.text(chestValue), findsOneWidget); + expect(find.text(waistValue), findsOneWidget); + expect(find.text(hipValue), findsOneWidget); + }); + + testWidgets('should display save weight button', (WidgetTester tester) async { + // Arrange + Widget buttonWidget = ElevatedButton(key: const Key('saveWeightButton'), onPressed: () {}, child: const Text('Сохранить вес')); + + // Act + await tester.pumpWidget(createTestWidget(buttonWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byKey(const Key('saveWeightButton')), findsOneWidget); + expect(find.text('Сохранить вес'), findsOneWidget); + }); + + testWidgets('should display save circumference button', (WidgetTester tester) async { + // Arrange + Widget buttonWidget = ElevatedButton(key: const Key('saveCircumferenceButton'), onPressed: () {}, child: const Text('Сохранить замеры')); + + // Act + await tester.pumpWidget(createTestWidget(buttonWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byKey(const Key('saveCircumferenceButton')), findsOneWidget); + expect(find.text('Сохранить замеры'), findsOneWidget); + }); + + testWidgets('should handle save weight button tap', (WidgetTester tester) async { + // Arrange + bool buttonPressed = false; + Widget buttonWidget = ElevatedButton( + key: const Key('saveWeightButton'), + onPressed: () { + buttonPressed = true; + }, + child: const Text('Сохранить вес'), + ); + + await tester.pumpWidget(createTestWidget(buttonWidget)); + await tester.pumpAndSettle(); + + // Act + await tester.tap(find.byKey(const Key('saveWeightButton'))); + await tester.pump(); + + // Assert + expect(buttonPressed, isTrue); + }); + + testWidgets('should handle save circumference button tap', (WidgetTester tester) async { + // Arrange + bool buttonPressed = false; + Widget buttonWidget = ElevatedButton( + key: const Key('saveCircumferenceButton'), + onPressed: () { + buttonPressed = true; + }, + child: const Text('Сохранить замеры'), + ); + + await tester.pumpWidget(createTestWidget(buttonWidget)); + await tester.pumpAndSettle(); + + // Act + await tester.tap(find.byKey(const Key('saveCircumferenceButton'))); + await tester.pump(); + + // Assert + expect(buttonPressed, isTrue); + }); + + testWidgets('should display profile information fields', (WidgetTester tester) async { + // Arrange + Widget profileWidget = const Column(children: [Text('Имя: Анна'), Text('Email: anna@example.com'), Text('Вес: 65.0 кг'), Text('Рост: 170 см')]); + + // Act + await tester.pumpWidget(createTestWidget(profileWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Имя: Анна'), findsOneWidget); + expect(find.text('Email: anna@example.com'), findsOneWidget); + expect(find.text('Вес: 65.0 кг'), findsOneWidget); + expect(find.text('Рост: 170 см'), findsOneWidget); + }); + + testWidgets('should display weight chart section', (WidgetTester tester) async { + // Arrange + Widget chartWidget = Column( + children: [ + const Text('График веса'), + Container( + key: const Key('weightChartContainer'), + height: 200, + color: Colors.grey[200], + child: const Center(child: Text('График веса')), + ), + ], + ); + + // Act + await tester.pumpWidget(createTestWidget(chartWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('График веса'), findsNWidgets(2)); + expect(find.byKey(const Key('weightChartContainer')), findsOneWidget); + }); + + testWidgets('should display circumference chart section', (WidgetTester tester) async { + // Arrange + Widget chartWidget = Column( + children: [ + const Text('График замеров'), + Container( + key: const Key('circumferenceChartContainer'), + height: 200, + color: Colors.grey[200], + child: const Center(child: Text('График замеров')), + ), + ], + ); + + // Act + await tester.pumpWidget(createTestWidget(chartWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('График замеров'), findsNWidgets(2)); + expect(find.byKey(const Key('circumferenceChartContainer')), findsOneWidget); + }); + + testWidgets('should send correct request when adding weight', (WidgetTester tester) async { + // Arrange + final testWeight = weightFixture['weight'] as double; + + // Act + await profileCubit.addWeight(weight: testWeight); + await tester.pumpAndSettle(); + + // Assert + expect(profileRepository.addWeightRequests.length, 1); + final request = profileRepository.addWeightRequests.single; + expect(request['weight'], testWeight); + }); + + testWidgets('should send correct request when adding circumference', (WidgetTester tester) async { + // Arrange + final chest = circumferenceFixture['chest_circumference'] as int; + final waist = circumferenceFixture['waist_circumference'] as int; + final hip = circumferenceFixture['hip_circumference'] as int; + + // Act + await profileCubit.addCircumference(chestCircumference: chest, waistCircumference: waist, hipCircumference: hip); + await tester.pumpAndSettle(); + + // Assert + expect(profileRepository.addCircumferenceRequests.length, 1); + final request = profileRepository.addCircumferenceRequests.single; + expect(request['chest_circumference'], chest); + expect(request['waist_circumference'], waist); + expect(request['hip_circumference'], hip); + }); + + testWidgets('should display loading state', (WidgetTester tester) async { + // Arrange + Widget loadingWidget = const Center(child: CircularProgressIndicator()); + + // Act + await tester.pumpWidget(createTestWidget(loadingWidget)); + await tester.pump(); + + // Assert + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + + testWidgets('should display error message', (WidgetTester tester) async { + // Arrange + Widget errorWidget = const Center(child: Text('Произошла ошибка при загрузке профиля')); + + // Act + await tester.pumpWidget(createTestWidget(errorWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Произошла ошибка при загрузке профиля'), findsOneWidget); + }); + + testWidgets('should validate weight input', (WidgetTester tester) async { + // Arrange + Widget validationWidget = TextFormField( + key: const Key('weightField'), + decoration: const InputDecoration(labelText: 'Вес', errorText: 'Введите корректное значение веса'), + ); + + // Act + await tester.pumpWidget(createTestWidget(validationWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Введите корректное значение веса'), findsOneWidget); + }); + + testWidgets('should validate circumference inputs', (WidgetTester tester) async { + // Arrange + Widget validationWidget = Column( + children: [ + TextFormField( + key: const Key('chestCircumferenceField'), + decoration: const InputDecoration(labelText: 'Обхват груди (см)', errorText: 'Введите корректное значение'), + ), + TextFormField( + key: const Key('waistCircumferenceField'), + decoration: const InputDecoration(labelText: 'Обхват талии (см)', errorText: 'Введите корректное значение'), + ), + TextFormField( + key: const Key('hipCircumferenceField'), + decoration: const InputDecoration(labelText: 'Обхват бедер (см)', errorText: 'Введите корректное значение'), + ), + ], + ); + + // Act + await tester.pumpWidget(createTestWidget(validationWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Введите корректное значение'), findsNWidgets(3)); + }); + + testWidgets('should display complete profile form', (WidgetTester tester) async { + // Arrange + Widget completeForm = Column( + children: [ + TextFormField( + key: const Key('weightField'), + decoration: const InputDecoration(labelText: 'Вес'), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + TextFormField( + key: const Key('chestCircumferenceField'), + decoration: const InputDecoration(labelText: 'Обхват груди (см)'), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + TextFormField( + key: const Key('waistCircumferenceField'), + decoration: const InputDecoration(labelText: 'Обхват талии (см)'), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + TextFormField( + key: const Key('hipCircumferenceField'), + decoration: const InputDecoration(labelText: 'Обхват бедер (см)'), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 32), + ElevatedButton(key: const Key('saveWeightButton'), onPressed: () {}, child: const Text('Сохранить вес')), + ElevatedButton(key: const Key('saveCircumferenceButton'), onPressed: () {}, child: const Text('Сохранить замеры')), + ], + ); + + // Act + await tester.pumpWidget(createTestWidget(completeForm)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byKey(const Key('weightField')), findsOneWidget); + expect(find.byKey(const Key('chestCircumferenceField')), findsOneWidget); + expect(find.byKey(const Key('waistCircumferenceField')), findsOneWidget); + expect(find.byKey(const Key('hipCircumferenceField')), findsOneWidget); + expect(find.byKey(const Key('saveWeightButton')), findsOneWidget); + expect(find.byKey(const Key('saveCircumferenceButton')), findsOneWidget); + }); + }); +} diff --git a/test/fixtures/doubles/profile_doubles.dart b/test/fixtures/doubles/profile_doubles.dart new file mode 100644 index 00000000..d3b98791 --- /dev/null +++ b/test/fixtures/doubles/profile_doubles.dart @@ -0,0 +1,106 @@ +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; +import 'package:eatvio_app/features/chat/services/chat_service.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart'; +import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; +import 'package:eatvio_app/features/profile/models/circumference_model.dart'; +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:eatvio_app/features/profile/models/weight_model.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:eatvio_app/features/profile/services/profile_service.dart'; + +import '../fixture_reader.dart'; + +class FixtureProfileRepository extends ProfileRepository { + FixtureProfileRepository() : super(profileService: _NoopProfileService()); + + final List> addWeightRequests = >[]; + final List> addCircumferenceRequests = >[]; + + @override + Future getMyProfile() async { + final profileFixture = FixtureReader.readAsMap('profile/my_profile_response.json'); + return ProfileModel.fromJson(profileFixture); + } + + @override + Future getMyProfileDetails() async { + final detailsFixture = FixtureReader.readAsMap('profile/profile_details_response.json'); + return ProfileDetailsModel.fromJson(detailsFixture); + } + + @override + Future addWeight({required double weight}) async { + addWeightRequests.add({'weight': weight}); + } + + @override + Future addCircumference({required int chestCircumference, required int waistCircumference, required int hipCircumference}) async { + addCircumferenceRequests.add({'chest_circumference': chestCircumference, 'waist_circumference': waistCircumference, 'hip_circumference': hipCircumference}); + } + + @override + Future> getWeight({required String period, String? slug}) async { + final weightListFixture = FixtureReader.readAsList('profile/weight_list_response.json'); + return weightListFixture.map((item) => WeightModel.fromJson(item as Map)).toList(); + } + + @override + Future> getCircumferences({required String period, String? slug}) async { + final circumferenceListFixture = FixtureReader.readAsList('profile/circumference_list_response.json'); + return circumferenceListFixture.map((item) => CircumferenceModel.fromJson(item as Map)).toList(); + } + + @override + Future> getBlogPostsByAuthorSlug({required String slug}) async { + return []; + } +} + +class _NoopProfileService implements ProfileService { + @override + noSuchMethod(Invocation invocation) => throw UnimplementedError('ProfileService call ${invocation.memberName}'); +} + +class FixtureChatRepository extends ChatRepository { + FixtureChatRepository() : super(chatService: _NoopChatService()); + + @override + Future getChatByUserSlug(String slug) async { + return 'chat-uuid-123'; + } +} + +class _NoopChatService implements ChatService { + @override + noSuchMethod(Invocation invocation) => throw UnimplementedError('ChatService call ${invocation.memberName}'); +} + +class FixtureConsultantRepository extends ConsultantRepository { + FixtureConsultantRepository() : super(consultantService: _NoopConsultantService()); + + @override + Future getConsultantRequestStatus(String slug) async { + return const ConsultationRequestModel( + educationModel: EducationModel( + hasMedicalEducation: 1, + completedCourses: null, + vkLink: null, + instaLink: null, + fbLink: null, + tgLink: null, + personalSiteLink: null, + requestState: ModerationStatus.approved, + ), + photos: [], + ratingPlace: null, + canceledModeration: null, + ); + } +} + +class _NoopConsultantService implements ConsultantService { + @override + noSuchMethod(Invocation invocation) => throw UnimplementedError('ConsultantService call ${invocation.memberName}'); +} diff --git a/test/fixtures/profile/add_circumference_request.json b/test/fixtures/profile/add_circumference_request.json new file mode 100644 index 00000000..c645ed0d --- /dev/null +++ b/test/fixtures/profile/add_circumference_request.json @@ -0,0 +1,6 @@ +{ + "chest_circumference": 100, + "waist_circumference": 80, + "hip_circumference": 90 +} + diff --git a/test/fixtures/profile/add_weight_request.json b/test/fixtures/profile/add_weight_request.json new file mode 100644 index 00000000..9f588499 --- /dev/null +++ b/test/fixtures/profile/add_weight_request.json @@ -0,0 +1,4 @@ +{ + "weight": 75.5 +} + diff --git a/test/fixtures/profile/circumference_list_response.json b/test/fixtures/profile/circumference_list_response.json new file mode 100644 index 00000000..c9750086 --- /dev/null +++ b/test/fixtures/profile/circumference_list_response.json @@ -0,0 +1,30 @@ +[ + { + "id": 1, + "user_id": 99, + "chest_circumference": 100, + "waist_circumference": 80, + "hip_circumference": 90, + "created_at": "2024-01-15T10:00:00Z", + "updated_at": "2024-01-15T10:00:00Z" + }, + { + "id": 2, + "user_id": 99, + "chest_circumference": 98, + "waist_circumference": 78, + "hip_circumference": 88, + "created_at": "2024-01-22T10:00:00Z", + "updated_at": "2024-01-22T10:00:00Z" + }, + { + "id": 3, + "user_id": 99, + "chest_circumference": 96, + "waist_circumference": 76, + "hip_circumference": 86, + "created_at": "2024-01-29T10:00:00Z", + "updated_at": "2024-01-29T10:00:00Z" + } +] + diff --git a/test/fixtures/profile/profile_details_response.json b/test/fixtures/profile/profile_details_response.json new file mode 100644 index 00000000..6c14bac9 --- /dev/null +++ b/test/fixtures/profile/profile_details_response.json @@ -0,0 +1,30 @@ +{ + "first_name": "Анна", + "last_name": "Нутрициолог", + "birthdate": "1990-05-15", + "gender": "female", + "country": "Россия", + "city": "Москва", + "lang": "ru", + "weight": 65.0, + "height": 170, + "chest_circumference": 90, + "waist_circumference": 70, + "hip_circumference": 95, + "blood_pressure_up": 120, + "blood_pressure_down": 80, + "has_hypo": false, + "has_hyper": false, + "about_alergy": "Нет аллергий", + "about_taking_pils": "Не принимаю лекарства", + "about_sport_hobbies": "Йога, бег", + "favorite_foods": "Овощи, фрукты", + "unfavorite_foods": "Жареное", + "additional_info": "Веду здоровый образ жизни", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-15T10:00:00Z", + "is_mood_fillable": true, + "is_bedtime_fillable": true, + "activity_coefficient": 1.5 +} + diff --git a/test/fixtures/profile/weight_list_response.json b/test/fixtures/profile/weight_list_response.json new file mode 100644 index 00000000..3aeae8bb --- /dev/null +++ b/test/fixtures/profile/weight_list_response.json @@ -0,0 +1,27 @@ +[ + { + "id": 1, + "user_id": 99, + "weight": 75, + "created_at": "2024-01-15T10:00:00Z", + "updated_at": "2024-01-15T10:00:00Z", + "bmi": 23.5 + }, + { + "id": 2, + "user_id": 99, + "weight": 74, + "created_at": "2024-01-22T10:00:00Z", + "updated_at": "2024-01-22T10:00:00Z", + "bmi": 23.2 + }, + { + "id": 3, + "user_id": 99, + "weight": 73, + "created_at": "2024-01-29T10:00:00Z", + "updated_at": "2024-01-29T10:00:00Z", + "bmi": 22.9 + } +] +