forked from eatvio/eatvio_app
added widget and unit tests for plans, diets, recipes
This commit is contained in:
3
Makefile
3
Makefile
@@ -8,3 +8,6 @@ run-checks: run-analyse run-tests
|
|||||||
|
|
||||||
build-generated-files:
|
build-generated-files:
|
||||||
dart run build_runner build --delete-conflicting-outputs
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
clean-coverage:
|
||||||
|
@./scripts/remove_generated_from_coverage.sh
|
||||||
73
scripts/remove_generated_from_coverage.sh
Executable file
73
scripts/remove_generated_from_coverage.sh
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to remove generated files from coverage/lcov.info
|
||||||
|
# Generated files include: .g.dart, .freezed.dart, .mocks.dart
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/remove_generated_from_coverage.sh
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# 1. Creates a backup of the original coverage file
|
||||||
|
# 2. Removes all entries for generated files (.g.dart, .freezed.dart, .mocks.dart)
|
||||||
|
# 3. Saves the cleaned file back to coverage/lcov.info
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
COVERAGE_FILE="coverage/lcov.info"
|
||||||
|
BACKUP_FILE="coverage/lcov.info.backup"
|
||||||
|
|
||||||
|
# Get the script directory and project root
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
# Check if coverage file exists
|
||||||
|
if [ ! -f "$COVERAGE_FILE" ]; then
|
||||||
|
echo "Error: $COVERAGE_FILE not found"
|
||||||
|
echo "Please run 'flutter test --coverage' first to generate the coverage file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create backup
|
||||||
|
cp "$COVERAGE_FILE" "$BACKUP_FILE"
|
||||||
|
echo "Backup created: $BACKUP_FILE"
|
||||||
|
|
||||||
|
# Remove entries for generated files
|
||||||
|
# This uses awk to process the lcov.info file
|
||||||
|
# It skips entries that start with SF: and contain .g.dart, .freezed.dart, or .mocks.dart
|
||||||
|
|
||||||
|
awk '
|
||||||
|
BEGIN {
|
||||||
|
skip_entry = 0
|
||||||
|
}
|
||||||
|
/^SF:/ {
|
||||||
|
# Check if this is a generated file
|
||||||
|
# Matches: .g.dart, .freezed.dart, .mocks.dart
|
||||||
|
if ($0 ~ /\.(g|freezed|mocks)\.dart$/) {
|
||||||
|
skip_entry = 1
|
||||||
|
} else {
|
||||||
|
skip_entry = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
if (!skip_entry) {
|
||||||
|
print
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/^end_of_record$/ {
|
||||||
|
skip_entry = 0
|
||||||
|
}
|
||||||
|
' "$BACKUP_FILE" > "$COVERAGE_FILE.tmp"
|
||||||
|
|
||||||
|
# Replace original file with cleaned version
|
||||||
|
mv "$COVERAGE_FILE.tmp" "$COVERAGE_FILE"
|
||||||
|
|
||||||
|
# Count removed entries
|
||||||
|
REMOVED=$(grep -c "^SF:.*\.\(g\|freezed\|mocks\)\.dart$" "$BACKUP_FILE" 2>/dev/null || echo "0")
|
||||||
|
echo "Removed $REMOVED generated file entries from coverage/lcov.info"
|
||||||
|
echo "Cleaned file saved to: $COVERAGE_FILE"
|
||||||
|
echo ""
|
||||||
|
echo "To restore the original file, run:"
|
||||||
|
echo " cp $BACKUP_FILE $COVERAGE_FILE"
|
||||||
|
|
||||||
194
test/features/diets/presentation/cubit/diets_cubit_test.dart
Normal file
194
test/features/diets/presentation/cubit/diets_cubit_test.dart
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import 'package:easy_debounce/easy_debounce.dart';
|
||||||
|
import 'package:eatvio_app/core/models/pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/diets/models/diet_likes_model.dart';
|
||||||
|
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'diets_cubit_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<FeedRepository>(), MockSpec<DietRepository>()])
|
||||||
|
void main() {
|
||||||
|
group('DietsCubit', () {
|
||||||
|
DietsCubit? dietsCubit;
|
||||||
|
late MockFeedRepository mockFeedRepository;
|
||||||
|
late MockDietRepository mockDietRepository;
|
||||||
|
|
||||||
|
const testDiet = Diet(
|
||||||
|
id: 1,
|
||||||
|
slug: 'test-diet',
|
||||||
|
name: 'Test Diet',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: DietState.active,
|
||||||
|
type: DietType(light: false, veg: false, carb: false, meat: false),
|
||||||
|
details: 'Test diet description',
|
||||||
|
dailySchedule: {},
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
dailyNutrition: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const testDietsPagination = DietsPagination(
|
||||||
|
data: [testDiet],
|
||||||
|
links: Links(first: '', last: '', prev: null, next: null),
|
||||||
|
meta: Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
|
||||||
|
);
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockFeedRepository = MockFeedRepository();
|
||||||
|
mockDietRepository = MockDietRepository();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
EasyDebounce.cancel('loadDiets');
|
||||||
|
EasyDebounce.cancel('like_diet');
|
||||||
|
EasyDebounce.cancel('add_or_remove_diet_from_favorite');
|
||||||
|
if (dietsCubit != null && !dietsCubit!.isClosed) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
dietsCubit!.close();
|
||||||
|
dietsCubit = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('начальное состояние - initial state', () {
|
||||||
|
dietsCubit = DietsCubit(feedRepository: mockFeedRepository, dietsRepository: mockDietRepository);
|
||||||
|
expect(dietsCubit!.state.dietsList, isEmpty);
|
||||||
|
expect(dietsCubit!.state.isLoading, true);
|
||||||
|
expect(dietsCubit!.state.errorMessage, null);
|
||||||
|
expect(dietsCubit!.state.filterState.durationIndex, 0);
|
||||||
|
expect(dietsCubit!.state.filterState.sortingIndex, 0);
|
||||||
|
expect(dietsCubit!.state.filterState.typeIndex, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadDiets - загружает диеты успешно', () async {
|
||||||
|
dietsCubit = DietsCubit(feedRepository: mockFeedRepository, dietsRepository: mockDietRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getDiets(query: null, types: null, duration: null, sorting: null)).thenAnswer((_) async => testDietsPagination);
|
||||||
|
|
||||||
|
await dietsCubit!.loadDiets();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(dietsCubit!.state.dietsList.length, 1);
|
||||||
|
expect(dietsCubit!.state.dietsList.first.name, 'Test Diet');
|
||||||
|
expect(dietsCubit!.state.errorMessage, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadDiets - обрабатывает ошибку', () async {
|
||||||
|
dietsCubit = DietsCubit(feedRepository: mockFeedRepository, dietsRepository: mockDietRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getDiets(query: null, types: null, duration: null, sorting: null)).thenThrow(Exception('Network error'));
|
||||||
|
|
||||||
|
await dietsCubit!.loadDiets();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(dietsCubit!.state.errorMessage, isNotNull);
|
||||||
|
expect(dietsCubit!.state.isLoading, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changeTypeFilter - меняет фильтр типа', () async {
|
||||||
|
dietsCubit = DietsCubit(feedRepository: mockFeedRepository, dietsRepository: mockDietRepository);
|
||||||
|
|
||||||
|
dietsCubit!.changeTypeFilter(1);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(dietsCubit!.state.filterState.typeIndex, 1);
|
||||||
|
|
||||||
|
// Wait for debounce to complete
|
||||||
|
await Future.delayed(const Duration(milliseconds: 1100));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changeDurationFilter - меняет фильтр длительности', () async {
|
||||||
|
dietsCubit = DietsCubit(feedRepository: mockFeedRepository, dietsRepository: mockDietRepository);
|
||||||
|
|
||||||
|
dietsCubit!.changeDurationFilter(1);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(dietsCubit!.state.filterState.durationIndex, 1);
|
||||||
|
|
||||||
|
// Wait for debounce to complete
|
||||||
|
await Future.delayed(const Duration(milliseconds: 1100));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changeSortingType - меняет тип сортировки', () async {
|
||||||
|
dietsCubit = DietsCubit(feedRepository: mockFeedRepository, dietsRepository: mockDietRepository);
|
||||||
|
|
||||||
|
dietsCubit!.changeSortingType(1);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(dietsCubit!.state.filterState.sortingIndex, 1);
|
||||||
|
|
||||||
|
// Wait for debounce to complete
|
||||||
|
await Future.delayed(const Duration(milliseconds: 1100));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likeDiet - лайкает диету', () async {
|
||||||
|
dietsCubit = DietsCubit(feedRepository: mockFeedRepository, dietsRepository: mockDietRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getDiets(query: null, types: null, duration: null, sorting: null)).thenAnswer((_) async => testDietsPagination);
|
||||||
|
|
||||||
|
await dietsCubit!.loadDiets();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
when(mockDietRepository.likeDiet(1)).thenAnswer((_) async => const DietLikesModel(totalLikes: 2, isLiked: true));
|
||||||
|
|
||||||
|
dietsCubit!.likeDiet(1, 0);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(dietsCubit!.state.dietsList[0].wasLikedByMe, true);
|
||||||
|
expect(dietsCubit!.state.dietsList[0].totalLikes, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addDietToFavorite - добавляет диету в избранное', () async {
|
||||||
|
dietsCubit = DietsCubit(feedRepository: mockFeedRepository, dietsRepository: mockDietRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getDiets(query: null, types: null, duration: null, sorting: null)).thenAnswer((_) async => testDietsPagination);
|
||||||
|
|
||||||
|
await dietsCubit!.loadDiets();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
when(mockDietRepository.addToFavorite(1)).thenAnswer((_) async => testDiet.copyWith(isFavorite: true));
|
||||||
|
|
||||||
|
dietsCubit!.addDietToFavorite(1, 0);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(dietsCubit!.state.dietsList[0].isFavorite, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeDietFromFavorite - удаляет диету из избранного', () async {
|
||||||
|
dietsCubit = DietsCubit(feedRepository: mockFeedRepository, dietsRepository: mockDietRepository);
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockFeedRepository.getDiets(query: null, types: null, duration: null, sorting: null),
|
||||||
|
).thenAnswer((_) async => testDietsPagination.copyWith(data: [testDiet.copyWith(isFavorite: true)]));
|
||||||
|
|
||||||
|
await dietsCubit!.loadDiets();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
when(mockDietRepository.removeFromFavorite(1)).thenAnswer((_) async => testDiet.copyWith(isFavorite: false));
|
||||||
|
|
||||||
|
dietsCubit!.removeDietFromFavorite(1, 0);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(dietsCubit!.state.dietsList[0].isFavorite, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteDiet - удаляет диету', () async {
|
||||||
|
dietsCubit = DietsCubit(feedRepository: mockFeedRepository, dietsRepository: mockDietRepository);
|
||||||
|
|
||||||
|
when(mockDietRepository.deleteDiet(1)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
final result = await dietsCubit!.deleteDiet(1);
|
||||||
|
|
||||||
|
expect(result, true);
|
||||||
|
verify(mockDietRepository.deleteDiet(1)).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
244
test/features/diets/presentation/widgets/basic_widget_test.dart
Normal file
244
test/features/diets/presentation/widgets/basic_widget_test.dart
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
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/diets/repositories/diet_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/widgets/diets.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/widgets/diets_widget.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/repositories/feed_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_portal/flutter_portal.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
import 'package:network_image_mock/network_image_mock.dart';
|
||||||
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
|
|
||||||
|
import 'basic_widget_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<FeedRepository>(), MockSpec<DietRepository>()])
|
||||||
|
void main() {
|
||||||
|
group('Diets Basic Widget Tests', () {
|
||||||
|
late MockFeedRepository mockFeedRepository;
|
||||||
|
late MockDietRepository mockDietRepository;
|
||||||
|
late DietsCubit dietsCubit;
|
||||||
|
|
||||||
|
const testDiet = Diet(
|
||||||
|
id: 1,
|
||||||
|
slug: 'test-diet',
|
||||||
|
name: 'Test Diet',
|
||||||
|
image: 'test-image.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: DietState.active,
|
||||||
|
type: DietType(light: false, veg: false, carb: false, meat: false),
|
||||||
|
details: 'Test diet description',
|
||||||
|
dailySchedule: {},
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
dailyNutrition: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await sl.reset();
|
||||||
|
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
||||||
|
mockFeedRepository = MockFeedRepository();
|
||||||
|
mockDietRepository = MockDietRepository();
|
||||||
|
dietsCubit = DietsCubit(feedRepository: mockFeedRepository, dietsRepository: mockDietRepository);
|
||||||
|
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
||||||
|
VisibilityDetectorController.instance.updateInterval = Duration.zero;
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
if (!dietsCubit.isClosed) {
|
||||||
|
dietsCubit.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget createTestWidget(Widget child, {bool wrapInScaffold = true}) {
|
||||||
|
final widget = BlocProvider<DietsCubit>(
|
||||||
|
create: (context) => dietsCubit,
|
||||||
|
child: Portal(
|
||||||
|
child: wrapInScaffold
|
||||||
|
? Scaffold(body: SingleChildScrollView(child: child))
|
||||||
|
: SizedBox(width: 800, height: 1200, child: SingleChildScrollView(child: child)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeHelper().themeData(),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [Locale('ru')],
|
||||||
|
home: widget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget createDietsTestWidget(Widget child) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeHelper().themeData(),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [Locale('ru')],
|
||||||
|
home: Portal(
|
||||||
|
child: Scaffold(
|
||||||
|
body: BlocProvider<DietsCubit>(
|
||||||
|
create: (context) => dietsCubit,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) => SizedBox(
|
||||||
|
width: constraints.maxWidth > 0 ? constraints.maxWidth : 800,
|
||||||
|
height: constraints.maxHeight > 0 ? constraints.maxHeight : 600,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('should create DietsCubit successfully', (WidgetTester tester) async {
|
||||||
|
// Arrange & Act
|
||||||
|
final cubit = DietsCubit(feedRepository: mockFeedRepository, dietsRepository: mockDietRepository);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(cubit, isA<DietsCubit>());
|
||||||
|
expect(cubit.state.dietsList, isEmpty);
|
||||||
|
|
||||||
|
cubit.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display DietsWidget with diet name', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
dietsCubit.emit(DietsState.initial().copyWith(dietsList: [testDiet], isLoading: false));
|
||||||
|
const widget = DietsWidget(diet: testDiet, index: 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.text('Test Diet'), findsOneWidget);
|
||||||
|
expect(find.byType(DietsWidget), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display DietsWidget with description when details are present', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
dietsCubit.emit(DietsState.initial().copyWith(dietsList: [testDiet], isLoading: false));
|
||||||
|
const widget = DietsWidget(diet: testDiet, index: 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.text('Test Diet'), findsOneWidget);
|
||||||
|
expect(find.byType(DietsWidget), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display DietsWidget structure correctly', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
dietsCubit.emit(DietsState.initial().copyWith(dietsList: [testDiet], isLoading: false));
|
||||||
|
const widget = DietsWidget(diet: testDiet, index: 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(DietsWidget), findsOneWidget);
|
||||||
|
expect(find.text('Test Diet'), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display Diets widget with loading state', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
dietsCubit.emit(DietsState.initial());
|
||||||
|
final widget = Diets(handleVisibility: ({required visibleFraction, required id, required type}) {});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createDietsTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(Diets), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display Diets widget with loaded state', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
dietsCubit.emit(DietsState.initial().copyWith(dietsList: [testDiet], isLoading: false));
|
||||||
|
final widget = Diets(handleVisibility: ({required visibleFraction, required id, required type}) {});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createDietsTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(Diets), findsOneWidget);
|
||||||
|
expect(find.byType(RefreshIndicator), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display Diets widget with error state', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
dietsCubit.emit(DietsState.initial().copyWith(isLoading: false, errorMessage: 'Error message'));
|
||||||
|
final widget = Diets(handleVisibility: ({required visibleFraction, required id, required type}) {});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createDietsTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(Diets), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display Diets widget with multiple diets', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
final testDiet2 = testDiet.copyWith(id: 2, name: 'Test Diet 2');
|
||||||
|
dietsCubit.emit(DietsState.initial().copyWith(dietsList: [testDiet, testDiet2], isLoading: false));
|
||||||
|
final widget = Diets(handleVisibility: ({required visibleFraction, required id, required type}) {});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createDietsTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(Diets), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
242
test/features/diets/presentation/widgets/widgets_test.dart
Normal file
242
test/features/diets/presentation/widgets/widgets_test.dart
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
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/diets/presentation/widgets/author_card.dart';
|
||||||
|
import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart';
|
||||||
|
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
|
||||||
|
import 'package:eatvio_app/features/diets/presentation/widgets/type_badges.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_portal/flutter_portal.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
import 'package:network_image_mock/network_image_mock.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Diets Widgets Tests', () {
|
||||||
|
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(Widget child) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeHelper().themeData(),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [Locale('ru')],
|
||||||
|
home: Portal(
|
||||||
|
child: Scaffold(body: SingleChildScrollView(child: child)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('InfoBouble', () {
|
||||||
|
testWidgets('should display InfoBouble with children', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const widget = InfoBouble(children: [Text('Test Text')]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(InfoBouble), findsOneWidget);
|
||||||
|
expect(find.text('Test Text'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display InfoBouble with multiple children', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const widget = InfoBouble(children: [Text('First'), Text('Second')]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(InfoBouble), findsOneWidget);
|
||||||
|
expect(find.text('First'), findsOneWidget);
|
||||||
|
expect(find.text('Second'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display InfoBouble with custom radius', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const widget = InfoBouble(radius: 20, children: [Text('Test')]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(InfoBouble), findsOneWidget);
|
||||||
|
expect(find.text('Test'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('ExpandableText', () {
|
||||||
|
testWidgets('should display ExpandableText with short text', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const widget = ExpandableText(text: 'Short text', maxLines: 5);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(ExpandableText), findsOneWidget);
|
||||||
|
expect(find.text('Short text'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display ExpandableText with long text', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const longText =
|
||||||
|
'This is a very long text that should be truncated and show an expand button when it exceeds the maximum number of lines specified in the widget. This text should be long enough to trigger the expand functionality.';
|
||||||
|
const widget = ExpandableText(text: longText, maxLines: 3);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(ExpandableText), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should toggle expansion when expand button is tapped', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const longText =
|
||||||
|
'This is a very long text that should be truncated and show an expand button when it exceeds the maximum number of lines specified in the widget. This text should be long enough to trigger the expand functionality.';
|
||||||
|
const widget = ExpandableText(text: longText, maxLines: 3);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
|
||||||
|
// Try to find and tap expand button if it exists
|
||||||
|
final expandButtons = find.text('Развернуть');
|
||||||
|
if (expandButtons.evaluate().isNotEmpty) {
|
||||||
|
await tester.tap(expandButtons);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(ExpandableText), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AuthorCard', () {
|
||||||
|
testWidgets('should display AuthorCard with author information', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
const author = Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false);
|
||||||
|
const widget = AuthorCard(author: author, title: 'Автор');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(AuthorCard), findsOneWidget);
|
||||||
|
expect(find.text('Test Author'), findsOneWidget);
|
||||||
|
expect(find.text('Автор'), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display AuthorCard with rating when available', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
const author = Author(
|
||||||
|
authorSlug: 'test-author',
|
||||||
|
authorName: 'Test Author',
|
||||||
|
authorProfileIcon: 'https://example.com/icon.jpg',
|
||||||
|
authorIsMe: false,
|
||||||
|
authorPlaceInRating: 5,
|
||||||
|
);
|
||||||
|
const widget = AuthorCard(author: author, title: 'Автор');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(AuthorCard), findsOneWidget);
|
||||||
|
expect(find.text('Test Author'), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('TypeBadges', () {
|
||||||
|
testWidgets('should display TypeBadges with diet type', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const dietType = DietType(light: true, veg: false, carb: false, meat: false);
|
||||||
|
const widget = TypeBadges(type: dietType, disableDescriptionOfTypes: false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(TypeBadges), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display TypeBadges with plan type', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const planType = PlanType(fit: true, slim: false, behavior: false, wellness: false);
|
||||||
|
const widget = TypeBadges(planType: planType, disableDescriptionOfTypes: false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(TypeBadges), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display TypeBadges with multiple types', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const dietType = DietType(light: true, veg: true, carb: false, meat: false);
|
||||||
|
const widget = TypeBadges(type: dietType, disableDescriptionOfTypes: false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(TypeBadges), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should not show overlay when disableDescriptionOfTypes is true', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const dietType = DietType(light: true, veg: false, carb: false, meat: false);
|
||||||
|
const widget = TypeBadges(type: dietType, disableDescriptionOfTypes: true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(TypeBadges), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
207
test/features/diets/repositories/diet_repository_test.dart
Normal file
207
test/features/diets/repositories/diet_repository_test.dart
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import 'package:eatvio_app/features/diets/models/diet_likes_model.dart';
|
||||||
|
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/diets/services/diet_service.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
|
||||||
|
import 'diet_repository_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<DietService>()])
|
||||||
|
void main() {
|
||||||
|
group('DietRepository', () {
|
||||||
|
late DietRepository dietRepository;
|
||||||
|
late MockDietService mockDietService;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockDietService = MockDietService();
|
||||||
|
dietRepository = DietRepository(dietsService: mockDietService);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likeDiet returns DietLikesModel', () async {
|
||||||
|
const id = 1;
|
||||||
|
const expected = DietLikesModel(totalLikes: 10, isLiked: true);
|
||||||
|
|
||||||
|
when(mockDietService.likeDiet(id: id)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await dietRepository.likeDiet(id);
|
||||||
|
|
||||||
|
expect(result, isA<DietLikesModel>());
|
||||||
|
expect(result.totalLikes, 10);
|
||||||
|
expect(result.isLiked, true);
|
||||||
|
verify(mockDietService.likeDiet(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getDiet returns Diet by id', () async {
|
||||||
|
const id = 1;
|
||||||
|
const expected = Diet(
|
||||||
|
id: id,
|
||||||
|
slug: 'test-diet',
|
||||||
|
name: 'Test Diet',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: DietState.active,
|
||||||
|
type: DietType(light: true, veg: false, carb: false, meat: false),
|
||||||
|
details: 'Test diet description',
|
||||||
|
dailySchedule: {},
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
dailyNutrition: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockDietService.getDiet(id: id)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await dietRepository.getDiet(id);
|
||||||
|
|
||||||
|
expect(result, isA<Diet>());
|
||||||
|
expect(result.id, id);
|
||||||
|
expect(result.name, 'Test Diet');
|
||||||
|
verify(mockDietService.getDiet(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getDietBySlug returns Diet by slug', () async {
|
||||||
|
const slug = 'test-diet-slug';
|
||||||
|
const expected = Diet(
|
||||||
|
id: 1,
|
||||||
|
slug: slug,
|
||||||
|
name: 'Test Diet',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: DietState.active,
|
||||||
|
type: DietType(light: true, veg: false, carb: false, meat: false),
|
||||||
|
details: 'Test diet description',
|
||||||
|
dailySchedule: {},
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
dailyNutrition: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockDietService.getDietBySlug(slug: slug)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await dietRepository.getDietBySlug(slug);
|
||||||
|
|
||||||
|
expect(result, isA<Diet>());
|
||||||
|
expect(result.slug, slug);
|
||||||
|
verify(mockDietService.getDietBySlug(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addToFavorite returns Diet', () async {
|
||||||
|
const id = 1;
|
||||||
|
const expected = Diet(
|
||||||
|
id: id,
|
||||||
|
slug: 'test-diet',
|
||||||
|
name: 'Test Diet',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: DietState.active,
|
||||||
|
type: DietType(light: true, veg: false, carb: false, meat: false),
|
||||||
|
details: 'Test diet description',
|
||||||
|
dailySchedule: {},
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: true,
|
||||||
|
dailyNutrition: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockDietService.addToFavorite(id: id)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await dietRepository.addToFavorite(id);
|
||||||
|
|
||||||
|
expect(result, isA<Diet>());
|
||||||
|
expect(result.isFavorite, true);
|
||||||
|
verify(mockDietService.addToFavorite(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeFromFavorite returns Diet', () async {
|
||||||
|
const id = 1;
|
||||||
|
const expected = Diet(
|
||||||
|
id: id,
|
||||||
|
slug: 'test-diet',
|
||||||
|
name: 'Test Diet',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: DietState.active,
|
||||||
|
type: DietType(light: true, veg: false, carb: false, meat: false),
|
||||||
|
details: 'Test diet description',
|
||||||
|
dailySchedule: {},
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
dailyNutrition: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockDietService.removeFromFavorite(id: id)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await dietRepository.removeFromFavorite(id);
|
||||||
|
|
||||||
|
expect(result, isA<Diet>());
|
||||||
|
expect(result.isFavorite, false);
|
||||||
|
verify(mockDietService.removeFromFavorite(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('startDiet starts diet with date', () async {
|
||||||
|
const id = 1;
|
||||||
|
const date = '2024-01-01';
|
||||||
|
|
||||||
|
when(mockDietService.startDiet(id: id, body: {'date': date})).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await dietRepository.startDiet(id, date);
|
||||||
|
|
||||||
|
verify(mockDietService.startDiet(id: id, body: {'date': date})).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cancelDiet cancels diet by id', () async {
|
||||||
|
const id = 1;
|
||||||
|
|
||||||
|
when(mockDietService.cancelDiet(id: id)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await dietRepository.cancelDiet(id);
|
||||||
|
|
||||||
|
verify(mockDietService.cancelDiet(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteDiet deletes diet by id', () async {
|
||||||
|
const id = 1;
|
||||||
|
|
||||||
|
when(mockDietService.deleteDiet(id: id)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await dietRepository.deleteDiet(id);
|
||||||
|
|
||||||
|
verify(mockDietService.deleteDiet(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likeDiet rethrows exception on error', () async {
|
||||||
|
const id = 1;
|
||||||
|
|
||||||
|
when(mockDietService.likeDiet(id: id)).thenThrow(Exception('Network error'));
|
||||||
|
|
||||||
|
expect(() => dietRepository.likeDiet(id), throwsException);
|
||||||
|
verify(mockDietService.likeDiet(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getDiet rethrows exception on error', () async {
|
||||||
|
const id = 1;
|
||||||
|
|
||||||
|
when(mockDietService.getDiet(id: id)).thenThrow(Exception('Network error'));
|
||||||
|
|
||||||
|
expect(() => dietRepository.getDiet(id), throwsException);
|
||||||
|
verify(mockDietService.getDiet(id: id)).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
203
test/features/diets/services/diet_service_test.dart
Normal file
203
test/features/diets/services/diet_service_test.dart
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import 'package:eatvio_app/features/diets/models/diet_likes_model.dart';
|
||||||
|
import 'package:eatvio_app/features/diets/services/diet_service.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
|
||||||
|
import 'diet_service_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<DietService>()])
|
||||||
|
void main() {
|
||||||
|
group('DietService', () {
|
||||||
|
late MockDietService mockDietService;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockDietService = MockDietService();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likeDiet returns DietLikesModel', () async {
|
||||||
|
const id = 1;
|
||||||
|
const expected = DietLikesModel(totalLikes: 10, isLiked: true);
|
||||||
|
|
||||||
|
when(mockDietService.likeDiet(id: id)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await mockDietService.likeDiet(id: id);
|
||||||
|
|
||||||
|
expect(result, isA<DietLikesModel>());
|
||||||
|
expect(result.totalLikes, 10);
|
||||||
|
expect(result.isLiked, true);
|
||||||
|
verify(mockDietService.likeDiet(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likeDiet returns DietLikesModel with different values', () async {
|
||||||
|
const id = 2;
|
||||||
|
const expected = DietLikesModel(totalLikes: 5, isLiked: false);
|
||||||
|
|
||||||
|
when(mockDietService.likeDiet(id: id)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await mockDietService.likeDiet(id: id);
|
||||||
|
|
||||||
|
expect(result, isA<DietLikesModel>());
|
||||||
|
expect(result.totalLikes, 5);
|
||||||
|
expect(result.isLiked, false);
|
||||||
|
verify(mockDietService.likeDiet(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addToFavorite returns Diet', () async {
|
||||||
|
const id = 1;
|
||||||
|
const expected = Diet(
|
||||||
|
id: id,
|
||||||
|
slug: 'test-diet',
|
||||||
|
name: 'Test Diet',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: DietState.active,
|
||||||
|
type: DietType(light: true, veg: false, carb: false, meat: false),
|
||||||
|
details: 'Test diet description',
|
||||||
|
dailySchedule: {},
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: true,
|
||||||
|
dailyNutrition: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockDietService.addToFavorite(id: id)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await mockDietService.addToFavorite(id: id);
|
||||||
|
|
||||||
|
expect(result, isA<Diet>());
|
||||||
|
expect(result.id, id);
|
||||||
|
expect(result.isFavorite, true);
|
||||||
|
verify(mockDietService.addToFavorite(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeFromFavorite returns Diet', () async {
|
||||||
|
const id = 1;
|
||||||
|
const expected = Diet(
|
||||||
|
id: id,
|
||||||
|
slug: 'test-diet',
|
||||||
|
name: 'Test Diet',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: DietState.active,
|
||||||
|
type: DietType(light: true, veg: false, carb: false, meat: false),
|
||||||
|
details: 'Test diet description',
|
||||||
|
dailySchedule: {},
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
dailyNutrition: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockDietService.removeFromFavorite(id: id)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await mockDietService.removeFromFavorite(id: id);
|
||||||
|
|
||||||
|
expect(result, isA<Diet>());
|
||||||
|
expect(result.id, id);
|
||||||
|
expect(result.isFavorite, false);
|
||||||
|
verify(mockDietService.removeFromFavorite(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getDiet returns Diet by id', () async {
|
||||||
|
const id = 1;
|
||||||
|
const expected = Diet(
|
||||||
|
id: id,
|
||||||
|
slug: 'test-diet',
|
||||||
|
name: 'Test Diet',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: DietState.active,
|
||||||
|
type: DietType(light: true, veg: false, carb: false, meat: false),
|
||||||
|
details: 'Test diet description',
|
||||||
|
dailySchedule: {},
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
dailyNutrition: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockDietService.getDiet(id: id)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await mockDietService.getDiet(id: id);
|
||||||
|
|
||||||
|
expect(result, isA<Diet>());
|
||||||
|
expect(result.id, id);
|
||||||
|
expect(result.name, 'Test Diet');
|
||||||
|
verify(mockDietService.getDiet(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getDietBySlug returns Diet by slug', () async {
|
||||||
|
const slug = 'test-diet-slug';
|
||||||
|
const expected = Diet(
|
||||||
|
id: 1,
|
||||||
|
slug: slug,
|
||||||
|
name: 'Test Diet',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: DietState.active,
|
||||||
|
type: DietType(light: true, veg: false, carb: false, meat: false),
|
||||||
|
details: 'Test diet description',
|
||||||
|
dailySchedule: {},
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
dailyNutrition: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockDietService.getDietBySlug(slug: slug)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await mockDietService.getDietBySlug(slug: slug);
|
||||||
|
|
||||||
|
expect(result, isA<Diet>());
|
||||||
|
expect(result.slug, slug);
|
||||||
|
verify(mockDietService.getDietBySlug(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('startDiet starts diet with date', () async {
|
||||||
|
const id = 1;
|
||||||
|
const date = '2024-01-01';
|
||||||
|
final body = {'date': date};
|
||||||
|
|
||||||
|
when(mockDietService.startDiet(id: id, body: body)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await mockDietService.startDiet(id: id, body: body);
|
||||||
|
|
||||||
|
verify(mockDietService.startDiet(id: id, body: body)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cancelDiet cancels diet by id', () async {
|
||||||
|
const id = 1;
|
||||||
|
|
||||||
|
when(mockDietService.cancelDiet(id: id)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await mockDietService.cancelDiet(id: id);
|
||||||
|
|
||||||
|
verify(mockDietService.cancelDiet(id: id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteDiet deletes diet by id', () async {
|
||||||
|
const id = 1;
|
||||||
|
|
||||||
|
when(mockDietService.deleteDiet(id: id)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await mockDietService.deleteDiet(id: id);
|
||||||
|
|
||||||
|
verify(mockDietService.deleteDiet(id: id)).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
181
test/features/plans/presentation/cubit/plans_cubit_test.dart
Normal file
181
test/features/plans/presentation/cubit/plans_cubit_test.dart
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import 'package:eatvio_app/core/models/pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
|
|
||||||
|
import 'plans_cubit_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<FeedRepository>(), MockSpec<PlanRepository>()])
|
||||||
|
void main() {
|
||||||
|
group('PlansCubit', () {
|
||||||
|
PlansCubit? plansCubit;
|
||||||
|
late MockFeedRepository mockFeedRepository;
|
||||||
|
late MockPlanRepository mockPlanRepository;
|
||||||
|
|
||||||
|
const testPlan = PlanModel(
|
||||||
|
id: 1,
|
||||||
|
slug: 'test-plan',
|
||||||
|
name: 'Test Plan',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: 'active',
|
||||||
|
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
|
||||||
|
details: 'Test plan description',
|
||||||
|
packetsCount: 1,
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
isFavorite: false,
|
||||||
|
minPriceOfOptions: 100.0,
|
||||||
|
maxPriceOfOptions: 500.0,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const testPlansPagination = PlanPaginationModel(
|
||||||
|
data: [testPlan],
|
||||||
|
links: Links(first: '', last: '', prev: null, next: null),
|
||||||
|
meta: Meta(
|
||||||
|
currentPage: 1,
|
||||||
|
lastPage: 1,
|
||||||
|
total: 1,
|
||||||
|
perPage: 10,
|
||||||
|
from: null,
|
||||||
|
links: [Link(url: null, label: null, active: null)],
|
||||||
|
to: null,
|
||||||
|
totalMinPrice: 100,
|
||||||
|
totalMaxPrice: 500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockFeedRepository = MockFeedRepository();
|
||||||
|
mockPlanRepository = MockPlanRepository();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
if (plansCubit != null && !plansCubit!.isClosed) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
plansCubit!.close();
|
||||||
|
plansCubit = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('начальное состояние - initial state', () {
|
||||||
|
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||||
|
expect(plansCubit!.state.plans, isEmpty);
|
||||||
|
expect(plansCubit!.state.isLoading, true);
|
||||||
|
expect(plansCubit!.state.errorMessage, null);
|
||||||
|
expect(plansCubit!.state.filterState.durationIndex, 0);
|
||||||
|
expect(plansCubit!.state.filterState.typeIndex, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadPlans - загружает планы успешно', () async {
|
||||||
|
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getPlans(query: null, types: null, duration: null, minPrice: null, maxPrice: null)).thenAnswer((_) async => testPlansPagination);
|
||||||
|
|
||||||
|
await plansCubit!.loadPlans();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(plansCubit!.state.plans.length, 1);
|
||||||
|
expect(plansCubit!.state.plans.first.name, 'Test Plan');
|
||||||
|
expect(plansCubit!.state.errorMessage, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadPlans - обрабатывает ошибку', () async {
|
||||||
|
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getPlans(query: null, types: null, duration: null, minPrice: null, maxPrice: null)).thenThrow(Exception('Network error'));
|
||||||
|
|
||||||
|
await plansCubit!.loadPlans();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(plansCubit!.state.errorMessage, isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changeTypeFilter - меняет фильтр типа', () async {
|
||||||
|
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||||
|
|
||||||
|
plansCubit!.changeTypeFilter(1);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(plansCubit!.state.filterState.typeIndex, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changeDurationFilter - меняет фильтр длительности', () async {
|
||||||
|
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||||
|
|
||||||
|
plansCubit!.changeDurationFilter(1);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(plansCubit!.state.filterState.durationIndex, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changePriceFilter - меняет фильтр цены', () async {
|
||||||
|
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||||
|
|
||||||
|
plansCubit!.changePriceFilter(100, 500, disableDebounce: true);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(plansCubit!.state.filterState.priceFilter.$1, 100);
|
||||||
|
expect(plansCubit!.state.filterState.priceFilter.$2, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likePlan - лайкает план', () async {
|
||||||
|
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getPlans(query: null, types: null, duration: null, minPrice: null, maxPrice: null)).thenAnswer((_) async => testPlansPagination);
|
||||||
|
|
||||||
|
await plansCubit!.loadPlans();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
plansCubit!.likePlan('test-plan', 0);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(plansCubit!.state.plans[0].wasLikedByMe, true);
|
||||||
|
expect(plansCubit!.state.plans[0].totalLikes, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addPlanToFavorite - добавляет план в избранное', () async {
|
||||||
|
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getPlans(query: null, types: null, duration: null, minPrice: null, maxPrice: null)).thenAnswer((_) async => testPlansPagination);
|
||||||
|
|
||||||
|
await plansCubit!.loadPlans();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
when(mockPlanRepository.addToFavorite('test-plan')).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
plansCubit!.addPlanToFavorite('test-plan', 0);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(plansCubit!.state.plans[0].isFavorite, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removePlanFromFavorite - удаляет план из избранного', () async {
|
||||||
|
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockFeedRepository.getPlans(query: null, types: null, duration: null, minPrice: null, maxPrice: null),
|
||||||
|
).thenAnswer((_) async => testPlansPagination.copyWith(data: [testPlan.copyWith(isFavorite: true)]));
|
||||||
|
|
||||||
|
await plansCubit!.loadPlans();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
when(mockPlanRepository.removeFromFavorite('test-plan')).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
plansCubit!.removePlanFromFavorite('test-plan', 0);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(plansCubit!.state.plans[0].isFavorite, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
246
test/features/plans/presentation/widgets/basic_widget_test.dart
Normal file
246
test/features/plans/presentation/widgets/basic_widget_test.dart
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
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/feed/models/diets_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/widgets/plans.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/repositories/plan_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_portal/flutter_portal.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
import 'package:network_image_mock/network_image_mock.dart';
|
||||||
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
|
|
||||||
|
import 'basic_widget_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<FeedRepository>(), MockSpec<PlanRepository>()])
|
||||||
|
void main() {
|
||||||
|
group('Plans Basic Widget Tests', () {
|
||||||
|
late MockFeedRepository mockFeedRepository;
|
||||||
|
late MockPlanRepository mockPlanRepository;
|
||||||
|
late PlansCubit plansCubit;
|
||||||
|
|
||||||
|
const testPlan = PlanModel(
|
||||||
|
id: 1,
|
||||||
|
slug: 'test-plan',
|
||||||
|
name: 'Test Plan',
|
||||||
|
image: 'test-image.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: 'active',
|
||||||
|
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
|
||||||
|
details: 'Test plan description',
|
||||||
|
packetsCount: 1,
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
isFavorite: false,
|
||||||
|
minPriceOfOptions: 100.0,
|
||||||
|
maxPriceOfOptions: 500.0,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await sl.reset();
|
||||||
|
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
||||||
|
mockFeedRepository = MockFeedRepository();
|
||||||
|
mockPlanRepository = MockPlanRepository();
|
||||||
|
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||||
|
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
||||||
|
VisibilityDetectorController.instance.updateInterval = Duration.zero;
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
if (!plansCubit.isClosed) {
|
||||||
|
plansCubit.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget createTestWidget(Widget child, {bool wrapInScaffold = true}) {
|
||||||
|
final widget = BlocProvider<PlansCubit>(
|
||||||
|
create: (context) => plansCubit,
|
||||||
|
child: Portal(
|
||||||
|
child: wrapInScaffold
|
||||||
|
? Scaffold(body: SingleChildScrollView(child: child))
|
||||||
|
: SizedBox(width: 800, height: 1200, child: SingleChildScrollView(child: child)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeHelper().themeData(),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [Locale('ru')],
|
||||||
|
home: widget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget createPlansTestWidget(Widget child) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeHelper().themeData(),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [Locale('ru')],
|
||||||
|
home: Portal(
|
||||||
|
child: Scaffold(
|
||||||
|
body: BlocProvider<PlansCubit>(
|
||||||
|
create: (context) => plansCubit,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) => SizedBox(
|
||||||
|
width: constraints.maxWidth > 0 ? constraints.maxWidth : 800,
|
||||||
|
height: constraints.maxHeight > 0 ? constraints.maxHeight : 600,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('should create PlansCubit successfully', (WidgetTester tester) async {
|
||||||
|
// Arrange & Act
|
||||||
|
final cubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(cubit, isA<PlansCubit>());
|
||||||
|
expect(cubit.state.plans, isEmpty);
|
||||||
|
|
||||||
|
cubit.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display PlanWidget with plan name', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
plansCubit.emit(PlansState.initial().copyWith(plans: [testPlan], isLoading: false));
|
||||||
|
const widget = PlanWidget(plan: testPlan, index: 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.text('Test Plan'), findsOneWidget);
|
||||||
|
expect(find.byType(PlanWidget), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display PlanWidget with description when details are present', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
plansCubit.emit(PlansState.initial().copyWith(plans: [testPlan], isLoading: false));
|
||||||
|
const widget = PlanWidget(plan: testPlan, index: 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.text('Test Plan'), findsOneWidget);
|
||||||
|
expect(find.byType(PlanWidget), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display PlanWidget structure correctly', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
plansCubit.emit(PlansState.initial().copyWith(plans: [testPlan], isLoading: false));
|
||||||
|
const widget = PlanWidget(plan: testPlan, index: 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(PlanWidget), findsOneWidget);
|
||||||
|
expect(find.text('Test Plan'), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display Plans widget with loading state', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
plansCubit.emit(PlansState.initial());
|
||||||
|
final widget = Plans(handleVisibility: ({required visibleFraction, required id, required type}) {});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createPlansTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(Plans), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display Plans widget with loaded state', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
plansCubit.emit(PlansState.initial().copyWith(plans: [testPlan], isLoading: false));
|
||||||
|
final widget = Plans(handleVisibility: ({required visibleFraction, required id, required type}) {});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createPlansTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(Plans), findsOneWidget);
|
||||||
|
expect(find.byType(RefreshIndicator), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display Plans widget with error state', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
plansCubit.emit(PlansState.initial().copyWith(isLoading: false, errorMessage: 'Error message'));
|
||||||
|
final widget = Plans(handleVisibility: ({required visibleFraction, required id, required type}) {});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createPlansTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(Plans), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display Plans widget with multiple plans', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
final testPlan2 = testPlan.copyWith(id: 2, name: 'Test Plan 2');
|
||||||
|
plansCubit.emit(PlansState.initial().copyWith(plans: [testPlan, testPlan2], isLoading: false));
|
||||||
|
final widget = Plans(handleVisibility: ({required visibleFraction, required id, required type}) {});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createPlansTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(Plans), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
131
test/features/plans/presentation/widgets/widgets_test.dart
Normal file
131
test/features/plans/presentation/widgets/widgets_test.dart
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
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/core/widgets/animated_option_button.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_state.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/presentation/widgets/options_tab.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/repositories/plan_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_portal/flutter_portal.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
|
||||||
|
import 'widgets_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<PlanRepository>()])
|
||||||
|
void main() {
|
||||||
|
group('Plans Widgets Tests', () {
|
||||||
|
late MockPlanRepository mockPlanRepository;
|
||||||
|
late AboutPlanCubit aboutPlanCubit;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await sl.reset();
|
||||||
|
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
||||||
|
mockPlanRepository = MockPlanRepository();
|
||||||
|
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
|
||||||
|
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
if (!aboutPlanCubit.isClosed) {
|
||||||
|
aboutPlanCubit.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget createTestWidget(Widget child) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeHelper().themeData(),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [Locale('ru')],
|
||||||
|
home: Portal(
|
||||||
|
child: Scaffold(body: SingleChildScrollView(child: child)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('OptionsTab', () {
|
||||||
|
testWidgets('should display OptionsTab with three options', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
aboutPlanCubit.emit(AboutPlanState.initial().copyWith(chosenOptionIndex: 0));
|
||||||
|
const widget = OptionsTab(cost: [100, 200, 300]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(BlocProvider<AboutPlanCubit>(create: (context) => aboutPlanCubit, child: createTestWidget(widget)));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(OptionsTab), findsOneWidget);
|
||||||
|
expect(find.byType(AnimatedOptionButton), findsNWidgets(3));
|
||||||
|
expect(find.text('100₽'), findsOneWidget);
|
||||||
|
expect(find.text('200₽'), findsOneWidget);
|
||||||
|
expect(find.text('300₽'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should highlight selected option', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
aboutPlanCubit.emit(AboutPlanState.initial().copyWith(chosenOptionIndex: 1));
|
||||||
|
const widget = OptionsTab(cost: [100, 200, 300]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(BlocProvider<AboutPlanCubit>(create: (context) => aboutPlanCubit, child: createTestWidget(widget)));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(OptionsTab), findsOneWidget);
|
||||||
|
expect(find.byType(AnimatedOptionButton), findsNWidgets(3));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should handle option selection', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
aboutPlanCubit.emit(AboutPlanState.initial().copyWith(chosenOptionIndex: 0));
|
||||||
|
const widget = OptionsTab(cost: [100, 200, 300]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(BlocProvider<AboutPlanCubit>(create: (context) => aboutPlanCubit, child: createTestWidget(widget)));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Tap the second option
|
||||||
|
final buttons = find.byType(AnimatedOptionButton);
|
||||||
|
expect(buttons, findsNWidgets(3));
|
||||||
|
await tester.tap(buttons.at(1));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(aboutPlanCubit.state.chosenOptionIndex, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display OptionsTab with different costs', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
aboutPlanCubit.emit(AboutPlanState.initial().copyWith(chosenOptionIndex: 0));
|
||||||
|
const widget = OptionsTab(cost: [500, 1000, 1500]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(BlocProvider<AboutPlanCubit>(create: (context) => aboutPlanCubit, child: createTestWidget(widget)));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(OptionsTab), findsOneWidget);
|
||||||
|
expect(find.text('500₽'), findsOneWidget);
|
||||||
|
expect(find.text('1000₽'), findsOneWidget);
|
||||||
|
expect(find.text('1500₽'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
144
test/features/plans/repositories/plan_repository_test.dart
Normal file
144
test/features/plans/repositories/plan_repository_test.dart
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/services/plan_service.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
|
|
||||||
|
import 'plan_repository_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<PlanService>()])
|
||||||
|
void main() {
|
||||||
|
group('PlanRepository', () {
|
||||||
|
late PlanRepository planRepository;
|
||||||
|
late MockPlanService mockPlanService;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockPlanService = MockPlanService();
|
||||||
|
planRepository = PlanRepository(planService: mockPlanService);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getPlan returns PlanModel by slug', () async {
|
||||||
|
const slug = 'test-plan-slug';
|
||||||
|
const expected = PlanModel(
|
||||||
|
id: 1,
|
||||||
|
slug: slug,
|
||||||
|
name: 'Test Plan',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: 'active',
|
||||||
|
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
|
||||||
|
details: 'Test plan description',
|
||||||
|
packetsCount: 1,
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
isFavorite: false,
|
||||||
|
minPriceOfOptions: 100.0,
|
||||||
|
maxPriceOfOptions: 500.0,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockPlanService.getPlan(slug)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await planRepository.getPlan(slug);
|
||||||
|
|
||||||
|
expect(result, isA<PlanModel>());
|
||||||
|
expect(result.slug, slug);
|
||||||
|
expect(result.name, 'Test Plan');
|
||||||
|
verify(mockPlanService.getPlan(slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likePlan likes plan by slug', () async {
|
||||||
|
const slug = 'test-plan-slug';
|
||||||
|
|
||||||
|
when(mockPlanService.likePlan(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await planRepository.likePlan(slug);
|
||||||
|
|
||||||
|
verify(mockPlanService.likePlan(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addToFavorite adds plan to favorite by slug', () async {
|
||||||
|
const slug = 'test-plan-slug';
|
||||||
|
|
||||||
|
when(mockPlanService.addToFavorite(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await planRepository.addToFavorite(slug);
|
||||||
|
|
||||||
|
verify(mockPlanService.addToFavorite(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeFromFavorite removes plan from favorite by slug', () async {
|
||||||
|
const slug = 'test-plan-slug';
|
||||||
|
|
||||||
|
when(mockPlanService.removeFromFavorite(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await planRepository.removeFromFavorite(slug);
|
||||||
|
|
||||||
|
verify(mockPlanService.removeFromFavorite(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chosePlan chooses plan with option', () async {
|
||||||
|
const slug = 'test-plan-slug';
|
||||||
|
final option = {'option_type': 'basic', 'start_date': '2024-01-01'};
|
||||||
|
final expected = ChosenPlanModel(
|
||||||
|
id: 1,
|
||||||
|
optionType: 'basic',
|
||||||
|
startDate: DateTime(2024, 1, 1),
|
||||||
|
chosenPlanState: ChosenPlanStateEnum.stateStarted,
|
||||||
|
chatId: 'chat-123',
|
||||||
|
consultantId: 1,
|
||||||
|
plan: const PlanModel(
|
||||||
|
id: 1,
|
||||||
|
slug: slug,
|
||||||
|
name: 'Test Plan',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: 'active',
|
||||||
|
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
|
||||||
|
details: 'Test plan description',
|
||||||
|
packetsCount: 1,
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
isFavorite: false,
|
||||||
|
minPriceOfOptions: 100.0,
|
||||||
|
maxPriceOfOptions: 500.0,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockPlanService.chosePlan(slug: slug, option: option)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await planRepository.chosePlan(slug, option);
|
||||||
|
|
||||||
|
expect(result, isA<ChosenPlanModel>());
|
||||||
|
expect(result.plan.slug, slug);
|
||||||
|
verify(mockPlanService.chosePlan(slug: slug, option: option)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getPlan rethrows exception on error', () async {
|
||||||
|
const slug = 'test-plan-slug';
|
||||||
|
|
||||||
|
when(mockPlanService.getPlan(slug)).thenThrow(Exception('Network error'));
|
||||||
|
|
||||||
|
expect(() => planRepository.getPlan(slug), throwsException);
|
||||||
|
verify(mockPlanService.getPlan(slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likePlan rethrows exception on error', () async {
|
||||||
|
const slug = 'test-plan-slug';
|
||||||
|
|
||||||
|
when(mockPlanService.likePlan(slug: slug)).thenThrow(Exception('Network error'));
|
||||||
|
|
||||||
|
expect(() => planRepository.likePlan(slug), throwsException);
|
||||||
|
verify(mockPlanService.likePlan(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
124
test/features/plans/services/plan_service_test.dart
Normal file
124
test/features/plans/services/plan_service_test.dart
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/services/plan_service.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
|
|
||||||
|
import 'plan_service_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<PlanService>()])
|
||||||
|
void main() {
|
||||||
|
group('PlanService', () {
|
||||||
|
late MockPlanService mockPlanService;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockPlanService = MockPlanService();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getPlan returns PlanModel by slug', () async {
|
||||||
|
const slug = 'test-plan-slug';
|
||||||
|
const expected = PlanModel(
|
||||||
|
id: 1,
|
||||||
|
slug: slug,
|
||||||
|
name: 'Test Plan',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: 'active',
|
||||||
|
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
|
||||||
|
details: 'Test plan description',
|
||||||
|
packetsCount: 1,
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
isFavorite: false,
|
||||||
|
minPriceOfOptions: 100.0,
|
||||||
|
maxPriceOfOptions: 500.0,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockPlanService.getPlan(slug)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await mockPlanService.getPlan(slug);
|
||||||
|
|
||||||
|
expect(result, isA<PlanModel>());
|
||||||
|
expect(result.slug, slug);
|
||||||
|
expect(result.name, 'Test Plan');
|
||||||
|
verify(mockPlanService.getPlan(slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likePlan likes plan by slug', () async {
|
||||||
|
const slug = 'test-plan-slug';
|
||||||
|
|
||||||
|
when(mockPlanService.likePlan(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await mockPlanService.likePlan(slug: slug);
|
||||||
|
|
||||||
|
verify(mockPlanService.likePlan(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addToFavorite adds plan to favorite by slug', () async {
|
||||||
|
const slug = 'test-plan-slug';
|
||||||
|
|
||||||
|
when(mockPlanService.addToFavorite(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await mockPlanService.addToFavorite(slug: slug);
|
||||||
|
|
||||||
|
verify(mockPlanService.addToFavorite(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeFromFavorite removes plan from favorite by slug', () async {
|
||||||
|
const slug = 'test-plan-slug';
|
||||||
|
|
||||||
|
when(mockPlanService.removeFromFavorite(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await mockPlanService.removeFromFavorite(slug: slug);
|
||||||
|
|
||||||
|
verify(mockPlanService.removeFromFavorite(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chosePlan chooses plan with option', () async {
|
||||||
|
const slug = 'test-plan-slug';
|
||||||
|
final option = {'option_type': 'basic', 'start_date': '2024-01-01'};
|
||||||
|
final expected = ChosenPlanModel(
|
||||||
|
id: 1,
|
||||||
|
optionType: 'basic',
|
||||||
|
startDate: DateTime(2024, 1, 1),
|
||||||
|
chosenPlanState: ChosenPlanStateEnum.stateStarted,
|
||||||
|
chatId: 'chat-123',
|
||||||
|
consultantId: 1,
|
||||||
|
plan: const PlanModel(
|
||||||
|
id: 1,
|
||||||
|
slug: slug,
|
||||||
|
name: 'Test Plan',
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
duration: 30,
|
||||||
|
state: 'active',
|
||||||
|
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
|
||||||
|
details: 'Test plan description',
|
||||||
|
packetsCount: 1,
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
totalLikes: 1,
|
||||||
|
totalViews: 1,
|
||||||
|
totalComments: 1,
|
||||||
|
isFavorite: false,
|
||||||
|
minPriceOfOptions: 100.0,
|
||||||
|
maxPriceOfOptions: 500.0,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockPlanService.chosePlan(slug: slug, option: option)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await mockPlanService.chosePlan(slug: slug, option: option);
|
||||||
|
|
||||||
|
expect(result, isA<ChosenPlanModel>());
|
||||||
|
expect(result.plan.slug, slug);
|
||||||
|
verify(mockPlanService.chosePlan(slug: slug, option: option)).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
192
test/features/recipes/presentation/cubit/recipes_cubit_test.dart
Normal file
192
test/features/recipes/presentation/cubit/recipes_cubit_test.dart
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart';
|
||||||
|
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
|
|
||||||
|
import 'recipes_cubit_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<FeedRepository>(), MockSpec<RecipeRepository>(), MockSpec<AboutRecipeRepository>()])
|
||||||
|
void main() {
|
||||||
|
group('RecipesCubit', () {
|
||||||
|
RecipesCubit? recipesCubit;
|
||||||
|
late MockFeedRepository mockFeedRepository;
|
||||||
|
late MockRecipeRepository mockRecipeRepository;
|
||||||
|
late MockAboutRecipeRepository mockAboutRecipeRepository;
|
||||||
|
|
||||||
|
const testRecipe = Recipe(
|
||||||
|
id: 1,
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
state: 'active',
|
||||||
|
cookingTime: 30,
|
||||||
|
activeCookingTime: 15,
|
||||||
|
ingredients: [],
|
||||||
|
description: 'Test recipe description',
|
||||||
|
steps: [],
|
||||||
|
videoUrl: null,
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
title: 'Test Recipe',
|
||||||
|
type: 'main',
|
||||||
|
slug: 'test-recipe-slug',
|
||||||
|
commentsCount: 5,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
calories: 300.0,
|
||||||
|
servings: 4,
|
||||||
|
likesCount: 10,
|
||||||
|
viewsCount: 100,
|
||||||
|
tags: [],
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
);
|
||||||
|
|
||||||
|
final testRecipesList = [testRecipe];
|
||||||
|
final testRecipeTags = [RecipeTag(id: 1, name: 'Breakfast', recipesCount: 10, createdAt: DateTime(2024, 1, 1), updatedAt: DateTime(2024, 1, 1))];
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockFeedRepository = MockFeedRepository();
|
||||||
|
mockRecipeRepository = MockRecipeRepository();
|
||||||
|
mockAboutRecipeRepository = MockAboutRecipeRepository();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
if (recipesCubit != null && !recipesCubit!.isClosed) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
recipesCubit!.close();
|
||||||
|
recipesCubit = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('начальное состояние - initial state', () {
|
||||||
|
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||||
|
expect(recipesCubit!.state.recipesList, isEmpty);
|
||||||
|
expect(recipesCubit!.state.recipesTags, isEmpty);
|
||||||
|
expect(recipesCubit!.state.selectedTags, isEmpty);
|
||||||
|
expect(recipesCubit!.state.selectedTagsCounter, 0);
|
||||||
|
expect(recipesCubit!.state.isLoading, true);
|
||||||
|
expect(recipesCubit!.state.errorMessage, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadRecipesList - загружает рецепты успешно', () async {
|
||||||
|
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getRecipes(query: null, tags: [])).thenAnswer((_) async => testRecipesList);
|
||||||
|
|
||||||
|
when(mockRecipeRepository.getRecipesTags()).thenAnswer((_) async => testRecipeTags);
|
||||||
|
|
||||||
|
await recipesCubit!.loadRecipesList();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(recipesCubit!.state.recipesList.length, 1);
|
||||||
|
expect(recipesCubit!.state.recipesList.first.title, 'Test Recipe');
|
||||||
|
expect(recipesCubit!.state.recipesTags.length, 1);
|
||||||
|
expect(recipesCubit!.state.errorMessage, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadRecipesList - обрабатывает ошибку', () async {
|
||||||
|
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getRecipes(query: null, tags: [])).thenThrow(Exception('Network error'));
|
||||||
|
|
||||||
|
await recipesCubit!.loadRecipesList();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(recipesCubit!.state.errorMessage, isNotNull);
|
||||||
|
expect(recipesCubit!.state.isLoading, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateTags - добавляет тег', () {
|
||||||
|
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||||
|
|
||||||
|
final tag = RecipeTag(id: 1, name: 'Breakfast', recipesCount: 10, createdAt: DateTime(2024, 1, 1), updatedAt: DateTime(2024, 1, 1));
|
||||||
|
|
||||||
|
recipesCubit!.updateTags(tag);
|
||||||
|
|
||||||
|
expect(recipesCubit!.state.selectedTags.contains(1), true);
|
||||||
|
expect(recipesCubit!.state.selectedTagsCounter, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateTags - удаляет тег', () {
|
||||||
|
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||||
|
|
||||||
|
final tag = RecipeTag(id: 1, name: 'Breakfast', recipesCount: 10, createdAt: DateTime(2024, 1, 1), updatedAt: DateTime(2024, 1, 1));
|
||||||
|
|
||||||
|
recipesCubit!.updateTags(tag);
|
||||||
|
recipesCubit!.updateTags(tag);
|
||||||
|
|
||||||
|
expect(recipesCubit!.state.selectedTags.contains(1), false);
|
||||||
|
expect(recipesCubit!.state.selectedTagsCounter, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likeRecipe - лайкает рецепт', () async {
|
||||||
|
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getRecipes(query: null, tags: [])).thenAnswer((_) async => testRecipesList);
|
||||||
|
|
||||||
|
when(mockRecipeRepository.getRecipesTags()).thenAnswer((_) async => testRecipeTags);
|
||||||
|
|
||||||
|
await recipesCubit!.loadRecipesList();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
when(mockAboutRecipeRepository.likeRecipe('test-recipe-slug')).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
recipesCubit!.likeRecipe('test-recipe-slug', 0);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(recipesCubit!.state.recipesList[0].wasLikedByMe, true);
|
||||||
|
expect(recipesCubit!.state.recipesList[0].likesCount, 11);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addRecipeToFavorite - добавляет рецепт в избранное', () async {
|
||||||
|
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getRecipes(query: null, tags: [])).thenAnswer((_) async => testRecipesList);
|
||||||
|
|
||||||
|
when(mockRecipeRepository.getRecipesTags()).thenAnswer((_) async => testRecipeTags);
|
||||||
|
|
||||||
|
await recipesCubit!.loadRecipesList();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
when(mockAboutRecipeRepository.addToFavorite('test-recipe-slug')).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
recipesCubit!.addRecipeToFavorite('test-recipe-slug', 0);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(recipesCubit!.state.recipesList[0].isFavorite, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeRecipeFromFavorite - удаляет рецепт из избранного', () async {
|
||||||
|
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||||
|
|
||||||
|
when(mockFeedRepository.getRecipes(query: null, tags: [])).thenAnswer((_) async => [testRecipe.copyWith(isFavorite: true)]);
|
||||||
|
|
||||||
|
when(mockRecipeRepository.getRecipesTags()).thenAnswer((_) async => testRecipeTags);
|
||||||
|
|
||||||
|
await recipesCubit!.loadRecipesList();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
when(mockAboutRecipeRepository.removeFromFavorite('test-recipe-slug')).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
recipesCubit!.removeRecipeFromFavorite('test-recipe-slug', 0);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(recipesCubit!.state.recipesList[0].isFavorite, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteRecipe - удаляет рецепт', () async {
|
||||||
|
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||||
|
|
||||||
|
when(mockAboutRecipeRepository.deleteRecipe('test-recipe-slug')).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
final result = await recipesCubit!.deleteRecipe('test-recipe-slug');
|
||||||
|
|
||||||
|
expect(result, true);
|
||||||
|
verify(mockAboutRecipeRepository.deleteRecipe('test-recipe-slug')).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,16 +2,15 @@ import 'package:eatvio_app/core/app_config.dart';
|
|||||||
import 'package:eatvio_app/core/app_initializer.dart';
|
import 'package:eatvio_app/core/app_initializer.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/core/widgets/animated_option_button.dart';
|
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
import 'package:eatvio_app/features/feed/models/recipes_model.dart' as recipes;
|
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||||
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
|
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
|
||||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
|
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart';
|
||||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart';
|
import 'package:eatvio_app/features/feed/presentation/widgets/recipes.dart';
|
||||||
import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipe_widget.dart';
|
import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.dart';
|
||||||
import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipes_tab.dart';
|
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
|
||||||
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
|
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
|
||||||
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
|
|
||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@@ -20,56 +19,70 @@ import 'package:flutter_portal/flutter_portal.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mockito/annotations.dart';
|
import 'package:mockito/annotations.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
import 'package:network_image_mock/network_image_mock.dart';
|
||||||
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
|
|
||||||
import 'basic_widget_test.mocks.dart';
|
import 'basic_widget_test.mocks.dart';
|
||||||
|
|
||||||
@GenerateNiceMocks([MockSpec<MyRecipesRepository>(), MockSpec<AboutRecipeRepository>()])
|
@GenerateNiceMocks([MockSpec<FeedRepository>(), MockSpec<RecipeRepository>(), MockSpec<AboutRecipeRepository>()])
|
||||||
void main() {
|
void main() {
|
||||||
group('MyRecipes Basic Widget Tests', () {
|
group('Recipes Basic Widget Tests', () {
|
||||||
late MockMyRecipesRepository mockMyRecipesRepository;
|
late MockFeedRepository mockFeedRepository;
|
||||||
|
late MockRecipeRepository mockRecipeRepository;
|
||||||
late MockAboutRecipeRepository mockAboutRecipeRepository;
|
late MockAboutRecipeRepository mockAboutRecipeRepository;
|
||||||
late MyRecipesCubit myRecipesCubit;
|
late RecipesCubit recipesCubit;
|
||||||
|
|
||||||
const testRecipe = MyRecipeModel(
|
const testRecipe = Recipe(
|
||||||
id: 1,
|
id: 1,
|
||||||
image: 'test-image.jpg',
|
image: 'test-image.jpg',
|
||||||
state: 'active',
|
state: 'active',
|
||||||
cookingTime: 30,
|
cookingTime: 30,
|
||||||
activeCookingTime: 20,
|
activeCookingTime: 15,
|
||||||
|
ingredients: [],
|
||||||
description: 'Test recipe description',
|
description: 'Test recipe description',
|
||||||
steps: [
|
steps: [],
|
||||||
recipes.Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1 description'),
|
videoUrl: null,
|
||||||
recipes.Step(image: 'https://someurl.com/step2.jpg', description: 'Step 2 description'),
|
createdAt: null,
|
||||||
],
|
updatedAt: null,
|
||||||
videoUrl: 'https://someurl.com/video.mp4',
|
|
||||||
title: 'Test Recipe',
|
title: 'Test Recipe',
|
||||||
slug: 'test-recipe',
|
type: 'main',
|
||||||
calories: 250.0,
|
slug: 'test-recipe-slug',
|
||||||
likesCount: 5,
|
commentsCount: 5,
|
||||||
viewsCount: 10,
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
calories: 300.0,
|
||||||
|
servings: 4,
|
||||||
|
likesCount: 10,
|
||||||
|
viewsCount: 100,
|
||||||
|
tags: [],
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
|
||||||
);
|
);
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
await sl.reset();
|
await sl.reset();
|
||||||
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
||||||
mockMyRecipesRepository = MockMyRecipesRepository();
|
mockFeedRepository = MockFeedRepository();
|
||||||
|
mockRecipeRepository = MockRecipeRepository();
|
||||||
mockAboutRecipeRepository = MockAboutRecipeRepository();
|
mockAboutRecipeRepository = MockAboutRecipeRepository();
|
||||||
myRecipesCubit = MyRecipesCubit(myRecipesRepository: mockMyRecipesRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||||
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
||||||
|
VisibilityDetectorController.instance.updateInterval = Duration.zero;
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
if (!myRecipesCubit.isClosed) {
|
if (!recipesCubit.isClosed) {
|
||||||
myRecipesCubit.close();
|
recipesCubit.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget createTestWidget(Widget child, {bool wrapInScaffold = true}) {
|
Widget createTestWidget(Widget child, {bool wrapInScaffold = true}) {
|
||||||
final widget = BlocProvider<MyRecipesCubit>(
|
final widget = BlocProvider<RecipesCubit>(
|
||||||
create: (context) => myRecipesCubit,
|
create: (context) => recipesCubit,
|
||||||
child: Portal(
|
child: Portal(
|
||||||
child: wrapInScaffold ? Scaffold(body: SingleChildScrollView(child: child)) : SizedBox(width: 800, height: 600, child: child),
|
child: wrapInScaffold
|
||||||
|
? Scaffold(body: SingleChildScrollView(child: child))
|
||||||
|
: SizedBox(width: 800, height: 1200, child: SingleChildScrollView(child: child)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
@@ -85,194 +98,158 @@ void main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
testWidgets('should create MyRecipesCubit successfully', (WidgetTester tester) async {
|
Widget createRecipesTestWidget(Widget child) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeHelper().themeData(),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [Locale('ru')],
|
||||||
|
home: Portal(
|
||||||
|
child: Scaffold(
|
||||||
|
body: BlocProvider<RecipesCubit>(
|
||||||
|
create: (context) => recipesCubit,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) => SizedBox(
|
||||||
|
width: constraints.maxWidth > 0 ? constraints.maxWidth : 800,
|
||||||
|
height: constraints.maxHeight > 0 ? constraints.maxHeight : 600,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('should create RecipesCubit successfully', (WidgetTester tester) async {
|
||||||
// Arrange & Act
|
// Arrange & Act
|
||||||
final cubit = MyRecipesCubit(myRecipesRepository: mockMyRecipesRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
final cubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(cubit, isA<MyRecipesCubit>());
|
expect(cubit, isA<RecipesCubit>());
|
||||||
expect(cubit.state, isA<MyRecipesStateLoading>());
|
expect(cubit.state.recipesList, isEmpty);
|
||||||
|
|
||||||
cubit.close();
|
cubit.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should display MyRecipesTab with all tabs', (WidgetTester tester) async {
|
testWidgets('should display RecipesWidget with recipe title', (WidgetTester tester) async {
|
||||||
// Arrange
|
await mockNetworkImagesFor(() async {
|
||||||
const widget = MyRecipesTab();
|
// Arrange
|
||||||
|
recipesCubit.emit(RecipesState.initial().copyWith(recipesList: [testRecipe], isLoading: false));
|
||||||
|
const widget = RecipesWidget(recipe: testRecipe, index: 0);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await tester.pumpWidget(createTestWidget(widget));
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(find.byType(MyRecipesTab), findsOneWidget);
|
expect(find.text('Test Recipe'), findsOneWidget);
|
||||||
expect(find.byType(AnimatedOptionButton), findsNWidgets(2));
|
expect(find.byType(RecipesWidget), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should handle tab selection in MyRecipesTab', (WidgetTester tester) async {
|
testWidgets('should display RecipesWidget with description when description is present', (WidgetTester tester) async {
|
||||||
// Arrange
|
await mockNetworkImagesFor(() async {
|
||||||
bool callbackCalled = false;
|
// Arrange
|
||||||
final widget = MyRecipesTab(
|
recipesCubit.emit(RecipesState.initial().copyWith(recipesList: [testRecipe], isLoading: false));
|
||||||
additionalOnTap: () {
|
const widget = RecipesWidget(recipe: testRecipe, index: 0);
|
||||||
callbackCalled = true;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpWidget(createTestWidget(widget));
|
// Act
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.text('Test Recipe'), findsOneWidget);
|
||||||
|
expect(find.byType(RecipesWidget), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display RecipesWidget structure correctly', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
recipesCubit.emit(RecipesState.initial().copyWith(recipesList: [testRecipe], isLoading: false));
|
||||||
|
const widget = RecipesWidget(recipe: testRecipe, index: 0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(RecipesWidget), findsOneWidget);
|
||||||
|
expect(find.text('Test Recipe'), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display Recipes widget with loading state', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
recipesCubit.emit(RecipesState.initial());
|
||||||
|
final widget = Recipes(handleVisibility: ({required visibleFraction, required id, required type}) {});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
final tabButtons = find.byType(AnimatedOptionButton);
|
await tester.pumpWidget(createRecipesTestWidget(widget));
|
||||||
expect(tabButtons, findsNWidgets(2));
|
|
||||||
|
|
||||||
await tester.tap(tabButtons.at(0));
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(callbackCalled, true);
|
expect(find.byType(Recipes), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should display MyRecipeWidget with recipe title', (WidgetTester tester) async {
|
testWidgets('should display Recipes widget with loaded state', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
recipesCubit.emit(RecipesState.initial().copyWith(recipesList: [testRecipe], isLoading: false));
|
||||||
|
final widget = Recipes(handleVisibility: ({required visibleFraction, required id, required type}) {});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createRecipesTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(Recipes), findsOneWidget);
|
||||||
|
expect(find.byType(RefreshIndicator), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display Recipes widget with error state', (WidgetTester tester) async {
|
||||||
// Arrange
|
// Arrange
|
||||||
myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active'));
|
recipesCubit.emit(RecipesState.initial().copyWith(isLoading: false, errorMessage: 'Error message'));
|
||||||
const widget = MyRecipeWidget(recipe: testRecipe);
|
final widget = Recipes(handleVisibility: ({required visibleFraction, required id, required type}) {});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await tester.pumpWidget(createTestWidget(widget));
|
await tester.pumpWidget(createRecipesTestWidget(widget));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(find.text('Test Recipe'), findsOneWidget);
|
expect(find.byType(Recipes), findsOneWidget);
|
||||||
expect(find.byType(MyRecipeWidget), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should display MyRecipeWidget with description when description is present', (WidgetTester tester) async {
|
testWidgets('should display Recipes widget with multiple recipes', (WidgetTester tester) async {
|
||||||
// Arrange
|
await mockNetworkImagesFor(() async {
|
||||||
myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active'));
|
// Arrange
|
||||||
const widget = MyRecipeWidget(recipe: testRecipe);
|
final testRecipe2 = testRecipe.copyWith(id: 2, title: 'Test Recipe 2');
|
||||||
|
recipesCubit.emit(RecipesState.initial().copyWith(recipesList: [testRecipe, testRecipe2], isLoading: false));
|
||||||
|
final widget = Recipes(handleVisibility: ({required visibleFraction, required id, required type}) {});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await tester.pumpWidget(createTestWidget(widget));
|
await tester.pumpWidget(createRecipesTestWidget(widget));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(find.text('Test Recipe'), findsOneWidget);
|
expect(find.byType(Recipes), findsOneWidget);
|
||||||
expect(find.byType(MyRecipeWidget), findsOneWidget);
|
expect(find.byType(ListView), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
testWidgets('should display MyRecipeWidget buttons', (WidgetTester tester) async {
|
|
||||||
// Arrange
|
|
||||||
myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active'));
|
|
||||||
const widget = MyRecipeWidget(recipe: testRecipe);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await tester.pumpWidget(createTestWidget(widget));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(find.byType(MyRecipeWidget), findsOneWidget);
|
|
||||||
expect(find.byType(CustomOutlinedButton), findsWidgets);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('should display MyRecipeWidget with cooking time', (WidgetTester tester) async {
|
|
||||||
// Arrange
|
|
||||||
myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active'));
|
|
||||||
const widget = MyRecipeWidget(recipe: testRecipe);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await tester.pumpWidget(createTestWidget(widget));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(find.byType(MyRecipeWidget), findsOneWidget);
|
|
||||||
expect(find.text('Test Recipe'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('should display MyRecipeWidget with steps count', (WidgetTester tester) async {
|
|
||||||
// Arrange
|
|
||||||
myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active'));
|
|
||||||
const widget = MyRecipeWidget(recipe: testRecipe);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await tester.pumpWidget(createTestWidget(widget));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(find.byType(MyRecipeWidget), findsOneWidget);
|
|
||||||
expect(find.text('Test Recipe'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('should display MyRecipeWidget with calories', (WidgetTester tester) async {
|
|
||||||
// Arrange
|
|
||||||
myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active'));
|
|
||||||
const widget = MyRecipeWidget(recipe: testRecipe);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await tester.pumpWidget(createTestWidget(widget));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(find.byType(MyRecipeWidget), findsOneWidget);
|
|
||||||
expect(find.text('Test Recipe'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('should display MyRecipeWidget with null steps', (WidgetTester tester) async {
|
|
||||||
// Arrange
|
|
||||||
myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active'));
|
|
||||||
final recipeWithoutSteps = testRecipe.copyWith(steps: null);
|
|
||||||
final widget = MyRecipeWidget(recipe: recipeWithoutSteps);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await tester.pumpWidget(createTestWidget(widget));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(find.byType(MyRecipeWidget), findsOneWidget);
|
|
||||||
expect(find.text('Test Recipe'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('should display MyRecipeWidget structure correctly', (WidgetTester tester) async {
|
|
||||||
// Arrange
|
|
||||||
myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active'));
|
|
||||||
const widget = MyRecipeWidget(recipe: testRecipe);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await tester.pumpWidget(createTestWidget(widget));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(find.byType(MyRecipeWidget), findsOneWidget);
|
|
||||||
expect(find.text('Test Recipe'), findsOneWidget);
|
|
||||||
expect(find.byType(CustomOutlinedButton), findsWidgets);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('should display MyRecipesTab structure correctly', (WidgetTester tester) async {
|
|
||||||
// Arrange
|
|
||||||
const widget = MyRecipesTab();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await tester.pumpWidget(createTestWidget(widget));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(find.byType(MyRecipesTab), findsOneWidget);
|
|
||||||
expect(find.byType(AnimatedOptionButton), findsNWidgets(2));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
253
test/features/recipes/presentation/widgets/widgets_test.dart
Normal file
253
test/features/recipes/presentation/widgets/widgets_test.dart
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
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/feed/models/diets_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/recipes_model.dart' as recipes;
|
||||||
|
import 'package:eatvio_app/features/recipe/presentation/widgets/ingredients_widget.dart';
|
||||||
|
import 'package:eatvio_app/features/recipe/presentation/widgets/steps_widget.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_portal/flutter_portal.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
import 'package:network_image_mock/network_image_mock.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Recipes Widgets Tests', () {
|
||||||
|
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(Widget child) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeHelper().themeData(),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [Locale('ru')],
|
||||||
|
home: Portal(
|
||||||
|
child: Scaffold(body: SingleChildScrollView(child: child)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('IngredientsWidget', () {
|
||||||
|
testWidgets('should display IngredientsWidget with ingredients', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const recipe = recipes.Recipe(
|
||||||
|
id: 1,
|
||||||
|
image: 'test-image.jpg',
|
||||||
|
state: 'active',
|
||||||
|
cookingTime: 30,
|
||||||
|
activeCookingTime: 15,
|
||||||
|
ingredients: [
|
||||||
|
recipes.Ingredient(name: 'Flour', unit: 'g', portion: '200', productId: 1),
|
||||||
|
recipes.Ingredient(name: 'Sugar', unit: 'g', portion: '100', productId: 2),
|
||||||
|
],
|
||||||
|
description: 'Test recipe',
|
||||||
|
steps: [],
|
||||||
|
videoUrl: null,
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
title: 'Test Recipe',
|
||||||
|
type: 'main',
|
||||||
|
slug: 'test-recipe',
|
||||||
|
commentsCount: 0,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
calories: 300.0,
|
||||||
|
servings: 4,
|
||||||
|
likesCount: 0,
|
||||||
|
viewsCount: 0,
|
||||||
|
tags: [],
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
);
|
||||||
|
const widget = IngredientsWidget(recipe: recipe);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(IngredientsWidget), findsOneWidget);
|
||||||
|
expect(find.byType(ExpansionTile), findsOneWidget);
|
||||||
|
// The ingredients text may be formatted with units, so just check the widget structure
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display IngredientsWidget with empty ingredients', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const recipe = recipes.Recipe(
|
||||||
|
id: 1,
|
||||||
|
image: 'test-image.jpg',
|
||||||
|
state: 'active',
|
||||||
|
cookingTime: 30,
|
||||||
|
activeCookingTime: 15,
|
||||||
|
ingredients: [],
|
||||||
|
description: 'Test recipe',
|
||||||
|
steps: [],
|
||||||
|
videoUrl: null,
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
title: 'Test Recipe',
|
||||||
|
type: 'main',
|
||||||
|
slug: 'test-recipe',
|
||||||
|
commentsCount: 0,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
calories: 300.0,
|
||||||
|
servings: 4,
|
||||||
|
likesCount: 0,
|
||||||
|
viewsCount: 0,
|
||||||
|
tags: [],
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
);
|
||||||
|
const widget = IngredientsWidget(recipe: recipe);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(IngredientsWidget), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('StepsWidget', () {
|
||||||
|
testWidgets('should display StepsWidget with steps', (WidgetTester tester) async {
|
||||||
|
await mockNetworkImagesFor(() async {
|
||||||
|
// Arrange
|
||||||
|
const recipe = recipes.Recipe(
|
||||||
|
id: 1,
|
||||||
|
image: 'test-image.jpg',
|
||||||
|
state: 'active',
|
||||||
|
cookingTime: 30,
|
||||||
|
activeCookingTime: 15,
|
||||||
|
ingredients: [],
|
||||||
|
description: 'Test recipe',
|
||||||
|
steps: [
|
||||||
|
recipes.Step(image: 'step1.jpg', description: 'Step 1 description'),
|
||||||
|
recipes.Step(image: 'step2.jpg', description: 'Step 2 description'),
|
||||||
|
],
|
||||||
|
videoUrl: null,
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
title: 'Test Recipe',
|
||||||
|
type: 'main',
|
||||||
|
slug: 'test-recipe',
|
||||||
|
commentsCount: 0,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
calories: 300.0,
|
||||||
|
servings: 4,
|
||||||
|
likesCount: 0,
|
||||||
|
viewsCount: 0,
|
||||||
|
tags: [],
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
);
|
||||||
|
const widget = StepsWidget(recipe: recipe);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(StepsWidget), findsOneWidget);
|
||||||
|
expect(find.text('Step 1 description'), findsOneWidget);
|
||||||
|
expect(find.text('Step 2 description'), findsOneWidget);
|
||||||
|
});
|
||||||
|
await tester.pump(const Duration(milliseconds: 600));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display StepsWidget with steps without images', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const recipe = recipes.Recipe(
|
||||||
|
id: 1,
|
||||||
|
image: 'test-image.jpg',
|
||||||
|
state: 'active',
|
||||||
|
cookingTime: 30,
|
||||||
|
activeCookingTime: 15,
|
||||||
|
ingredients: [],
|
||||||
|
description: 'Test recipe',
|
||||||
|
steps: [
|
||||||
|
recipes.Step(image: '', description: 'Step 1 description'),
|
||||||
|
recipes.Step(image: '', description: 'Step 2 description'),
|
||||||
|
],
|
||||||
|
videoUrl: null,
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
title: 'Test Recipe',
|
||||||
|
type: 'main',
|
||||||
|
slug: 'test-recipe',
|
||||||
|
commentsCount: 0,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
calories: 300.0,
|
||||||
|
servings: 4,
|
||||||
|
likesCount: 0,
|
||||||
|
viewsCount: 0,
|
||||||
|
tags: [],
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
);
|
||||||
|
const widget = StepsWidget(recipe: recipe);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(StepsWidget), findsOneWidget);
|
||||||
|
expect(find.text('Step 1 description'), findsOneWidget);
|
||||||
|
expect(find.text('Step 2 description'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display StepsWidget with empty steps', (WidgetTester tester) async {
|
||||||
|
// Arrange
|
||||||
|
const recipe = recipes.Recipe(
|
||||||
|
id: 1,
|
||||||
|
image: 'test-image.jpg',
|
||||||
|
state: 'active',
|
||||||
|
cookingTime: 30,
|
||||||
|
activeCookingTime: 15,
|
||||||
|
ingredients: [],
|
||||||
|
description: 'Test recipe',
|
||||||
|
steps: [],
|
||||||
|
videoUrl: null,
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
title: 'Test Recipe',
|
||||||
|
type: 'main',
|
||||||
|
slug: 'test-recipe',
|
||||||
|
commentsCount: 0,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
calories: 300.0,
|
||||||
|
servings: 4,
|
||||||
|
likesCount: 0,
|
||||||
|
viewsCount: 0,
|
||||||
|
tags: [],
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
);
|
||||||
|
const widget = StepsWidget(recipe: recipe);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tester.pumpWidget(createTestWidget(widget));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(find.byType(StepsWidget), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||||
|
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
|
|
||||||
|
import 'about_recipe_repository_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<AboutRecipeService>()])
|
||||||
|
void main() {
|
||||||
|
group('AboutRecipeRepository', () {
|
||||||
|
late AboutRecipeRepository aboutRecipeRepository;
|
||||||
|
late MockAboutRecipeService mockAboutRecipeService;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockAboutRecipeService = MockAboutRecipeService();
|
||||||
|
aboutRecipeRepository = AboutRecipeRepository(recipeService: mockAboutRecipeService);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getRecipe returns Recipe by id', () async {
|
||||||
|
const id = '1';
|
||||||
|
const expected = Recipe(
|
||||||
|
id: 1,
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
state: 'active',
|
||||||
|
cookingTime: 30,
|
||||||
|
activeCookingTime: 15,
|
||||||
|
ingredients: [],
|
||||||
|
description: 'Test recipe description',
|
||||||
|
steps: [],
|
||||||
|
videoUrl: null,
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
title: 'Test Recipe',
|
||||||
|
type: 'main',
|
||||||
|
slug: 'test-recipe-slug',
|
||||||
|
commentsCount: 5,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
calories: 300.0,
|
||||||
|
servings: 4,
|
||||||
|
likesCount: 10,
|
||||||
|
viewsCount: 100,
|
||||||
|
tags: [],
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockAboutRecipeService.getRecipe(id)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await aboutRecipeRepository.getRecipe(id);
|
||||||
|
|
||||||
|
expect(result, isA<Recipe>());
|
||||||
|
expect(result.id, 1);
|
||||||
|
expect(result.title, 'Test Recipe');
|
||||||
|
verify(mockAboutRecipeService.getRecipe(id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likeRecipe likes recipe by slug', () async {
|
||||||
|
const slug = 'test-recipe-slug';
|
||||||
|
|
||||||
|
when(mockAboutRecipeService.likeRecipe(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await aboutRecipeRepository.likeRecipe(slug);
|
||||||
|
|
||||||
|
verify(mockAboutRecipeService.likeRecipe(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addToFavorite adds recipe to favorite by slug', () async {
|
||||||
|
const slug = 'test-recipe-slug';
|
||||||
|
|
||||||
|
when(mockAboutRecipeService.addToFavorite(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await aboutRecipeRepository.addToFavorite(slug);
|
||||||
|
|
||||||
|
verify(mockAboutRecipeService.addToFavorite(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeFromFavorite removes recipe from favorite by slug', () async {
|
||||||
|
const slug = 'test-recipe-slug';
|
||||||
|
|
||||||
|
when(mockAboutRecipeService.removeFromFavorite(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await aboutRecipeRepository.removeFromFavorite(slug);
|
||||||
|
|
||||||
|
verify(mockAboutRecipeService.removeFromFavorite(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteRecipe deletes recipe by slug', () async {
|
||||||
|
const slug = 'test-recipe-slug';
|
||||||
|
|
||||||
|
when(mockAboutRecipeService.deleteRecipe(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await aboutRecipeRepository.deleteRecipe(slug);
|
||||||
|
|
||||||
|
verify(mockAboutRecipeService.deleteRecipe(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getRecipe rethrows exception on error', () async {
|
||||||
|
const id = '1';
|
||||||
|
|
||||||
|
when(mockAboutRecipeService.getRecipe(id)).thenThrow(Exception('Network error'));
|
||||||
|
|
||||||
|
expect(() => aboutRecipeRepository.getRecipe(id), throwsException);
|
||||||
|
verify(mockAboutRecipeService.getRecipe(id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likeRecipe rethrows exception on error', () async {
|
||||||
|
const slug = 'test-recipe-slug';
|
||||||
|
|
||||||
|
when(mockAboutRecipeService.likeRecipe(slug: slug)).thenThrow(Exception('Network error'));
|
||||||
|
|
||||||
|
expect(() => aboutRecipeRepository.likeRecipe(slug), throwsException);
|
||||||
|
verify(mockAboutRecipeService.likeRecipe(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||||
|
import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
|
|
||||||
|
import 'about_recipe_service_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<AboutRecipeService>()])
|
||||||
|
void main() {
|
||||||
|
group('AboutRecipeService', () {
|
||||||
|
late MockAboutRecipeService mockAboutRecipeService;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockAboutRecipeService = MockAboutRecipeService();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('likeRecipe likes recipe by slug', () async {
|
||||||
|
const slug = 'test-recipe-slug';
|
||||||
|
|
||||||
|
when(mockAboutRecipeService.likeRecipe(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await mockAboutRecipeService.likeRecipe(slug: slug);
|
||||||
|
|
||||||
|
verify(mockAboutRecipeService.likeRecipe(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addToFavorite adds recipe to favorite by slug', () async {
|
||||||
|
const slug = 'test-recipe-slug';
|
||||||
|
|
||||||
|
when(mockAboutRecipeService.addToFavorite(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await mockAboutRecipeService.addToFavorite(slug: slug);
|
||||||
|
|
||||||
|
verify(mockAboutRecipeService.addToFavorite(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeFromFavorite removes recipe from favorite by slug', () async {
|
||||||
|
const slug = 'test-recipe-slug';
|
||||||
|
|
||||||
|
when(mockAboutRecipeService.removeFromFavorite(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await mockAboutRecipeService.removeFromFavorite(slug: slug);
|
||||||
|
|
||||||
|
verify(mockAboutRecipeService.removeFromFavorite(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getRecipe returns Recipe by id', () async {
|
||||||
|
const id = '1';
|
||||||
|
const expected = Recipe(
|
||||||
|
id: 1,
|
||||||
|
image: 'https://someurl.com/img.jpg',
|
||||||
|
state: 'active',
|
||||||
|
cookingTime: 30,
|
||||||
|
activeCookingTime: 15,
|
||||||
|
ingredients: [],
|
||||||
|
description: 'Test recipe description',
|
||||||
|
steps: [],
|
||||||
|
videoUrl: null,
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
title: 'Test Recipe',
|
||||||
|
type: 'main',
|
||||||
|
slug: 'test-recipe-slug',
|
||||||
|
commentsCount: 5,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
isFavorite: false,
|
||||||
|
calories: 300.0,
|
||||||
|
servings: 4,
|
||||||
|
likesCount: 10,
|
||||||
|
viewsCount: 100,
|
||||||
|
tags: [],
|
||||||
|
author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||||
|
);
|
||||||
|
|
||||||
|
when(mockAboutRecipeService.getRecipe(id)).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await mockAboutRecipeService.getRecipe(id);
|
||||||
|
|
||||||
|
expect(result, isA<Recipe>());
|
||||||
|
expect(result.id, 1);
|
||||||
|
expect(result.title, 'Test Recipe');
|
||||||
|
verify(mockAboutRecipeService.getRecipe(id)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteRecipe deletes recipe by slug', () async {
|
||||||
|
const slug = 'test-recipe-slug';
|
||||||
|
|
||||||
|
when(mockAboutRecipeService.deleteRecipe(slug: slug)).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
|
await mockAboutRecipeService.deleteRecipe(slug: slug);
|
||||||
|
|
||||||
|
verify(mockAboutRecipeService.deleteRecipe(slug: slug)).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user