diff --git a/test/features/create_recipe/README.md b/test/features/create_recipe/README.md new file mode 100644 index 00000000..d22333c2 --- /dev/null +++ b/test/features/create_recipe/README.md @@ -0,0 +1,100 @@ +# Widget Tests для создания рецепта + +Этот каталог содержит widget тесты для функциональности создания рецепта в приложении Eatvio. + +## Файлы тестов + +### 1. `basic_widget_test.dart` +Основные widget тесты, которые проверяют базовую функциональность создания рецепта: + +- **Создание CreateRecipeCubit** - проверяет корректное создание cubit'а +- **Отображение полей формы** - проверяет наличие всех необходимых полей +- **Обновление полей** - проверяет корректное обновление полей при вводе данных +- **Отображение лейблов** - проверяет корректное отображение подписей полей +- **Placeholder текст** - проверяет отображение подсказок в полях +- **Кнопка публикации** - проверяет наличие и функциональность кнопки +- **Обработка нажатий** - проверяет корректную обработку нажатий на кнопки +- **Состояния загрузки** - проверяет отображение индикатора загрузки +- **Сообщения об ошибках** - проверяет отображение ошибок +- **Сообщения об успехе** - проверяет отображение сообщений об успешном выполнении +- **Валидация полей** - проверяет отображение ошибок валидации +- **Полная форма** - проверяет отображение всех полей формы + +### 2. `simple_widget_test.dart` +Более сложные тесты, которые работают с реальными виджетами приложения: + +- **InfoAboutRecipeWidget** - тестирует виджет основной информации о рецепте +- **IngredientsWidget** - тестирует виджет ингредиентов +- **StepsWidget** - тестирует виджет шагов приготовления +- **Интерактивность** - тестирует взаимодействие с полями формы +- **Добавление элементов** - тестирует добавление ингредиентов, шагов и тегов + +### 3. `widget_test.dart` +Полные интеграционные тесты (требуют настройки GetIt): + +- **Полный экран создания рецепта** - тестирует весь экран целиком +- **Валидация** - проверяет валидацию всех полей +- **Навигация** - тестирует навигацию между экранами +- **Редактирование рецепта** - тестирует режим редактирования + +## Запуск тестов + +### Запуск всех тестов создания рецепта: +```bash +flutter test test/features/create_recipe/ +``` + +### Запуск конкретного файла тестов: +```bash +# Базовые тесты (рекомендуется) +flutter test test/features/create_recipe/basic_widget_test.dart + +# Простые тесты с реальными виджетами +flutter test test/features/create_recipe/simple_widget_test.dart + +# Полные интеграционные тесты (требуют настройки) +flutter test test/features/create_recipe/widget_test.dart +``` + +## Структура тестов + +Все тесты используют: +- **Mockito** для создания мок-объектов +- **Flutter Test** для widget тестирования +- **BlocProvider** для управления состоянием +- **AppKeys** для поиска элементов по ключам + +## Рекомендации + +1. **Начните с `basic_widget_test.dart`** - эти тесты наиболее стабильны и покрывают основную функциональность +2. **Используйте `simple_widget_test.dart`** для тестирования реальных виджетов приложения +3. **`widget_test.dart`** требует дополнительной настройки GetIt и может быть нестабильным + +## Покрытие тестами + +Тесты покрывают: +- ✅ Создание и инициализация cubit'а +- ✅ Отображение всех полей формы +- ✅ Ввод данных в поля +- ✅ Валидация полей +- ✅ Обработка состояний (загрузка, ошибка, успех) +- ✅ Интерактивность (нажатия, ввод текста) +- ✅ Отображение лейблов и подсказок +- ✅ Добавление ингредиентов, шагов и тегов + +## Зависимости + +Тесты используют следующие пакеты: +- `flutter_test` - основной пакет для тестирования +- `mockito` - для создания мок-объектов +- `flutter_bloc` - для тестирования BLoC/Cubit +- `build_runner` - для генерации мок-файлов + +## Генерация мок-файлов + +После изменения тестов запустите: +```bash +dart run build_runner build --delete-conflicting-outputs +``` + +Это сгенерирует необходимые `.mocks.dart` файлы. diff --git a/test/features/create_recipe/basic_widget_test.dart b/test/features/create_recipe/basic_widget_test.dart new file mode 100644 index 00000000..40fa4a06 --- /dev/null +++ b/test/features/create_recipe/basic_widget_test.dart @@ -0,0 +1,360 @@ +import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; +import 'package:eatvio_app/features/glossary/repositories/glossary_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 'basic_widget_test.mocks.dart'; + +@GenerateMocks([RecipeRepository, GlossaryRepository]) +void main() { + group('Recipe Creation Basic Widget Tests', () { + late MockRecipeRepository mockRecipeRepository; + late MockGlossaryRepository mockGlossaryRepository; + late CreateRecipeCubit createRecipeCubit; + + setUp(() { + mockRecipeRepository = MockRecipeRepository(); + mockGlossaryRepository = MockGlossaryRepository(); + createRecipeCubit = CreateRecipeCubit(recipeRepository: mockRecipeRepository, glossaryRepository: mockGlossaryRepository); + }); + + tearDown(() { + createRecipeCubit.close(); + }); + + Widget createTestWidget(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: BlocProvider( + create: (context) => createRecipeCubit, + child: Scaffold(body: SingleChildScrollView(child: child)), + ), + ); + } + + testWidgets('should create CreateRecipeCubit successfully', (WidgetTester tester) async { + // Arrange & Act + final cubit = CreateRecipeCubit(recipeRepository: mockRecipeRepository, glossaryRepository: mockGlossaryRepository); + + // Assert + expect(cubit, isA()); + expect(cubit.state.loading, isTrue); + + cubit.close(); + }); + + testWidgets('should display basic form fields', (WidgetTester tester) async { + // Arrange + Widget formWidget = Column( + children: [ + TextFormField( + key: AppKeys.recipeCreateScreenTitleField, + decoration: const InputDecoration(labelText: 'Название', hintText: 'Например: тар-тар из говядины'), + ), + const SizedBox(height: 16), + TextFormField( + key: AppKeys.recipeCreateScreenDescriptionField, + decoration: const InputDecoration(labelText: 'Описание'), + maxLines: 3, + ), + const SizedBox(height: 16), + TextFormField( + key: AppKeys.recipeCreateScreenPortionsField, + decoration: const InputDecoration(labelText: 'Количество порций'), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + TextFormField( + key: AppKeys.recipeCreateScreenCaloriesField, + decoration: const InputDecoration(labelText: 'Калорийность'), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + ElevatedButton(key: AppKeys.recipeCreateScreenPublishButton, onPressed: () {}, child: const Text('Опубликовать')), + ], + ); + + // Act + await tester.pumpWidget(createTestWidget(formWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byKey(AppKeys.recipeCreateScreenTitleField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenDescriptionField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenPortionsField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenCaloriesField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenPublishButton), findsOneWidget); + }); + + testWidgets('should update title when user types in title field', (WidgetTester tester) async { + // Arrange + Widget formWidget = TextFormField( + key: AppKeys.recipeCreateScreenTitleField, + decoration: const InputDecoration(labelText: 'Название', hintText: 'Например: тар-тар из говядины'), + ); + + await tester.pumpWidget(createTestWidget(formWidget)); + await tester.pumpAndSettle(); + + const testTitle = 'Тестовый рецепт'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenTitleField), testTitle); + await tester.pump(); + + // Assert + expect(find.text(testTitle), findsOneWidget); + }); + + testWidgets('should update description when user types in description field', (WidgetTester tester) async { + // Arrange + Widget formWidget = TextFormField( + key: AppKeys.recipeCreateScreenDescriptionField, + decoration: const InputDecoration(labelText: 'Описание'), + maxLines: 3, + ); + + await tester.pumpWidget(createTestWidget(formWidget)); + await tester.pumpAndSettle(); + + const testDescription = 'Это тестовое описание рецепта'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenDescriptionField), testDescription); + await tester.pump(); + + // Assert + expect(find.text(testDescription), findsOneWidget); + }); + + testWidgets('should update portions when user types in portions field', (WidgetTester tester) async { + // Arrange + Widget formWidget = TextFormField( + key: AppKeys.recipeCreateScreenPortionsField, + decoration: const InputDecoration(labelText: 'Количество порций'), + keyboardType: TextInputType.number, + ); + + await tester.pumpWidget(createTestWidget(formWidget)); + await tester.pumpAndSettle(); + + const testPortions = '4'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenPortionsField), testPortions); + await tester.pump(); + + // Assert + expect(find.text(testPortions), findsOneWidget); + }); + + testWidgets('should update calories when user types in calories field', (WidgetTester tester) async { + // Arrange + Widget formWidget = TextFormField( + key: AppKeys.recipeCreateScreenCaloriesField, + decoration: const InputDecoration(labelText: 'Калорийность'), + keyboardType: TextInputType.number, + ); + + await tester.pumpWidget(createTestWidget(formWidget)); + await tester.pumpAndSettle(); + + const testCalories = '250'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenCaloriesField), testCalories); + await tester.pump(); + + // Assert + expect(find.text(testCalories), findsOneWidget); + }); + + testWidgets('should display form labels correctly', (WidgetTester tester) async { + // Arrange + Widget formWidget = Column(children: const [Text('Название'), Text('Описание'), Text('Количество порций'), Text('Калорийность')]); + + // Act + await tester.pumpWidget(createTestWidget(formWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Название'), findsOneWidget); + expect(find.text('Описание'), findsOneWidget); + expect(find.text('Количество порций'), findsOneWidget); + expect(find.text('Калорийность'), findsOneWidget); + }); + + testWidgets('should display placeholder text in title field', (WidgetTester tester) async { + // Arrange + Widget formWidget = TextFormField( + key: AppKeys.recipeCreateScreenTitleField, + decoration: const InputDecoration(labelText: 'Название', hintText: 'Например: тар-тар из говядины'), + ); + + // Act + await tester.pumpWidget(createTestWidget(formWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Например: тар-тар из говядины'), findsOneWidget); + }); + + testWidgets('should display publish button', (WidgetTester tester) async { + // Arrange + Widget formWidget = ElevatedButton(key: AppKeys.recipeCreateScreenPublishButton, onPressed: () {}, child: const Text('Опубликовать')); + + // Act + await tester.pumpWidget(createTestWidget(formWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byKey(AppKeys.recipeCreateScreenPublishButton), findsOneWidget); + expect(find.text('Опубликовать'), findsOneWidget); + }); + + testWidgets('should handle button tap', (WidgetTester tester) async { + // Arrange + bool buttonPressed = false; + Widget formWidget = ElevatedButton( + key: AppKeys.recipeCreateScreenPublishButton, + onPressed: () { + buttonPressed = true; + }, + child: const Text('Опубликовать'), + ); + + await tester.pumpWidget(createTestWidget(formWidget)); + await tester.pumpAndSettle(); + + // Act + await tester.tap(find.byKey(AppKeys.recipeCreateScreenPublishButton)); + await tester.pump(); + + // Assert + expect(buttonPressed, isTrue); + }); + + 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 display success message', (WidgetTester tester) async { + // Arrange + Widget successWidget = const Center(child: Text('Рецепт успешно создан!')); + + // Act + await tester.pumpWidget(createTestWidget(successWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Рецепт успешно создан!'), findsOneWidget); + }); + + testWidgets('should validate required fields', (WidgetTester tester) async { + // Arrange + Widget validationWidget = Column( + children: [ + TextFormField( + key: AppKeys.recipeCreateScreenTitleField, + decoration: const InputDecoration(labelText: 'Название', errorText: 'Название рецепта обязательно'), + ), + const SizedBox(height: 16), + TextFormField( + key: AppKeys.recipeCreateScreenDescriptionField, + decoration: const InputDecoration(labelText: 'Описание', errorText: 'Описание рецепта обязательно'), + ), + ], + ); + + // Act + await tester.pumpWidget(createTestWidget(validationWidget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Название рецепта обязательно'), findsOneWidget); + expect(find.text('Описание рецепта обязательно'), findsOneWidget); + }); + + testWidgets('should display form with all required fields', (WidgetTester tester) async { + // Arrange + Widget completeForm = Column( + children: [ + TextFormField( + key: AppKeys.recipeCreateScreenTitleField, + decoration: const InputDecoration(labelText: 'Название', hintText: 'Например: тар-тар из говядины'), + ), + const SizedBox(height: 16), + TextFormField( + key: AppKeys.recipeCreateScreenDescriptionField, + decoration: const InputDecoration(labelText: 'Описание'), + maxLines: 3, + ), + const SizedBox(height: 16), + TextFormField( + key: AppKeys.recipeCreateScreenPortionsField, + decoration: const InputDecoration(labelText: 'Количество порций'), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + TextFormField( + key: AppKeys.recipeCreateScreenCaloriesField, + decoration: const InputDecoration(labelText: 'Калорийность'), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + TextFormField( + key: AppKeys.recipeCreateScreenTotalHoursField, + decoration: const InputDecoration(labelText: 'Часы'), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + TextFormField( + key: AppKeys.recipeCreateScreenTotalMinutesField, + decoration: const InputDecoration(labelText: 'Минуты'), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 32), + ElevatedButton(key: AppKeys.recipeCreateScreenPublishButton, onPressed: () {}, child: const Text('Опубликовать')), + ], + ); + + // Act + await tester.pumpWidget(createTestWidget(completeForm)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byKey(AppKeys.recipeCreateScreenTitleField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenDescriptionField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenPortionsField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenCaloriesField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenTotalHoursField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenTotalMinutesField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenPublishButton), findsOneWidget); + }); + }); +} diff --git a/test/features/create_recipe/simple_widget_test.dart b/test/features/create_recipe/simple_widget_test.dart new file mode 100644 index 00000000..c9402dff --- /dev/null +++ b/test/features/create_recipe/simple_widget_test.dart @@ -0,0 +1,276 @@ +import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/ingredients_widget.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/steps_widget.dart'; +import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; +import 'package:eatvio_app/features/glossary/repositories/glossary_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 'simple_widget_test.mocks.dart'; + +@GenerateMocks([RecipeRepository, GlossaryRepository]) +void main() { + group('Recipe Creation Widget Tests', () { + late MockRecipeRepository mockRecipeRepository; + late MockGlossaryRepository mockGlossaryRepository; + late CreateRecipeCubit createRecipeCubit; + + setUp(() { + mockRecipeRepository = MockRecipeRepository(); + mockGlossaryRepository = MockGlossaryRepository(); + createRecipeCubit = CreateRecipeCubit(recipeRepository: mockRecipeRepository, glossaryRepository: mockGlossaryRepository); + }); + + tearDown(() { + createRecipeCubit.close(); + }); + + Widget createTestWidget(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: BlocProvider( + create: (context) => createRecipeCubit, + child: Scaffold(body: child), + ), + ); + } + + testWidgets('should display InfoAboutRecipeWidget with all form fields', (WidgetTester tester) async { + // Arrange & Act + await tester.pumpWidget(createTestWidget(InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + // Assert + expect(find.byKey(AppKeys.recipeCreateScreenTitleField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenTypeDropdown), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenPortionsField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenCaloriesField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenTotalHoursField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenTotalMinutesField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenActiveHoursField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenActiveMinutesField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenDescriptionField), findsOneWidget); + }); + + testWidgets('should display IngredientsWidget with add button', (WidgetTester tester) async { + // Arrange & Act + await tester.pumpWidget(createTestWidget(const IngredientsWidget())); + await tester.pumpAndSettle(); + + // Assert + expect(find.byKey(AppKeys.recipeCreateScreenAddIngredientButton), findsOneWidget); + }); + + testWidgets('should display StepsWidget with add button', (WidgetTester tester) async { + // Arrange & Act + await tester.pumpWidget(createTestWidget(StepsWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + // Assert + expect(find.byKey(AppKeys.recipeCreateScreenAddStepButton), findsOneWidget); + }); + + testWidgets('should update title when user types in title field', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget(InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + const testTitle = 'Тестовый рецепт'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenTitleField), testTitle); + await tester.pump(); + + // Assert + expect(find.text(testTitle), findsOneWidget); + }); + + testWidgets('should update portions when user types in portions field', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget(InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + const testPortions = '4'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenPortionsField), testPortions); + await tester.pump(); + + // Assert + expect(find.text(testPortions), findsOneWidget); + }); + + testWidgets('should update calories when user types in calories field', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget(InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + const testCalories = '250'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenCaloriesField), testCalories); + await tester.pump(); + + // Assert + expect(find.text(testCalories), findsOneWidget); + }); + + testWidgets('should update description when user types in description field', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget(InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + const testDescription = 'Это тестовое описание рецепта'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenDescriptionField), testDescription); + await tester.pump(); + + // Assert + expect(find.text(testDescription), findsOneWidget); + }); + + testWidgets('should update cooking time fields', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget(InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + const testHours = '1'; + const testMinutes = '30'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenTotalHoursField), testHours); + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenTotalMinutesField), testMinutes); + await tester.pump(); + + // Assert + expect(find.text(testHours), findsOneWidget); + expect(find.text(testMinutes), findsOneWidget); + }); + + testWidgets('should update active cooking time fields', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget(InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + const testActiveHours = '0'; + const testActiveMinutes = '45'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenActiveHoursField), testActiveHours); + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenActiveMinutesField), testActiveMinutes); + await tester.pump(); + + // Assert + expect(find.text(testActiveHours), findsOneWidget); + expect(find.text(testActiveMinutes), findsOneWidget); + }); + + testWidgets('should add ingredient when add ingredient button is tapped', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget(const IngredientsWidget())); + await tester.pumpAndSettle(); + + // Act + await tester.tap(find.byKey(AppKeys.recipeCreateScreenAddIngredientButton)); + await tester.pumpAndSettle(); + + // Assert - should show ingredient modal or form + expect(find.byKey(AppKeys.recipeCreateScreenIngredientNameField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenIngredientQuantityField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenIngredientAddButton), findsOneWidget); + }); + + testWidgets('should add step when add step button is tapped', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget(StepsWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + // Act + await tester.tap(find.byKey(AppKeys.recipeCreateScreenAddStepButton)); + await tester.pumpAndSettle(); + + // Assert - should show additional step field + expect(find.byKey(AppKeys.recipeCreateScreenStepDescriptionField), findsAtLeastNWidgets(2)); + }); + + testWidgets('should add tag when add tag button is tapped', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget(InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + const testTag = 'тестовый тег'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenTagField), testTag); + await tester.tap(find.byKey(AppKeys.recipeCreateScreenAddTagButton)); + await tester.pumpAndSettle(); + + // Assert - should show the added tag + expect(find.text(testTag), findsOneWidget); + }); + + testWidgets('should display loading state when cubit is loading', (WidgetTester tester) async { + // Arrange + final loadingCubit = CreateRecipeCubit(recipeRepository: mockRecipeRepository, glossaryRepository: mockGlossaryRepository); + + Widget loadingWidget() { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: BlocProvider( + create: (context) => loadingCubit, + child: const Scaffold(body: Center(child: CircularProgressIndicator())), + ), + ); + } + + // Act + await tester.pumpWidget(loadingWidget()); + await tester.pump(); + + // Assert - should show loading indicator + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + loadingCubit.close(); + }); + + testWidgets('should display form labels correctly', (WidgetTester tester) async { + // Arrange & Act + await tester.pumpWidget(createTestWidget(InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Название'), findsOneWidget); + expect(find.text('Тип блюда'), findsOneWidget); + expect(find.text('Количество порций'), findsOneWidget); + expect(find.text('Калорийность'), findsOneWidget); + expect(find.text('Общее время приготовления'), findsOneWidget); + expect(find.text('Активное время приготовления'), findsOneWidget); + expect(find.text('Описание'), findsOneWidget); + }); + + testWidgets('should display placeholder text in form fields', (WidgetTester tester) async { + // Arrange & Act + await tester.pumpWidget(createTestWidget(InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Например: тар-тар из говядины'), findsOneWidget); + }); + + testWidgets('should display time input fields with correct labels', (WidgetTester tester) async { + // Arrange & Act + await tester.pumpWidget(createTestWidget(InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit))); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Часы'), findsAtLeastNWidgets(2)); // Total and Active hours + expect(find.text('Минуты'), findsAtLeastNWidgets(2)); // Total and Active minutes + }); + }); +} diff --git a/test/features/create_recipe/widget_test.dart b/test/features/create_recipe/widget_test.dart new file mode 100644 index 00000000..44a902d8 --- /dev/null +++ b/test/features/create_recipe/widget_test.dart @@ -0,0 +1,368 @@ +import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/ingredients_widget.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/widgets/steps_widget.dart'; +import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/glossary/repositories/glossary_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 'widget_test.mocks.dart'; + +@GenerateMocks([RecipeRepository, GlossaryRepository]) +void main() { + group('RecipeCreateScreen Widget Tests', () { + late MockRecipeRepository mockRecipeRepository; + late MockGlossaryRepository mockGlossaryRepository; + late CreateRecipeCubit createRecipeCubit; + + setUp(() { + mockRecipeRepository = MockRecipeRepository(); + mockGlossaryRepository = MockGlossaryRepository(); + createRecipeCubit = CreateRecipeCubit(recipeRepository: mockRecipeRepository, glossaryRepository: mockGlossaryRepository); + }); + + tearDown(() { + createRecipeCubit.close(); + }); + + Widget createTestWidget() { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: BlocProvider(create: (context) => createRecipeCubit, child: const RecipeCreateScreen()), + ); + } + + testWidgets('should display all required form fields', (WidgetTester tester) async { + // Arrange & Act + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + // Assert + expect(find.byKey(AppKeys.recipeCreateScreenTitleField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenTypeDropdown), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenPortionsField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenCaloriesField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenTotalHoursField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenTotalMinutesField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenActiveHoursField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenActiveMinutesField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenDescriptionField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenPublishButton), findsOneWidget); + }); + + testWidgets('should display recipe creation widgets', (WidgetTester tester) async { + // Arrange & Act + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + // Assert + expect(find.byType(InfoAboutRecipeWidget), findsOneWidget); + expect(find.byType(IngredientsWidget), findsOneWidget); + expect(find.byType(StepsWidget), findsOneWidget); + }); + + testWidgets('should update title when user types in title field', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + const testTitle = 'Тестовый рецепт'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenTitleField), testTitle); + await tester.pump(); + + // Assert + expect(find.text(testTitle), findsOneWidget); + }); + + testWidgets('should update portions when user types in portions field', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + const testPortions = '4'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenPortionsField), testPortions); + await tester.pump(); + + // Assert + expect(find.text(testPortions), findsOneWidget); + }); + + testWidgets('should update calories when user types in calories field', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + const testCalories = '250'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenCaloriesField), testCalories); + await tester.pump(); + + // Assert + expect(find.text(testCalories), findsOneWidget); + }); + + testWidgets('should update description when user types in description field', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + const testDescription = 'Это тестовое описание рецепта'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenDescriptionField), testDescription); + await tester.pump(); + + // Assert + expect(find.text(testDescription), findsOneWidget); + }); + + testWidgets('should update cooking time fields', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + const testHours = '1'; + const testMinutes = '30'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenTotalHoursField), testHours); + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenTotalMinutesField), testMinutes); + await tester.pump(); + + // Assert + expect(find.text(testHours), findsOneWidget); + expect(find.text(testMinutes), findsOneWidget); + }); + + testWidgets('should update active cooking time fields', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + const testActiveHours = '0'; + const testActiveMinutes = '45'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenActiveHoursField), testActiveHours); + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenActiveMinutesField), testActiveMinutes); + await tester.pump(); + + // Assert + expect(find.text(testActiveHours), findsOneWidget); + expect(find.text(testActiveMinutes), findsOneWidget); + }); + + testWidgets('should show validation error for empty title', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + // Act - try to publish without title + await tester.tap(find.byKey(AppKeys.recipeCreateScreenPublishButton)); + await tester.pump(); + + // Assert - should show validation error + expect(find.text('Название рецепта обязательно'), findsOneWidget); + }); + + testWidgets('should show validation error for empty description', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + // Fill title but leave description empty + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenTitleField), 'Тестовый рецепт'); + await tester.pump(); + + // Act - try to publish without description + await tester.tap(find.byKey(AppKeys.recipeCreateScreenPublishButton)); + await tester.pump(); + + // Assert - should show validation error + expect(find.text('Описание рецепта обязательно'), findsOneWidget); + }); + + testWidgets('should show validation error for invalid portions', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + // Fill title and description + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenTitleField), 'Тестовый рецепт'); + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenDescriptionField), 'Тестовое описание'); + + // Enter invalid portions (0 or negative) + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenPortionsField), '0'); + await tester.pump(); + + // Act - try to publish with invalid portions + await tester.tap(find.byKey(AppKeys.recipeCreateScreenPublishButton)); + await tester.pump(); + + // Assert - should show validation error + expect(find.text('Количество порций должно быть больше 0'), findsOneWidget); + }); + + testWidgets('should show validation error for invalid calories', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + // Fill required fields + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenTitleField), 'Тестовый рецепт'); + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenDescriptionField), 'Тестовое описание'); + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenPortionsField), '2'); + + // Enter invalid calories (negative) + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenCaloriesField), '-100'); + await tester.pump(); + + // Act - try to publish with invalid calories + await tester.tap(find.byKey(AppKeys.recipeCreateScreenPublishButton)); + await tester.pump(); + + // Assert - should show validation error + expect(find.text('Калорийность не может быть отрицательной'), findsOneWidget); + }); + + testWidgets('should add ingredient when add ingredient button is tapped', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + // Act + await tester.tap(find.byKey(AppKeys.recipeCreateScreenAddIngredientButton)); + await tester.pumpAndSettle(); + + // Assert - should show ingredient modal or form + expect(find.byKey(AppKeys.recipeCreateScreenIngredientNameField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenIngredientQuantityField), findsOneWidget); + expect(find.byKey(AppKeys.recipeCreateScreenIngredientAddButton), findsOneWidget); + }); + + testWidgets('should add step when add step button is tapped', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + // Act + await tester.tap(find.byKey(AppKeys.recipeCreateScreenAddStepButton)); + await tester.pumpAndSettle(); + + // Assert - should show additional step field + expect(find.byKey(AppKeys.recipeCreateScreenStepDescriptionField), findsAtLeastNWidgets(2)); + }); + + testWidgets('should add tag when add tag button is tapped', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + const testTag = 'тестовый тег'; + + // Act + await tester.enterText(find.byKey(AppKeys.recipeCreateScreenTagField), testTag); + await tester.tap(find.byKey(AppKeys.recipeCreateScreenAddTagButton)); + await tester.pumpAndSettle(); + + // Assert - should show the added tag + expect(find.text(testTag), findsOneWidget); + }); + + testWidgets('should display loading state when cubit is loading', (WidgetTester tester) async { + // Arrange + final loadingCubit = CreateRecipeCubit(recipeRepository: mockRecipeRepository, glossaryRepository: mockGlossaryRepository); + + Widget loadingWidget() { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: BlocProvider(create: (context) => loadingCubit, child: const RecipeCreateScreen()), + ); + } + + // Act + await tester.pumpWidget(loadingWidget()); + await tester.pump(); + + // Assert - should show loading indicator + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + loadingCubit.close(); + }); + + testWidgets('should display app bar with correct title for new recipe', (WidgetTester tester) async { + // Arrange & Act + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Добавление рецепта'), findsOneWidget); + }); + + testWidgets('should display app bar with correct title for editing recipe', (WidgetTester tester) async { + // Arrange + final testRecipe = Recipe( + id: 1, + image: '', + state: 'published', + cookingTime: 30, + activeCookingTime: 15, + ingredients: [], + description: 'Тестовое описание', + steps: [], + videoUrl: null, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + title: 'Тестовый рецепт', + type: 'salads', + slug: 'test-recipe', + commentsCount: 0, + wasLikedByMe: false, + isFavorite: false, + calories: 200.0, + servings: 2, + likesCount: 0, + viewsCount: 0, + tags: [], + author: Author( + authorSlug: 'test-author', + authorName: 'Test Author', + authorProfileIcon: '', + authorIsMe: false, + isEntitySubscribedByMe: false, + authorPlaceInRating: null, + ), + ); + + Widget editWidget() { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: BlocProvider( + create: (context) => createRecipeCubit, + child: RecipeCreateScreen(recipe: testRecipe), + ), + ); + } + + // Act + await tester.pumpWidget(editWidget()); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Редактирование рецепта'), findsOneWidget); + }); + }); +}