forked from eatvio/eatvio_app
564 lines
23 KiB
Dart
564 lines
23 KiB
Dart
import 'dart:io';
|
||
|
||
import 'package:eatvio_app/core/app_initializer.dart';
|
||
import 'package:eatvio_app/core/app_config.dart';
|
||
import 'package:eatvio_app/core/locator_service.dart';
|
||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||
import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart';
|
||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:image/image.dart' as img;
|
||
|
||
import '../../fixtures/doubles/create_diet_doubles.dart';
|
||
import '../../fixtures/fixture_reader.dart';
|
||
|
||
void main() {
|
||
group('Diet Creation Basic Widget Tests', () {
|
||
late FixtureCreateDietRepository createDietRepository;
|
||
late FixtureProfileRepository profileRepository;
|
||
late FixtureGlossaryRepository glossaryRepository;
|
||
late CreateDietCubit createDietCubit;
|
||
late Map<String, dynamic> dietFixture;
|
||
|
||
setUp(() async {
|
||
// Initialize AppConfig for tests to enable compression bypass
|
||
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'));
|
||
dietFixture = FixtureReader.readAsMap('create_diet/create_diet_request.json');
|
||
createDietRepository = FixtureCreateDietRepository();
|
||
profileRepository = FixtureProfileRepository();
|
||
glossaryRepository = FixtureGlossaryRepository();
|
||
createDietCubit = CreateDietCubit(
|
||
createDietRepository: createDietRepository,
|
||
profileRepository: profileRepository,
|
||
glossaryRepository: glossaryRepository,
|
||
);
|
||
});
|
||
|
||
tearDown(() {
|
||
createDietCubit.close();
|
||
});
|
||
|
||
Widget createTestWidget(Widget child) {
|
||
return MaterialApp(
|
||
theme: ThemeHelper().themeData(),
|
||
home: BlocProvider<CreateDietCubit>(
|
||
create: (context) => createDietCubit,
|
||
child: Scaffold(body: SingleChildScrollView(child: child)),
|
||
),
|
||
);
|
||
}
|
||
|
||
testWidgets('should create CreateDietCubit successfully', (WidgetTester tester) async {
|
||
// Arrange & Act
|
||
final cubit = CreateDietCubit(
|
||
createDietRepository: createDietRepository,
|
||
profileRepository: profileRepository,
|
||
glossaryRepository: glossaryRepository,
|
||
);
|
||
|
||
// Assert
|
||
expect(cubit, isA<CreateDietCubit>());
|
||
expect(cubit.state.isLoading, isFalse);
|
||
expect(cubit.state.isDietPublishing, isFalse);
|
||
|
||
cubit.close();
|
||
});
|
||
|
||
testWidgets('should display basic diet form fields', (WidgetTester tester) async {
|
||
// Arrange
|
||
Widget formWidget = Column(
|
||
children: [
|
||
TextFormField(
|
||
key: const Key('dietNameField'),
|
||
decoration: const InputDecoration(labelText: 'Название диеты', hintText: 'Например: Здоровая диета'),
|
||
),
|
||
const SizedBox(height: 16),
|
||
TextFormField(
|
||
key: const Key('dietDescriptionField'),
|
||
decoration: const InputDecoration(labelText: 'Описание диеты'),
|
||
maxLines: 3,
|
||
),
|
||
const SizedBox(height: 16),
|
||
DropdownButtonFormField<int>(
|
||
key: const Key('dietDurationField'),
|
||
decoration: const InputDecoration(labelText: 'Продолжительность'),
|
||
items: const [
|
||
DropdownMenuItem(value: 7, child: Text('7 дней')),
|
||
DropdownMenuItem(value: 14, child: Text('14 дней')),
|
||
DropdownMenuItem(value: 21, child: Text('21 день')),
|
||
],
|
||
onChanged: (value) {},
|
||
),
|
||
const SizedBox(height: 16),
|
||
ElevatedButton(key: const Key('publishDietButton'), onPressed: () {}, child: const Text('Опубликовать диету')),
|
||
],
|
||
);
|
||
|
||
// Act
|
||
await tester.pumpWidget(createTestWidget(formWidget));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Assert
|
||
expect(find.byKey(const Key('dietNameField')), findsOneWidget);
|
||
expect(find.byKey(const Key('dietDescriptionField')), findsOneWidget);
|
||
expect(find.byKey(const Key('dietDurationField')), findsOneWidget);
|
||
expect(find.byKey(const Key('publishDietButton')), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('should update diet name when user types in name field', (WidgetTester tester) async {
|
||
// Arrange
|
||
Widget formWidget = TextFormField(
|
||
key: const Key('dietNameField'),
|
||
decoration: const InputDecoration(labelText: 'Название диеты', hintText: 'Например: Здоровая диета'),
|
||
);
|
||
|
||
await tester.pumpWidget(createTestWidget(formWidget));
|
||
await tester.pumpAndSettle();
|
||
|
||
const testName = 'Тестовая диета';
|
||
|
||
// Act
|
||
await tester.enterText(find.byKey(const Key('dietNameField')), testName);
|
||
await tester.pump();
|
||
|
||
// Assert
|
||
expect(find.text(testName), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('should update diet description when user types in description field', (WidgetTester tester) async {
|
||
// Arrange
|
||
Widget formWidget = TextFormField(
|
||
key: const Key('dietDescriptionField'),
|
||
decoration: const InputDecoration(labelText: 'Описание диеты'),
|
||
maxLines: 3,
|
||
);
|
||
|
||
await tester.pumpWidget(createTestWidget(formWidget));
|
||
await tester.pumpAndSettle();
|
||
|
||
const testDescription = 'Это тестовое описание диеты для похудения';
|
||
|
||
// Act
|
||
await tester.enterText(find.byKey(const Key('dietDescriptionField')), testDescription);
|
||
await tester.pump();
|
||
|
||
// Assert
|
||
expect(find.text(testDescription), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('should display diet type selection', (WidgetTester tester) async {
|
||
// Arrange
|
||
Widget typeWidget = Column(
|
||
children: [
|
||
const Text('Тип диеты'),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: CheckboxListTile(key: const Key('veganTypeCheckbox'), title: const Text('Веганская'), value: false, onChanged: (value) {}),
|
||
),
|
||
Expanded(
|
||
child: CheckboxListTile(key: const Key('meatTypeCheckbox'), title: const Text('Мясная'), value: false, onChanged: (value) {}),
|
||
),
|
||
],
|
||
),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: CheckboxListTile(key: const Key('carbTypeCheckbox'), title: const Text('Углеводная'), value: false, onChanged: (value) {}),
|
||
),
|
||
Expanded(
|
||
child: CheckboxListTile(key: const Key('lightTypeCheckbox'), title: const Text('Легкая'), value: false, onChanged: (value) {}),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
|
||
// Act
|
||
await tester.pumpWidget(createTestWidget(typeWidget));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Assert
|
||
expect(find.text('Тип диеты'), findsOneWidget);
|
||
expect(find.byKey(const Key('veganTypeCheckbox')), findsOneWidget);
|
||
expect(find.byKey(const Key('meatTypeCheckbox')), findsOneWidget);
|
||
expect(find.byKey(const Key('carbTypeCheckbox')), findsOneWidget);
|
||
expect(find.byKey(const Key('lightTypeCheckbox')), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('should handle diet type selection', (WidgetTester tester) async {
|
||
// Arrange
|
||
bool veganSelected = false;
|
||
Widget typeWidget = CheckboxListTile(
|
||
key: const Key('veganTypeCheckbox'),
|
||
title: const Text('Веганская'),
|
||
value: veganSelected,
|
||
onChanged: (value) {
|
||
veganSelected = value ?? false;
|
||
},
|
||
);
|
||
|
||
await tester.pumpWidget(createTestWidget(typeWidget));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Act
|
||
await tester.tap(find.byKey(const Key('veganTypeCheckbox')));
|
||
await tester.pump();
|
||
|
||
// Assert
|
||
expect(veganSelected, isTrue);
|
||
});
|
||
|
||
testWidgets('should display diet duration dropdown', (WidgetTester tester) async {
|
||
// Arrange
|
||
Widget durationWidget = DropdownButtonFormField<int>(
|
||
key: const Key('dietDurationField'),
|
||
decoration: const InputDecoration(labelText: 'Продолжительность диеты'),
|
||
items: const [
|
||
DropdownMenuItem(value: 7, child: Text('7 дней')),
|
||
DropdownMenuItem(value: 14, child: Text('14 дней')),
|
||
DropdownMenuItem(value: 21, child: Text('21 день')),
|
||
DropdownMenuItem(value: 30, child: Text('30 дней')),
|
||
],
|
||
onChanged: (value) {},
|
||
);
|
||
|
||
// Act
|
||
await tester.pumpWidget(createTestWidget(durationWidget));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Assert
|
||
expect(find.byKey(const Key('dietDurationField')), findsOneWidget);
|
||
expect(find.text('Продолжительность диеты'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('should display diet status dropdown', (WidgetTester tester) async {
|
||
// Arrange
|
||
Widget statusWidget = DropdownButtonFormField<String>(
|
||
key: const Key('dietStatusField'),
|
||
decoration: const InputDecoration(labelText: 'Статус диеты'),
|
||
items: const [
|
||
DropdownMenuItem(value: 'active', child: Text('Активная')),
|
||
DropdownMenuItem(value: 'draft', child: Text('Черновик')),
|
||
DropdownMenuItem(value: 'personal', child: Text('Личная')),
|
||
],
|
||
onChanged: (value) {},
|
||
);
|
||
|
||
// Act
|
||
await tester.pumpWidget(createTestWidget(statusWidget));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Assert
|
||
expect(find.byKey(const Key('dietStatusField')), findsOneWidget);
|
||
expect(find.text('Статус диеты'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('should display form labels correctly', (WidgetTester tester) async {
|
||
// Arrange
|
||
Widget formWidget = const Column(
|
||
children: [Text('Название диеты'), 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);
|
||
expect(find.text('Статус'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('should display placeholder text in name field', (WidgetTester tester) async {
|
||
// Arrange
|
||
Widget formWidget = TextFormField(
|
||
key: const Key('dietNameField'),
|
||
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 buttonWidget = ElevatedButton(key: const Key('publishDietButton'), onPressed: () {}, child: const Text('Опубликовать диету'));
|
||
|
||
// Act
|
||
await tester.pumpWidget(createTestWidget(buttonWidget));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Assert
|
||
expect(find.byKey(const Key('publishDietButton')), findsOneWidget);
|
||
expect(find.text('Опубликовать диету'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('should handle publish button tap', (WidgetTester tester) async {
|
||
// Arrange
|
||
bool buttonPressed = false;
|
||
Widget buttonWidget = ElevatedButton(
|
||
key: const Key('publishDietButton'),
|
||
onPressed: () {
|
||
buttonPressed = true;
|
||
},
|
||
child: const Text('Опубликовать диету'),
|
||
);
|
||
|
||
await tester.pumpWidget(createTestWidget(buttonWidget));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Act
|
||
await tester.tap(find.byKey(const Key('publishDietButton')));
|
||
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: const Key('dietNameField'),
|
||
decoration: const InputDecoration(labelText: 'Название диеты', errorText: 'Название диеты обязательно'),
|
||
),
|
||
const SizedBox(height: 16),
|
||
TextFormField(
|
||
key: const Key('dietDescriptionField'),
|
||
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 complete diet creation form', (WidgetTester tester) async {
|
||
// Arrange
|
||
Widget completeForm = Column(
|
||
children: [
|
||
TextFormField(
|
||
key: const Key('dietNameField'),
|
||
decoration: const InputDecoration(labelText: 'Название диеты', hintText: 'Например: Здоровая диета'),
|
||
),
|
||
const SizedBox(height: 16),
|
||
TextFormField(
|
||
key: const Key('dietDescriptionField'),
|
||
decoration: const InputDecoration(labelText: 'Описание диеты'),
|
||
maxLines: 3,
|
||
),
|
||
const SizedBox(height: 16),
|
||
DropdownButtonFormField<int>(
|
||
key: const Key('dietDurationField'),
|
||
decoration: const InputDecoration(labelText: 'Продолжительность'),
|
||
items: const [
|
||
DropdownMenuItem(value: 7, child: Text('7 дней')),
|
||
DropdownMenuItem(value: 14, child: Text('14 дней')),
|
||
DropdownMenuItem(value: 21, child: Text('21 день')),
|
||
],
|
||
onChanged: (value) {},
|
||
),
|
||
const SizedBox(height: 16),
|
||
DropdownButtonFormField<String>(
|
||
key: const Key('dietStatusField'),
|
||
decoration: const InputDecoration(labelText: 'Статус диеты'),
|
||
items: const [
|
||
DropdownMenuItem(value: 'active', child: Text('Активная')),
|
||
DropdownMenuItem(value: 'draft', child: Text('Черновик')),
|
||
],
|
||
onChanged: (value) {},
|
||
),
|
||
const SizedBox(height: 32),
|
||
ElevatedButton(key: const Key('publishDietButton'), onPressed: () {}, child: const Text('Опубликовать диету')),
|
||
],
|
||
);
|
||
|
||
// Act
|
||
await tester.pumpWidget(createTestWidget(completeForm));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Assert
|
||
expect(find.byKey(const Key('dietNameField')), findsOneWidget);
|
||
expect(find.byKey(const Key('dietDescriptionField')), findsOneWidget);
|
||
expect(find.byKey(const Key('dietDurationField')), findsOneWidget);
|
||
expect(find.byKey(const Key('dietStatusField')), findsOneWidget);
|
||
expect(find.byKey(const Key('publishDietButton')), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('should display diet type options', (WidgetTester tester) async {
|
||
// Arrange
|
||
Widget typeOptionsWidget = const Column(
|
||
children: [
|
||
Text('Выберите тип диеты:'),
|
||
SizedBox(height: 8),
|
||
Text('• Веганская - без продуктов животного происхождения'),
|
||
Text('• Мясная - с мясными продуктами'),
|
||
Text('• Углеводная - с высоким содержанием углеводов'),
|
||
Text('• Легкая - низкокалорийная диета'),
|
||
],
|
||
);
|
||
|
||
// Act
|
||
await tester.pumpWidget(createTestWidget(typeOptionsWidget));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Assert
|
||
expect(find.text('Выберите тип диеты:'), findsOneWidget);
|
||
expect(find.text('• Веганская - без продуктов животного происхождения'), findsOneWidget);
|
||
expect(find.text('• Мясная - с мясными продуктами'), findsOneWidget);
|
||
expect(find.text('• Углеводная - с высоким содержанием углеводов'), findsOneWidget);
|
||
expect(find.text('• Легкая - низкокалорийная диета'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('should display diet duration options', (WidgetTester tester) async {
|
||
// Arrange
|
||
Widget durationOptionsWidget = const Column(
|
||
children: [
|
||
Text('Доступные продолжительности:'),
|
||
SizedBox(height: 8),
|
||
Text('• 7 дней - недельная диета'),
|
||
Text('• 14 дней - двухнедельная диета'),
|
||
Text('• 21 день - трехнедельная диета'),
|
||
Text('• 30 дней - месячная диета'),
|
||
],
|
||
);
|
||
|
||
// Act
|
||
await tester.pumpWidget(createTestWidget(durationOptionsWidget));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Assert
|
||
expect(find.text('Доступные продолжительности:'), findsOneWidget);
|
||
expect(find.text('• 7 дней - недельная диета'), findsOneWidget);
|
||
expect(find.text('• 14 дней - двухнедельная диета'), findsOneWidget);
|
||
expect(find.text('• 21 день - трехнедельная диета'), findsOneWidget);
|
||
expect(find.text('• 30 дней - месячная диета'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('should send correct request when creating diet', (WidgetTester tester) async {
|
||
// Arrange
|
||
// Create a valid JPEG file using image package (100x100 pixel image)
|
||
final testImage = img.Image(width: 100, height: 100);
|
||
img.fill(testImage, color: img.ColorRgb8(128, 128, 128));
|
||
final jpegBytes = img.encodeJpg(testImage, quality: 85);
|
||
final tempFile = File('${Directory.systemTemp.path}/test_image_${DateTime.now().millisecondsSinceEpoch}.jpg');
|
||
await tempFile.writeAsBytes(jpegBytes);
|
||
final xFile = XFile(tempFile.path);
|
||
|
||
createDietCubit.setName(dietFixture['name'] as String);
|
||
createDietCubit.setDetails(dietFixture['details'] as String);
|
||
createDietCubit.setDuration(0); // 7 days - this creates dailySchedule with 7 days
|
||
createDietCubit.setDietState(DietState.draft);
|
||
|
||
final types = dietFixture['types'] as Map<String, dynamic>;
|
||
createDietCubit.setDietType(key: 'meat', value: types['meat'] as bool);
|
||
createDietCubit.setDietType(key: 'carb', value: types['carb'] as bool);
|
||
createDietCubit.setDietType(key: 'veg', value: types['veg'] as bool);
|
||
createDietCubit.setDietType(key: 'light', value: types['light'] as bool);
|
||
|
||
// Add at least one product to each day to pass validation
|
||
final glossaryResults = await glossaryRepository.getGlossaryListByQuery('test');
|
||
if (glossaryResults.isNotEmpty) {
|
||
final product = glossaryResults.first;
|
||
final dailySchedule = DailySchedule(
|
||
name: product.name,
|
||
portion: Portion.grams,
|
||
quantity: product.grInPortion,
|
||
productNutritionalValueId: product.id.toString(),
|
||
);
|
||
// Add product to first meal of first day for all 7 days
|
||
for (int day = 0; day < 7; day++) {
|
||
createDietCubit.addPortion(dayIndex: day, mealIndex: 0, portion: dailySchedule);
|
||
}
|
||
}
|
||
|
||
createDietCubit.setImage(xFile, imageUrl: null);
|
||
|
||
// Act
|
||
final result = await createDietCubit.createDiet().timeout(
|
||
const Duration(seconds: 10),
|
||
);
|
||
await tester.pump(); // Allow state changes to propagate
|
||
await tester.pump(); // Additional pump for any delayed updates
|
||
|
||
// Assert
|
||
expect(result, isNull); // Should return null on success
|
||
expect(createDietRepository.createRequests.length, 1);
|
||
final request = createDietRepository.createRequests.single;
|
||
expect(request['name'], dietFixture['name']);
|
||
expect(request['details'], dietFixture['details']);
|
||
expect(request['duration'], dietFixture['duration']);
|
||
expect(request['state'], dietFixture['state']);
|
||
expect(request['types'], dietFixture['types']);
|
||
|
||
// Cleanup - remove original and compressed files
|
||
if (await tempFile.exists()) {
|
||
await tempFile.delete();
|
||
}
|
||
// ImageCompressUtility creates compressed file with _compressed suffix
|
||
final filePathWithoutExtension = tempFile.path.replaceAll(RegExp(r'\.(jpg|jpeg)$', caseSensitive: false), '');
|
||
final compressedFile = File('${filePathWithoutExtension}_compressed.jpg');
|
||
if (await compressedFile.exists()) {
|
||
await compressedFile.delete();
|
||
}
|
||
});
|
||
});
|
||
}
|