Compare commits

...

8 Commits

31 changed files with 5439 additions and 30739 deletions

View File

@@ -7,4 +7,7 @@ run-analyse:
run-checks: run-analyse run-tests
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

File diff suppressed because it is too large Load Diff

View File

@@ -86,6 +86,7 @@ dev_dependencies:
json_serializable: ^6.10.0
retrofit_generator: ^10.0.2
mockito: ^5.5.0
network_image_mock: ^2.1.1
flutter_test:
sdk: flutter

View 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"

View 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);
});
});
}

View 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));
});
});
}

View 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);
});
});
});
}

View 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);
});
});
}

View 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);
});
});
}

View File

@@ -21,6 +21,8 @@ import 'package:go_router/go_router.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.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';
@@ -29,12 +31,13 @@ void main() {
group('Feed Basic Widget Tests', () {
late MockFeedRepository mockFeedRepository;
late FeedCubit feedCubit;
VisibilityDetectorController.instance.updateInterval = Duration.zero;
final testBlogPost = FeedBlogPostModel(
id: 1,
title: 'Test Post',
description: 'Test description',
image: 'base64image', // Empty to avoid network calls in tests
image: '', // Empty - CachedNetworkImage will show errorWidget (SizedBox.shrink)
state: 'published',
author: const Author(authorSlug: 'author-1', authorName: 'Author Name', authorProfileIcon: '', authorIsMe: false),
videoUrl: '',
@@ -50,7 +53,7 @@ void main() {
id: 2,
title: 'Test Post 2',
description: 'Test description 2',
image: 'base64image', // Empty to avoid network calls in tests
image: '', // Empty - CachedNetworkImage will show errorWidget (SizedBox.shrink)
state: 'published',
author: const Author(authorSlug: 'author-2', authorName: 'Author Name 2', authorProfileIcon: '', authorIsMe: false),
videoUrl: '',
@@ -251,26 +254,30 @@ void main() {
});
testWidgets('should display BlogPosts widget with loaded state', (WidgetTester tester) async {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost, testBlogPost2], isLoading: false));
await mockNetworkImagesFor(() async {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost, testBlogPost2], isLoading: false));
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
// Use pump() multiple times to allow error widgets to render without waiting for network
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
await tester.pump(const Duration(milliseconds: 50));
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
// Pump frames to allow widgets to render
// Note: VisibilityDetector creates a 500ms timer, but we don't need to wait for it
// to complete to verify the widget structure
await tester.pump();
// Assert - Verify state and widget structure
expect(feedCubit.state, isA<FeedStateLoaded>());
final loadedState = feedCubit.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.length, 2);
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(RefreshIndicator), findsOneWidget);
// ListView should be present in loaded state
expect(find.byType(ListView), findsOneWidget);
// Assert - Verify state and widget structure
expect(feedCubit.state, isA<FeedStateLoaded>());
final loadedState = feedCubit.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.length, 2);
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(RefreshIndicator), findsOneWidget);
// ListView should be present in loaded state
expect(find.byType(ListView), findsOneWidget);
});
// Pump one more time after mockNetworkImagesFor to allow any pending timers to complete
await tester.pump(const Duration(milliseconds: 600));
});
testWidgets('should display BlogPosts widget with error state', (WidgetTester tester) async {
@@ -282,7 +289,6 @@ void main() {
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
// Assert
expect(feedCubit.state, isA<FeedStateError>());
@@ -300,7 +306,8 @@ void main() {
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
// Assert
expect(feedCubit.state, isA<FeedStateLoaded>());
@@ -311,22 +318,23 @@ void main() {
});
testWidgets('should display BlogPosts with loading indicator when isLoading is true', (WidgetTester tester) async {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: true));
await mockNetworkImagesFor(() async {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: true));
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
// Assert
expect(feedCubit.state, isA<FeedStateLoaded>());
final loadedState = feedCubit.state as FeedStateLoaded;
expect(loadedState.isLoading, true);
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
// Assert
expect(feedCubit.state, isA<FeedStateLoaded>());
final loadedState = feedCubit.state as FeedStateLoaded;
expect(loadedState.isLoading, true);
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
});
});
testWidgets('should handle chip selection with multiple chips', (WidgetTester tester) async {
@@ -366,28 +374,29 @@ void main() {
});
testWidgets('should handle BlogPosts refresh', (WidgetTester tester) async {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false));
await mockNetworkImagesFor(() async {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false));
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer(
(_) async => FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost, testBlogPost2],
meta: const Meta(currentPage: 1, lastPage: 1, total: 2, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
),
);
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer(
(_) async => FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost, testBlogPost2],
meta: const Meta(currentPage: 1, lastPage: 1, total: 2, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
),
);
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pumpAndSettle();
// Act - Verify refresh indicator exists
expect(find.byType(RefreshIndicator), findsOneWidget);
// Act - Verify refresh indicator exists
expect(find.byType(RefreshIndicator), findsOneWidget);
// Assert
expect(feedCubit.state, isA<FeedStateLoaded>());
expect(find.byType(ListView), findsOneWidget);
// Assert
expect(feedCubit.state, isA<FeedStateLoaded>());
expect(find.byType(ListView), findsOneWidget);
});
});
testWidgets('should display ChipGroup with correct selected chip', (WidgetTester tester) async {
@@ -406,40 +415,42 @@ void main() {
});
testWidgets('should handle BlogPosts visibility detection', (WidgetTester tester) async {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false));
await mockNetworkImagesFor(() async {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false));
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
// Assert
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(RefreshIndicator), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
// Visibility detection is handled by VisibilityDetector internally
// Assert
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(RefreshIndicator), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
// Visibility detection is handled by VisibilityDetector internally
});
});
testWidgets('should display BlogPosts with multiple posts', (WidgetTester tester) async {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost, testBlogPost2], isLoading: false));
await mockNetworkImagesFor(() async {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost, testBlogPost2], isLoading: false));
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
// Assert
expect(feedCubit.state, isA<FeedStateLoaded>());
final loadedState = feedCubit.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.length, 2);
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
// Assert
expect(feedCubit.state, isA<FeedStateLoaded>());
final loadedState = feedCubit.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.length, 2);
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
});
});
testWidgets('should handle ChipGroup with empty label list', (WidgetTester tester) async {
@@ -456,21 +467,22 @@ void main() {
});
testWidgets('should display BlogPosts structure correctly', (WidgetTester tester) async {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false));
await mockNetworkImagesFor(() async {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false));
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
// Assert
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(RefreshIndicator), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
expect(feedCubit.state, isA<FeedStateLoaded>());
// Assert
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(RefreshIndicator), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
expect(feedCubit.state, isA<FeedStateLoaded>());
});
});
testWidgets('should handle chip selection callback', (WidgetTester tester) async {
@@ -497,24 +509,25 @@ void main() {
});
testWidgets('should display BlogPosts with scrollable list', (WidgetTester tester) async {
// Arrange
final posts = List.generate(10, (index) => testBlogPost.copyWith(id: index + 1));
feedCubit.emit(FeedState.loaded(feedBlogPostList: posts, isLoading: false));
await mockNetworkImagesFor(() async {
// Arrange
final posts = List.generate(10, (index) => testBlogPost.copyWith(id: index + 1));
feedCubit.emit(FeedState.loaded(feedBlogPostList: posts, isLoading: false));
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
// Act
await tester.pumpWidget(createBlogPostsTestWidget(widget));
await tester.pump();
// Assert
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(RefreshIndicator), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
expect(feedCubit.state, isA<FeedStateLoaded>());
final loadedState = feedCubit.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.length, 10);
// Assert
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(RefreshIndicator), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
expect(feedCubit.state, isA<FeedStateLoaded>());
final loadedState = feedCubit.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.length, 10);
});
});
});
}

View File

@@ -0,0 +1,338 @@
import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'my_diets_cubit_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<MyDietsRepository>(),
MockSpec<CreateDietRepository>(),
])
void main() {
group('MyDietsCubit', () {
MyDietsCubit? myDietsCubit;
late MockMyDietsRepository mockMyDietsRepository;
late MockCreateDietRepository mockCreateDietRepository;
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(() {
mockMyDietsRepository = MockMyDietsRepository();
mockCreateDietRepository = MockCreateDietRepository();
});
tearDown(() async {
if (myDietsCubit != null && !myDietsCubit!.isClosed) {
await Future.delayed(const Duration(milliseconds: 100));
myDietsCubit!.close();
myDietsCubit = null;
}
});
test('начальное состояние - empty state', () {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
expect(myDietsCubit!.state.activeDiets, isEmpty);
expect(myDietsCubit!.state.draftDiets, isEmpty);
expect(myDietsCubit!.state.personalDiets, isEmpty);
expect(myDietsCubit!.state.isLoading, false);
expect(myDietsCubit!.state.selectedStateIndex, 0);
});
test('loadAllDiets - загружает все диеты', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
when(mockMyDietsRepository.getDiets(
page: 1,
query: null,
state: 'active',
types: null,
duration: null,
sorting: null,
)).thenAnswer((_) async => testDietsPagination);
when(mockMyDietsRepository.getDiets(
page: 1,
query: null,
state: 'drafted',
types: null,
duration: null,
sorting: null,
)).thenAnswer((_) async => testDietsPagination);
when(mockMyDietsRepository.getDiets(
page: 1,
query: null,
state: 'personal',
types: null,
duration: null,
sorting: null,
)).thenAnswer((_) async => testDietsPagination);
await myDietsCubit!.loadAllDiets();
await Future.delayed(const Duration(milliseconds: 200));
expect(myDietsCubit!.state.activeDiets.length, 1);
expect(myDietsCubit!.state.draftDiets.length, 1);
expect(myDietsCubit!.state.personalDiets.length, 1);
expect(myDietsCubit!.state.isLoading, false);
});
test('loadCurrentDiets - загружает активные диеты', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
when(mockMyDietsRepository.getDiets(
page: 1,
query: null,
state: 'active',
types: null,
duration: null,
sorting: null,
)).thenAnswer((_) async => testDietsPagination);
await myDietsCubit!.loadCurrentDiets();
await Future.delayed(const Duration(milliseconds: 200));
expect(myDietsCubit!.state.activeDiets.length, 1);
});
test('changeSelectedStateIndex - меняет выбранный индекс состояния', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
myDietsCubit!.changeSelectedStateIndex(1);
expect(myDietsCubit!.state.selectedStateIndex, 1);
// Wait for debounce to complete
await Future.delayed(const Duration(milliseconds: 1200));
myDietsCubit!.changeSelectedStateIndex(2);
expect(myDietsCubit!.state.selectedStateIndex, 2);
// Wait for debounce to complete before tearDown
await Future.delayed(const Duration(milliseconds: 1200));
});
test('changeTypeFilter - меняет фильтр типа для активных диет', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
when(mockMyDietsRepository.getDiets(
page: 1,
query: null,
state: 'active',
types: 'light',
duration: null,
sorting: null,
)).thenAnswer((_) async => testDietsPagination);
myDietsCubit!.changeTypeFilter(1);
await Future.delayed(const Duration(milliseconds: 1200));
expect(myDietsCubit!.state.activeFilterState.typeIndex, 1);
});
test('changeSortingType - меняет тип сортировки', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
when(mockMyDietsRepository.getDiets(
page: 1,
query: null,
state: 'active',
types: null,
duration: null,
sorting: 'chosen_diets_count:desc',
)).thenAnswer((_) async => testDietsPagination);
myDietsCubit!.changeSortingType(1);
await Future.delayed(const Duration(milliseconds: 1200));
expect(myDietsCubit!.state.activeFilterState.sortingIndex, 1);
});
test('changeDurationFilter - меняет фильтр длительности', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
when(mockMyDietsRepository.getDiets(
page: 1,
query: null,
state: 'active',
types: null,
duration: 7,
sorting: null,
)).thenAnswer((_) async => testDietsPagination);
myDietsCubit!.changeDurationFilter(1);
await Future.delayed(const Duration(milliseconds: 1200));
expect(myDietsCubit!.state.activeFilterState.durationIndex, 1);
});
test('deleteDiet - удаляет диету успешно', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
when(mockMyDietsRepository.deleteDiet(1)).thenAnswer((_) async => Future.value());
final result = await myDietsCubit!.deleteDiet(1);
expect(result, true);
verify(mockMyDietsRepository.deleteDiet(1)).called(1);
});
test('deleteDiet - обрабатывает ошибку при удалении', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
when(mockMyDietsRepository.deleteDiet(1)).thenThrow(Exception('Network error'));
final result = await myDietsCubit!.deleteDiet(1);
expect(result, false);
expect(myDietsCubit!.state.error, isNotNull);
});
test('publishDiet - публикует диету успешно', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
when(mockCreateDietRepository.updateDiet(
dietId: 1,
state: DietState.active,
)).thenAnswer((_) async => Future.value());
final result = await myDietsCubit!.publishDiet(1);
expect(result, true);
verify(mockCreateDietRepository.updateDiet(
dietId: 1,
state: DietState.active,
)).called(1);
});
test('publishDiet - обрабатывает ошибку при публикации', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
when(mockCreateDietRepository.updateDiet(
dietId: 1,
state: DietState.active,
)).thenThrow(Exception('Network error'));
final result = await myDietsCubit!.publishDiet(1);
expect(result, false);
});
test('unpublishDiet - снимает с публикации диету успешно', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
when(mockCreateDietRepository.updateDiet(
dietId: 1,
state: DietState.draft,
)).thenAnswer((_) async => Future.value());
final result = await myDietsCubit!.unpublishDiet(1);
expect(result, true);
verify(mockCreateDietRepository.updateDiet(
dietId: 1,
state: DietState.draft,
)).called(1);
});
test('unpublishDiet - обрабатывает ошибку при снятии с публикации', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
when(mockCreateDietRepository.updateDiet(
dietId: 1,
state: DietState.draft,
)).thenThrow(Exception('Network error'));
final result = await myDietsCubit!.unpublishDiet(1);
expect(result, false);
});
test('_loadActiveDiets - обрабатывает ошибку', () async {
myDietsCubit = MyDietsCubit(
myDietsRepository: mockMyDietsRepository,
createDietRepository: mockCreateDietRepository,
);
when(mockMyDietsRepository.getDiets(
page: 1,
query: null,
state: 'active',
types: null,
duration: null,
sorting: null,
)).thenThrow(Exception('Network error'));
await myDietsCubit!.loadCurrentDiets();
await Future.delayed(const Duration(milliseconds: 200));
expect(myDietsCubit!.state.activeDietsError, isNotNull);
});
});
}

View File

@@ -0,0 +1,267 @@
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/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart';
import 'package:eatvio_app/features/my_diets/presentation/widgets/my_diet_widget.dart';
import 'package:eatvio_app/features/my_diets/presentation/widgets/my_diets_tab.dart';
import 'package:eatvio_app/features/my_diets/repositories/my_diets_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 'basic_widget_test.mocks.dart';
@GenerateNiceMocks([MockSpec<MyDietsRepository>(), MockSpec<CreateDietRepository>()])
void main() {
group('MyDiets Basic Widget Tests', () {
late MockMyDietsRepository mockMyDietsRepository;
late MockCreateDietRepository mockCreateDietRepository;
late MyDietsCubit myDietsCubit;
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: '', 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');
mockMyDietsRepository = MockMyDietsRepository();
mockCreateDietRepository = MockCreateDietRepository();
myDietsCubit = MyDietsCubit(myDietsRepository: mockMyDietsRepository, createDietRepository: mockCreateDietRepository);
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
tearDown(() async {
await Future.delayed(const Duration(milliseconds: 100));
if (!myDietsCubit.isClosed) {
myDietsCubit.close();
}
});
Widget createTestWidget(Widget child, {bool wrapInScaffold = true}) {
final widget = BlocProvider<MyDietsCubit>(
create: (context) => myDietsCubit,
child: Portal(
child: wrapInScaffold ? Scaffold(body: SingleChildScrollView(child: child)) : SizedBox(width: 800, height: 600, child: child),
),
);
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: widget,
);
}
testWidgets('should create MyDietsCubit successfully', (WidgetTester tester) async {
// Arrange & Act
final cubit = MyDietsCubit(myDietsRepository: mockMyDietsRepository, createDietRepository: mockCreateDietRepository);
// Assert
expect(cubit, isA<MyDietsCubit>());
expect(cubit.state.activeDiets, isEmpty);
cubit.close();
});
testWidgets('should display MyDietsTab with all tabs', (WidgetTester tester) async {
// Arrange
myDietsCubit.emit(MyDietsState(selectedStateIndex: 0));
const widget = MyDietsTab();
// Act
await tester.pumpWidget(createTestWidget(widget));
await tester.pumpAndSettle();
// Assert
expect(find.byType(MyDietsTab), findsOneWidget);
expect(find.byType(AnimatedOptionButton), findsNWidgets(3));
});
testWidgets('should handle tab selection in MyDietsTab', (WidgetTester tester) async {
// Arrange
myDietsCubit.emit(MyDietsState(selectedStateIndex: 0));
const widget = MyDietsTab();
await tester.pumpWidget(createTestWidget(widget));
await tester.pumpAndSettle();
// Act - Find and tap the second tab
final tabButtons = find.byType(AnimatedOptionButton);
expect(tabButtons, findsNWidgets(3));
// Tap the second tab (index 1)
await tester.tap(tabButtons.at(1));
await tester.pump();
await tester.pump(const Duration(milliseconds: 1200));
// Assert
expect(myDietsCubit.state.selectedStateIndex, 1);
});
testWidgets('should display MyDietWidget with diet name', (WidgetTester tester) async {
// Arrange
myDietsCubit.emit(MyDietsState(selectedStateIndex: 0));
const widget = MyDietWidget(diet: testDiet);
// 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.text('Test Diet'), findsOneWidget);
expect(find.byType(MyDietWidget), findsOneWidget);
});
testWidgets('should display MyDietWidget with description when details are present', (WidgetTester tester) async {
// Arrange
myDietsCubit.emit(MyDietsState(selectedStateIndex: 0));
const widget = MyDietWidget(diet: testDiet);
// 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.text('Test Diet'), findsOneWidget);
expect(find.byType(MyDietWidget), findsOneWidget);
});
testWidgets('should display MyDietWidget buttons for active state', (WidgetTester tester) async {
// Arrange
myDietsCubit.emit(MyDietsState(selectedStateIndex: 0));
const widget = MyDietWidget(diet: testDiet);
// 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(MyDietWidget), findsOneWidget);
expect(find.byType(CustomOutlinedButton), findsWidgets);
});
testWidgets('should display MyDietWidget buttons for draft state', (WidgetTester tester) async {
// Arrange
myDietsCubit.emit(MyDietsState(selectedStateIndex: 1));
const widget = MyDietWidget(diet: testDiet);
// 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(MyDietWidget), findsOneWidget);
expect(find.byType(CustomOutlinedButton), findsWidgets);
});
testWidgets('should display MyDietWidget buttons for personal state', (WidgetTester tester) async {
// Arrange
myDietsCubit.emit(MyDietsState(selectedStateIndex: 2));
const widget = MyDietWidget(diet: testDiet);
// 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(MyDietWidget), findsOneWidget);
expect(find.byType(CustomOutlinedButton), findsWidgets);
});
testWidgets('should display MyDietsTab with correct selected tab', (WidgetTester tester) async {
// Arrange
myDietsCubit.emit(MyDietsState(selectedStateIndex: 1));
const widget = MyDietsTab();
// Act
await tester.pumpWidget(createTestWidget(widget));
await tester.pumpAndSettle();
// Assert
expect(find.byType(MyDietsTab), findsOneWidget);
expect(find.byType(AnimatedOptionButton), findsNWidgets(3));
});
testWidgets('should handle MyDietsTab additionalOnTap callback', (WidgetTester tester) async {
// Arrange
bool callbackCalled = false;
myDietsCubit.emit(MyDietsState(selectedStateIndex: 0));
final widget = MyDietsTab(
additionalOnTap: () {
callbackCalled = true;
},
);
await tester.pumpWidget(createTestWidget(widget));
await tester.pumpAndSettle();
// Act
final tabButtons = find.byType(AnimatedOptionButton);
await tester.tap(tabButtons.at(0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 1200));
// Assert
expect(callbackCalled, true);
});
testWidgets('should display MyDietWidget structure correctly', (WidgetTester tester) async {
// Arrange
myDietsCubit.emit(MyDietsState(selectedStateIndex: 0));
const widget = MyDietWidget(diet: testDiet);
// 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(MyDietWidget), findsOneWidget);
expect(find.text('Test Diet'), findsOneWidget);
expect(find.byType(CustomOutlinedButton), findsWidgets);
});
});
}

View File

@@ -0,0 +1,213 @@
import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'my_diets_repository_test.mocks.dart';
@GenerateNiceMocks([MockSpec<MyDietsService>()])
void main() {
group('MyDietsRepository', () {
late MyDietsRepository myDietsRepository;
late MockMyDietsService mockMyDietsService;
setUp(() {
mockMyDietsService = MockMyDietsService();
myDietsRepository = MyDietsRepository(myDietsService: mockMyDietsService);
});
test('getDiets returns pagination with all parameters', () async {
const page = 1;
const query = 'test diet';
const state = 'active';
const types = 'light,veg';
const duration = 30;
const sorting = 'name';
const expected = DietsPagination(
data: [
Diet(
id: 1,
slug: 'test-diet',
name: 'Test Diet',
image: 'https://someurl.com/img.jpg',
duration: duration,
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: [],
),
],
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),
);
when(mockMyDietsService.getDiets(
page: page,
query: query,
state: state,
types: types,
duration: duration,
sorting: sorting,
)).thenAnswer((_) async => expected);
final result = await myDietsRepository.getDiets(
page: page,
query: query,
state: state,
types: types,
duration: duration,
sorting: sorting,
);
expect(result, isA<DietsPagination>());
expect(result.data.length, 1);
expect(result.data.first.name, 'Test Diet');
verify(mockMyDietsService.getDiets(
page: page,
query: query,
state: state,
types: types,
duration: duration,
sorting: sorting,
)).called(1);
});
test('getDiets returns pagination with default parameters', () async {
const expected = DietsPagination(
data: [],
links: Links(first: '', last: '', prev: null, next: null),
meta: Meta(currentPage: 1, lastPage: 1, total: 0, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockMyDietsService.getDiets(
page: 1,
query: null,
state: null,
types: null,
duration: null,
sorting: null,
)).thenAnswer((_) async => expected);
final result = await myDietsRepository.getDiets();
expect(result, isA<DietsPagination>());
expect(result.data.length, 0);
verify(mockMyDietsService.getDiets(
page: 1,
query: null,
state: null,
types: null,
duration: null,
sorting: null,
)).called(1);
});
test('getDiets returns pagination with partial parameters', () async {
const page = 2;
const query = 'wellness';
const duration = 21;
const expected = DietsPagination(
data: [
Diet(
id: 2,
slug: 'another-diet',
name: 'Another Diet',
image: 'https://someurl.com/img2.jpg',
duration: duration,
state: DietState.draft,
type: DietType(light: false, veg: true, carb: false, meat: false),
details: '',
dailySchedule: {},
author: Author(authorSlug: 'author-slug', authorName: 'Author Name', authorProfileIcon: '', authorIsMe: true),
totalLikes: 5,
totalViews: 10,
totalComments: 2,
wasLikedByMe: true,
isFavorite: true,
dailyNutrition: [],
),
],
links: Links(first: '', last: '', prev: null, next: null),
meta: Meta(currentPage: 2, lastPage: 2, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockMyDietsService.getDiets(
page: page,
query: query,
state: null,
types: null,
duration: duration,
sorting: null,
)).thenAnswer((_) async => expected);
final result = await myDietsRepository.getDiets(
page: page,
query: query,
duration: duration,
);
expect(result, isA<DietsPagination>());
expect(result.data.length, 1);
expect(result.data.first.id, 2);
verify(mockMyDietsService.getDiets(
page: page,
query: query,
state: null,
types: null,
duration: duration,
sorting: null,
)).called(1);
});
test('deleteDiet deletes diet by id', () async {
const dietId = 1;
when(mockMyDietsService.deleteDiet(dietId: dietId)).thenAnswer((_) async => Future.value());
await myDietsRepository.deleteDiet(dietId);
verify(mockMyDietsService.deleteDiet(dietId: dietId)).called(1);
});
test('deleteDiet rethrows exception on error', () async {
const dietId = 1;
when(mockMyDietsService.deleteDiet(dietId: dietId)).thenThrow(Exception('Network error'));
expect(() => myDietsRepository.deleteDiet(dietId), throwsException);
verify(mockMyDietsService.deleteDiet(dietId: dietId)).called(1);
});
test('getDiets rethrows exception on error', () async {
when(mockMyDietsService.getDiets(
page: 1,
query: null,
state: null,
types: null,
duration: null,
sorting: null,
)).thenThrow(Exception('Network error'));
expect(() => myDietsRepository.getDiets(), throwsException);
verify(mockMyDietsService.getDiets(
page: 1,
query: null,
state: null,
types: null,
duration: null,
sorting: null,
)).called(1);
});
});
}

View File

@@ -0,0 +1,220 @@
import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'my_diets_service_test.mocks.dart';
@GenerateNiceMocks([MockSpec<MyDietsService>()])
void main() {
group('MyDietsService', () {
late MockMyDietsService mockMyDietsService;
setUp(() {
mockMyDietsService = MockMyDietsService();
});
test('getDiets returns pagination with all parameters', () async {
const page = 1;
const query = 'test diet';
const state = 'active';
const types = 'light,veg';
const duration = 30;
const sorting = 'name';
const expected = DietsPagination(
data: [
Diet(
id: 1,
slug: 'test-diet',
name: 'Test Diet',
image: 'https://someurl.com/img.jpg',
duration: duration,
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: [],
),
],
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),
);
when(
mockMyDietsService.getDiets(page: page, query: query, state: state, types: types, duration: duration, sorting: sorting),
).thenAnswer((_) async => expected);
final result = await mockMyDietsService.getDiets(page: page, query: query, state: state, types: types, duration: duration, sorting: sorting);
expect(result, isA<DietsPagination>());
expect(result.data.length, 1);
expect(result.data.first.name, 'Test Diet');
verify(mockMyDietsService.getDiets(page: page, query: query, state: state, types: types, duration: duration, sorting: sorting)).called(1);
});
test('getDiets returns pagination with minimal parameters', () async {
const page = 1;
const expected = DietsPagination(
data: [
Diet(
id: 2,
slug: 'another-diet',
name: 'Another Diet',
image: 'https://someurl.com/img2.jpg',
duration: 14,
state: DietState.draft,
type: DietType(light: false, veg: true, carb: false, meat: false),
details: '',
dailySchedule: {},
author: Author(authorSlug: 'author-slug', authorName: 'Author Name', authorProfileIcon: '', authorIsMe: true),
totalLikes: 5,
totalViews: 10,
totalComments: 2,
wasLikedByMe: true,
isFavorite: true,
dailyNutrition: [],
),
],
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),
);
when(mockMyDietsService.getDiets(page: page, query: null, state: null, types: null, duration: null, sorting: null)).thenAnswer((_) async => expected);
final result = await mockMyDietsService.getDiets(page: page, query: null, state: null, types: null, duration: null, sorting: null);
expect(result, isA<DietsPagination>());
expect(result.data.length, 1);
expect(result.data.first.id, 2);
verify(mockMyDietsService.getDiets(page: page, query: null, state: null, types: null, duration: null, sorting: null)).called(1);
});
test('getDiets returns pagination with partial parameters', () async {
const page = 2;
const query = 'wellness';
const state = 'draft';
const duration = 21;
const expected = DietsPagination(
data: [],
links: Links(first: '', last: '', prev: null, next: null),
meta: Meta(currentPage: 2, lastPage: 2, total: 0, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(
mockMyDietsService.getDiets(page: page, query: query, state: state, types: null, duration: duration, sorting: null),
).thenAnswer((_) async => expected);
final result = await mockMyDietsService.getDiets(page: page, query: query, state: state, types: null, duration: duration, sorting: null);
expect(result, isA<DietsPagination>());
expect(result.data.length, 0);
expect(result.meta.currentPage, 2);
verify(mockMyDietsService.getDiets(page: page, query: query, state: state, types: null, duration: duration, sorting: null)).called(1);
});
test('getDiets returns pagination with state filter', () async {
const page = 1;
const state = 'active';
const expected = DietsPagination(
data: [
Diet(
id: 3,
slug: 'active-diet',
name: 'Active Diet',
image: 'https://someurl.com/img3.jpg',
duration: 7,
state: DietState.active,
type: DietType(light: true, veg: true, carb: false, meat: false),
details: 'Active diet description',
dailySchedule: {},
author: Author(authorSlug: 'author-2', authorName: 'Author 2', authorProfileIcon: '', authorIsMe: false),
totalLikes: 10,
totalViews: 20,
totalComments: 5,
wasLikedByMe: false,
isFavorite: false,
dailyNutrition: [],
),
],
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),
);
when(mockMyDietsService.getDiets(page: page, query: null, state: state, types: null, duration: null, sorting: null)).thenAnswer((_) async => expected);
final result = await mockMyDietsService.getDiets(page: page, query: null, state: state, types: null, duration: null, sorting: null);
expect(result, isA<DietsPagination>());
expect(result.data.length, 1);
expect(result.data.first.state, DietState.active);
verify(mockMyDietsService.getDiets(page: page, query: null, state: state, types: null, duration: null, sorting: null)).called(1);
});
test('getDiets returns pagination with sorting parameter', () async {
const page = 1;
const sorting = 'duration';
const expected = DietsPagination(
data: [
Diet(
id: 4,
slug: 'sorted-diet',
name: 'Sorted Diet',
image: 'https://someurl.com/img4.jpg',
duration: 60,
state: DietState.personal,
type: DietType(light: false, veg: false, carb: true, meat: true),
details: 'Sorted diet description',
dailySchedule: {},
author: Author(authorSlug: 'author-3', authorName: 'Author 3', authorProfileIcon: '', authorIsMe: false),
totalLikes: 15,
totalViews: 30,
totalComments: 8,
wasLikedByMe: true,
isFavorite: true,
dailyNutrition: [],
),
],
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),
);
when(mockMyDietsService.getDiets(page: page, query: null, state: null, types: null, duration: null, sorting: sorting)).thenAnswer((_) async => expected);
final result = await mockMyDietsService.getDiets(page: page, query: null, state: null, types: null, duration: null, sorting: sorting);
expect(result, isA<DietsPagination>());
expect(result.data.length, 1);
expect(result.data.first.duration, 60);
verify(mockMyDietsService.getDiets(page: page, query: null, state: null, types: null, duration: null, sorting: sorting)).called(1);
});
test('deleteDiet deletes diet by id', () async {
const dietId = 1;
when(mockMyDietsService.deleteDiet(dietId: dietId)).thenAnswer((_) async => Future.value());
await mockMyDietsService.deleteDiet(dietId: dietId);
verify(mockMyDietsService.deleteDiet(dietId: dietId)).called(1);
});
test('deleteDiet deletes diet with different id', () async {
const dietId = 42;
when(mockMyDietsService.deleteDiet(dietId: dietId)).thenAnswer((_) async => Future.value());
await mockMyDietsService.deleteDiet(dietId: dietId);
verify(mockMyDietsService.deleteDiet(dietId: dietId)).called(1);
});
});
}

View File

@@ -0,0 +1,256 @@
import 'package:eatvio_app/core/models/pagination_model.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/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'my_plans_cubit_test.mocks.dart';
@GenerateNiceMocks([MockSpec<MyPlansRepository>()])
void main() {
group('MyPlansCubit', () {
MyPlansCubit? myPlansCubit;
late MockMyPlansRepository mockMyPlansRepository;
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(() {
mockMyPlansRepository = MockMyPlansRepository();
});
tearDown(() async {
if (myPlansCubit != null && !myPlansCubit!.isClosed) {
await Future.delayed(const Duration(milliseconds: 100));
myPlansCubit!.close();
myPlansCubit = null;
}
});
test('начальное состояние - empty state', () {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
expect(myPlansCubit!.state.activePlans, isEmpty);
expect(myPlansCubit!.state.draftPlans, isEmpty);
expect(myPlansCubit!.state.isLoading, false);
expect(myPlansCubit!.state.selectedStateIndex, 0);
});
test('loadAllPlans - загружает все планы', () async {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
when(mockMyPlansRepository.getPlans(
page: 1,
query: null,
types: null,
duration: null,
minPrice: null,
maxPrice: null,
)).thenAnswer((_) async => testPlansPagination);
await myPlansCubit!.loadAllPlans();
await Future.delayed(const Duration(milliseconds: 200));
expect(myPlansCubit!.state.activePlans.length, 1);
expect(myPlansCubit!.state.draftPlans.length, 1);
expect(myPlansCubit!.state.isLoading, false);
});
test('loadPlans - загружает активные планы', () async {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
when(mockMyPlansRepository.getPlans(
page: 1,
query: null,
types: null,
duration: null,
minPrice: null,
maxPrice: null,
)).thenAnswer((_) async => testPlansPagination);
await myPlansCubit!.loadPlans();
await Future.delayed(const Duration(milliseconds: 200));
expect(myPlansCubit!.state.activePlans.length, 1);
});
test('changeSelectedStateIndex - меняет выбранный индекс состояния', () {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
myPlansCubit!.changeSelectedStateIndex(1);
expect(myPlansCubit!.state.selectedStateIndex, 1);
myPlansCubit!.changeSelectedStateIndex(0);
expect(myPlansCubit!.state.selectedStateIndex, 0);
});
test('changeTypeFilter - меняет фильтр типа для активных планов', () async {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
when(mockMyPlansRepository.getPlans(
page: 1,
query: null,
types: 'fit',
duration: null,
minPrice: null,
maxPrice: null,
)).thenAnswer((_) async => testPlansPagination);
myPlansCubit!.changeTypeFilter(1);
await Future.delayed(const Duration(milliseconds: 1200));
expect(myPlansCubit!.state.activeFilterState.typeIndex, 1);
});
test('changeDurationFilter - меняет фильтр длительности', () async {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
when(mockMyPlansRepository.getPlans(
page: 1,
query: null,
types: null,
duration: 7,
minPrice: null,
maxPrice: null,
)).thenAnswer((_) async => testPlansPagination);
myPlansCubit!.changeDurationFilter(1);
await Future.delayed(const Duration(milliseconds: 1200));
expect(myPlansCubit!.state.activeFilterState.durationIndex, 1);
});
test('changePriceFilter - меняет фильтр цены', () async {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
when(mockMyPlansRepository.getPlans(
page: 1,
query: null,
types: null,
duration: null,
minPrice: 100,
maxPrice: 500,
)).thenAnswer((_) async => testPlansPagination);
myPlansCubit!.changePriceFilter(100, 500);
await Future.delayed(const Duration(milliseconds: 1200));
expect(myPlansCubit!.state.activeFilterState.priceFilter.$1, 100);
expect(myPlansCubit!.state.activeFilterState.priceFilter.$2, 500);
});
test('deletePlan - удаляет план успешно', () async {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
when(mockMyPlansRepository.deletePlan('test-plan-slug')).thenAnswer((_) async => Future.value());
when(mockMyPlansRepository.getPlans(
page: 1,
query: null,
types: null,
duration: null,
minPrice: null,
maxPrice: null,
)).thenAnswer((_) async => testPlansPagination);
final result = await myPlansCubit!.deletePlan('test-plan-slug');
await Future.delayed(const Duration(milliseconds: 200));
expect(result, true);
verify(mockMyPlansRepository.deletePlan('test-plan-slug')).called(1);
});
test('deletePlan - обрабатывает ошибку при удалении', () async {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
when(mockMyPlansRepository.deletePlan('test-plan-slug')).thenThrow(Exception('Network error'));
final result = await myPlansCubit!.deletePlan('test-plan-slug');
expect(result, false);
expect(myPlansCubit!.state.error, isNotNull);
});
test('unpublish - снимает с публикации план успешно', () async {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
when(mockMyPlansRepository.unpublish('test-plan-slug')).thenAnswer((_) async => Future.value());
final result = await myPlansCubit!.unpublish('test-plan-slug');
expect(result, true);
verify(mockMyPlansRepository.unpublish('test-plan-slug')).called(1);
});
test('unpublish - обрабатывает ошибку при снятии с публикации', () async {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
when(mockMyPlansRepository.unpublish('test-plan-slug')).thenThrow(Exception('Network error'));
final result = await myPlansCubit!.unpublish('test-plan-slug');
expect(result, false);
});
test('_loadActivePlans - обрабатывает ошибку', () async {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
when(mockMyPlansRepository.getPlans(
page: 1,
query: null,
types: null,
duration: null,
minPrice: null,
maxPrice: null,
)).thenThrow(Exception('Network error'));
await myPlansCubit!.loadPlans();
await Future.delayed(const Duration(milliseconds: 200));
expect(myPlansCubit!.state.error, isNotNull);
});
test('_loadActivePlans - устанавливает ценовой фильтр из мета', () async {
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
when(mockMyPlansRepository.getPlans(
page: 1,
query: null,
types: null,
duration: null,
minPrice: null,
maxPrice: null,
)).thenAnswer((_) async => testPlansPagination);
await myPlansCubit!.loadPlans();
await Future.delayed(const Duration(milliseconds: 200));
expect(myPlansCubit!.state.minActivePlanPrice, 100);
expect(myPlansCubit!.state.maxActivePlanPrice, 500);
});
});
}

View File

@@ -0,0 +1,195 @@
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/custom_outlined_button.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/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart';
import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plan_widget.dart';
import 'package:eatvio_app/features/my_plans/repositories/my_diets_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 'basic_widget_test.mocks.dart';
@GenerateNiceMocks([MockSpec<MyPlansRepository>()])
void main() {
group('MyPlans Basic Widget Tests', () {
late MockMyPlansRepository mockMyPlansRepository;
late MyPlansCubit myPlansCubit;
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: '', authorIsMe: false),
totalLikes: 1,
totalViews: 1,
totalComments: 1,
isFavorite: false,
minPriceOfOptions: 100.0,
maxPriceOfOptions: 500.0,
wasLikedByMe: false,
packet1Options: null,
);
setUp(() async {
await sl.reset();
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
mockMyPlansRepository = MockMyPlansRepository();
myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
tearDown(() async {
await Future.delayed(const Duration(milliseconds: 100));
if (!myPlansCubit.isClosed) {
myPlansCubit.close();
}
});
Widget createTestWidget(Widget child, {bool wrapInScaffold = true}) {
final widget = BlocProvider<MyPlansCubit>(
create: (context) => myPlansCubit,
child: Portal(
child: wrapInScaffold ? Scaffold(body: SingleChildScrollView(child: child)) : SizedBox(width: 800, height: 600, child: child),
),
);
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: widget,
);
}
testWidgets('should create MyPlansCubit successfully', (WidgetTester tester) async {
// Arrange & Act
final cubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository);
// Assert
expect(cubit, isA<MyPlansCubit>());
expect(cubit.state.activePlans, isEmpty);
cubit.close();
});
testWidgets('should display MyPlanWidget with plan name', (WidgetTester tester) async {
// Arrange
myPlansCubit.emit(MyPlansState(selectedStateIndex: 0));
const widget = MyPlanWidget(plan: testPlan);
// 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.text('Test Plan'), findsOneWidget);
expect(find.byType(MyPlanWidget), findsOneWidget);
});
testWidgets('should display MyPlanWidget with description when details are present', (WidgetTester tester) async {
// Arrange
myPlansCubit.emit(MyPlansState(selectedStateIndex: 0));
const widget = MyPlanWidget(plan: testPlan);
// 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.text('Test Plan'), findsOneWidget);
expect(find.byType(MyPlanWidget), findsOneWidget);
});
testWidgets('should display MyPlanWidget with price', (WidgetTester tester) async {
// Arrange
myPlansCubit.emit(MyPlansState(selectedStateIndex: 0));
const widget = MyPlanWidget(plan: testPlan);
// 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.text('Test Plan'), findsOneWidget);
expect(find.byType(MyPlanWidget), findsOneWidget);
});
testWidgets('should display MyPlanWidget buttons', (WidgetTester tester) async {
// Arrange
myPlansCubit.emit(MyPlansState(selectedStateIndex: 0));
const widget = MyPlanWidget(plan: testPlan);
// 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(MyPlanWidget), findsOneWidget);
expect(find.byType(CustomOutlinedButton), findsWidgets);
});
testWidgets('should display MyPlanWidget structure correctly', (WidgetTester tester) async {
// Arrange
myPlansCubit.emit(MyPlansState(selectedStateIndex: 0));
const widget = MyPlanWidget(plan: testPlan);
// 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(MyPlanWidget), findsOneWidget);
expect(find.text('Test Plan'), findsOneWidget);
expect(find.byType(CustomOutlinedButton), findsWidgets);
});
testWidgets('should display MyPlanWidget with empty details', (WidgetTester tester) async {
// Arrange
myPlansCubit.emit(MyPlansState(selectedStateIndex: 0));
final planWithoutDetails = testPlan.copyWith(details: '');
final widget = MyPlanWidget(plan: planWithoutDetails);
// 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.text('Test Plan'), findsOneWidget);
expect(find.byType(MyPlanWidget), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,236 @@
import 'package:eatvio_app/core/models/pagination_model.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/my_plans/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'my_plans_repository_test.mocks.dart';
@GenerateNiceMocks([MockSpec<MyPlansService>()])
void main() {
group('MyPlansRepository', () {
late MyPlansRepository myPlansRepository;
late MockMyPlansService mockMyPlansService;
setUp(() {
mockMyPlansService = MockMyPlansService();
myPlansRepository = MyPlansRepository(myPlansService: mockMyPlansService);
});
test('getPlans returns pagination with all parameters', () async {
const page = 1;
const query = 'test plan';
const types = 'fit,slim';
const duration = 30;
const minPrice = 100;
const maxPrice = 500;
final expected = PlanPaginationModel(
data: [
PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://someurl.com/img.jpg',
duration: duration,
state: 'active',
type: const PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test plan description',
packetsCount: 1,
author: const Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
totalLikes: 1,
totalViews: 1,
totalComments: 1,
isFavorite: false,
minPriceOfOptions: minPrice.toDouble(),
maxPriceOfOptions: maxPrice.toDouble(),
wasLikedByMe: false,
),
],
links: const Links(first: '', last: '', prev: null, next: null),
meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockMyPlansService.getPlans(
page: page,
query: query,
types: types,
duration: duration,
minPrice: minPrice,
maxPrice: maxPrice,
)).thenAnswer((_) async => expected);
final result = await myPlansRepository.getPlans(
page: page,
query: query,
types: types,
duration: duration,
minPrice: minPrice,
maxPrice: maxPrice,
);
expect(result, isA<PlanPaginationModel>());
expect(result.data.length, 1);
expect(result.data.first.name, 'Test Plan');
verify(mockMyPlansService.getPlans(
page: page,
query: query,
types: types,
duration: duration,
minPrice: minPrice,
maxPrice: maxPrice,
)).called(1);
});
test('getPlans returns pagination with default parameters', () async {
const expected = PlanPaginationModel(
data: [
PlanModel(
id: 2,
slug: 'another-plan',
name: 'Another Plan',
image: 'https://someurl.com/img2.jpg',
duration: 14,
state: 'draft',
type: PlanType(fit: false, slim: true, behavior: false, wellness: false),
details: '',
packetsCount: 2,
author: Author(authorSlug: 'author-slug', authorName: 'Author Name', authorProfileIcon: '', authorIsMe: true),
totalLikes: 5,
totalViews: 10,
totalComments: 2,
isFavorite: true,
minPriceOfOptions: 200.0,
maxPriceOfOptions: 600.0,
wasLikedByMe: true,
),
],
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),
);
when(mockMyPlansService.getPlans(
page: 1,
query: null,
types: null,
duration: null,
minPrice: null,
maxPrice: null,
)).thenAnswer((_) async => expected);
final result = await myPlansRepository.getPlans();
expect(result, isA<PlanPaginationModel>());
expect(result.data.length, 1);
expect(result.data.first.id, 2);
verify(mockMyPlansService.getPlans(
page: 1,
query: null,
types: null,
duration: null,
minPrice: null,
maxPrice: null,
)).called(1);
});
test('getPlans returns pagination with partial parameters', () async {
const page = 2;
const query = 'wellness';
const duration = 21;
const expected = PlanPaginationModel(
data: [],
links: Links(first: '', last: '', prev: null, next: null),
meta: Meta(currentPage: 2, lastPage: 2, total: 0, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockMyPlansService.getPlans(
page: page,
query: query,
types: null,
duration: duration,
minPrice: null,
maxPrice: null,
)).thenAnswer((_) async => expected);
final result = await myPlansRepository.getPlans(
page: page,
query: query,
duration: duration,
);
expect(result, isA<PlanPaginationModel>());
expect(result.data.length, 0);
expect(result.meta.currentPage, 2);
verify(mockMyPlansService.getPlans(
page: page,
query: query,
types: null,
duration: duration,
minPrice: null,
maxPrice: null,
)).called(1);
});
test('deletePlan deletes plan by slug', () async {
const slug = 'test-plan-slug';
when(mockMyPlansService.deletePlan(slug: slug)).thenAnswer((_) async => Future.value());
await myPlansRepository.deletePlan(slug);
verify(mockMyPlansService.deletePlan(slug: slug)).called(1);
});
test('unpublish unpublishes plan by slug', () async {
const slug = 'draft-plan-slug';
when(mockMyPlansService.unpublish(slug: slug)).thenAnswer((_) async => Future.value());
await myPlansRepository.unpublish(slug);
verify(mockMyPlansService.unpublish(slug: slug)).called(1);
});
test('deletePlan rethrows exception on error', () async {
const slug = 'test-plan-slug';
when(mockMyPlansService.deletePlan(slug: slug)).thenThrow(Exception('Network error'));
expect(() => myPlansRepository.deletePlan(slug), throwsException);
verify(mockMyPlansService.deletePlan(slug: slug)).called(1);
});
test('unpublish rethrows exception on error', () async {
const slug = 'draft-plan-slug';
when(mockMyPlansService.unpublish(slug: slug)).thenThrow(Exception('Network error'));
expect(() => myPlansRepository.unpublish(slug), throwsException);
verify(mockMyPlansService.unpublish(slug: slug)).called(1);
});
test('getPlans rethrows exception on error', () async {
when(mockMyPlansService.getPlans(
page: 1,
query: null,
types: null,
duration: null,
minPrice: null,
maxPrice: null,
)).thenThrow(Exception('Network error'));
expect(() => myPlansRepository.getPlans(), throwsException);
verify(mockMyPlansService.getPlans(
page: 1,
query: null,
types: null,
duration: null,
minPrice: null,
maxPrice: null,
)).called(1);
});
});
}

View File

@@ -0,0 +1,165 @@
import 'package:eatvio_app/core/models/pagination_model.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/my_plans/services/my_plans_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'my_plans_service_test.mocks.dart';
@GenerateNiceMocks([MockSpec<MyPlansService>()])
void main() {
group('MyPlansService', () {
late MockMyPlansService mockMyPlansService;
setUp(() {
mockMyPlansService = MockMyPlansService();
});
test('getPlans returns pagination with all parameters', () async {
const page = 1;
const query = 'test plan';
const types = 'fit,slim';
const duration = 30;
const minPrice = 100;
const maxPrice = 500;
final expected = PlanPaginationModel(
data: [
PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://someurl.com/img.jpg',
duration: duration,
state: 'active',
type: const PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test plan description',
packetsCount: 1,
author: const Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
totalLikes: 1,
totalViews: 1,
totalComments: 1,
isFavorite: false,
minPriceOfOptions: minPrice.toDouble(),
maxPriceOfOptions: maxPrice.toDouble(),
wasLikedByMe: false,
),
],
links: const Links(first: '', last: '', prev: null, next: null),
meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(
mockMyPlansService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice),
).thenAnswer((_) async => expected);
final result = await mockMyPlansService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice);
expect(result, isA<PlanPaginationModel>());
expect(result.data.length, 1);
expect(result.data.first.name, 'Test Plan');
verify(mockMyPlansService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice)).called(1);
});
test('getPlans returns pagination with minimal parameters', () async {
const page = 1;
const expected = PlanPaginationModel(
data: [
PlanModel(
id: 2,
slug: 'another-plan',
name: 'Another Plan',
image: 'https://someurl.com/img2.jpg',
duration: 14,
state: 'draft',
type: PlanType(fit: false, slim: true, behavior: false, wellness: false),
details: '',
packetsCount: 2,
author: Author(authorSlug: 'author-slug', authorName: 'Author Name', authorProfileIcon: '', authorIsMe: true),
totalLikes: 5,
totalViews: 10,
totalComments: 2,
isFavorite: true,
minPriceOfOptions: 200.0,
maxPriceOfOptions: 600.0,
wasLikedByMe: true,
),
],
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),
);
when(mockMyPlansService.getPlans(page: page, query: null, types: null, duration: null, minPrice: null, maxPrice: null)).thenAnswer((_) async => expected);
final result = await mockMyPlansService.getPlans(page: page, query: null, types: null, duration: null, minPrice: null, maxPrice: null);
expect(result, isA<PlanPaginationModel>());
expect(result.data.length, 1);
expect(result.data.first.id, 2);
verify(mockMyPlansService.getPlans(page: page, query: null, types: null, duration: null, minPrice: null, maxPrice: null)).called(1);
});
test('getPlans returns pagination with partial parameters', () async {
const page = 2;
const query = 'wellness';
const duration = 21;
const expected = PlanPaginationModel(
data: [],
links: Links(first: '', last: '', prev: null, next: null),
meta: Meta(currentPage: 2, lastPage: 2, total: 0, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(
mockMyPlansService.getPlans(page: page, query: query, types: null, duration: duration, minPrice: null, maxPrice: null),
).thenAnswer((_) async => expected);
final result = await mockMyPlansService.getPlans(page: page, query: query, types: null, duration: duration, minPrice: null, maxPrice: null);
expect(result, isA<PlanPaginationModel>());
expect(result.data.length, 0);
expect(result.meta.currentPage, 2);
verify(mockMyPlansService.getPlans(page: page, query: query, types: null, duration: duration, minPrice: null, maxPrice: null)).called(1);
});
test('deletePlan deletes plan by slug', () async {
const slug = 'test-plan-slug';
when(mockMyPlansService.deletePlan(slug: slug)).thenAnswer((_) async => Future.value());
await mockMyPlansService.deletePlan(slug: slug);
verify(mockMyPlansService.deletePlan(slug: slug)).called(1);
});
test('deletePlan deletes plan with different slug', () async {
const slug = 'another-plan-slug';
when(mockMyPlansService.deletePlan(slug: slug)).thenAnswer((_) async => Future.value());
await mockMyPlansService.deletePlan(slug: slug);
verify(mockMyPlansService.deletePlan(slug: slug)).called(1);
});
test('unpublish unpublishes plan by slug', () async {
const slug = 'draft-plan-slug';
when(mockMyPlansService.unpublish(slug: slug)).thenAnswer((_) async => Future.value());
await mockMyPlansService.unpublish(slug: slug);
verify(mockMyPlansService.unpublish(slug: slug)).called(1);
});
test('unpublish unpublishes plan with different slug', () async {
const slug = 'published-plan-slug';
when(mockMyPlansService.unpublish(slug: slug)).thenAnswer((_) async => Future.value());
await mockMyPlansService.unpublish(slug: slug);
verify(mockMyPlansService.unpublish(slug: slug)).called(1);
});
});
}

View 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);
});
});
}

View 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));
});
});
}

View 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);
});
});
});
}

View 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);
});
});
}

View 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);
});
});
}

View File

@@ -0,0 +1,244 @@
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.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:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'my_recipes_cubit_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<MyRecipesRepository>(),
MockSpec<AboutRecipeRepository>(),
])
void main() {
group('MyRecipesCubit', () {
MyRecipesCubit? myRecipesCubit;
late MockMyRecipesRepository mockMyRecipesRepository;
late MockAboutRecipeRepository mockAboutRecipeRepository;
const testActiveRecipe = MyRecipeModel(
id: 1,
image: 'https://someurl.com/img.jpg',
state: 'active',
cookingTime: 30,
activeCookingTime: 20,
description: 'Test recipe description',
steps: [
Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1 description'),
],
videoUrl: 'https://someurl.com/video.mp4',
title: 'Test Recipe',
slug: 'test-recipe',
calories: 250.0,
likesCount: 5,
viewsCount: 10,
);
const testArchivedRecipe = MyRecipeModel(
id: 2,
image: 'https://someurl.com/img2.jpg',
state: 'archived',
cookingTime: 45,
activeCookingTime: 30,
description: 'Archived recipe description',
steps: null,
videoUrl: null,
title: 'Archived Recipe',
slug: 'archived-recipe',
calories: 350.0,
likesCount: 8,
viewsCount: 15,
);
final testRecipe = Recipe(
id: 1,
image: 'https://someurl.com/img.jpg',
state: 'published',
cookingTime: 30,
activeCookingTime: 20,
ingredients: null,
description: 'Test recipe description',
steps: [
const Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1 description'),
],
videoUrl: 'https://someurl.com/video.mp4',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
title: 'Test Recipe',
type: 'recipe',
slug: 'test-recipe',
commentsCount: 5,
wasLikedByMe: false,
isFavorite: false,
calories: 250.0,
servings: 1,
likesCount: 5,
viewsCount: 10,
tags: [],
author: const Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
);
setUp(() {
mockMyRecipesRepository = MockMyRecipesRepository();
mockAboutRecipeRepository = MockAboutRecipeRepository();
});
tearDown(() async {
if (myRecipesCubit != null && !myRecipesCubit!.isClosed) {
await Future.delayed(const Duration(milliseconds: 100));
myRecipesCubit!.close();
myRecipesCubit = null;
}
});
test('начальное состояние - loading', () {
myRecipesCubit = MyRecipesCubit(
myRecipesRepository: mockMyRecipesRepository,
aboutRecipeRepository: mockAboutRecipeRepository,
);
expect(myRecipesCubit!.state, isA<MyRecipesStateLoading>());
});
test('getMyRecipes - загружает активные рецепты', () async {
myRecipesCubit = MyRecipesCubit(
myRecipesRepository: mockMyRecipesRepository,
aboutRecipeRepository: mockAboutRecipeRepository,
);
when(mockMyRecipesRepository.getMyRecipes()).thenAnswer(
(_) async => [testActiveRecipe, testArchivedRecipe],
);
await myRecipesCubit!.getMyRecipes();
await Future.delayed(const Duration(milliseconds: 100));
expect(myRecipesCubit!.state, isA<MyRecipesStateLoaded>());
final loadedState = myRecipesCubit!.state as MyRecipesStateLoaded;
expect(loadedState.recipes.length, 1);
expect(loadedState.recipes.first.state, 'active');
expect(loadedState.selectedState, 'active');
});
test('getMyRecipes - обрабатывает ошибку', () async {
myRecipesCubit = MyRecipesCubit(
myRecipesRepository: mockMyRecipesRepository,
aboutRecipeRepository: mockAboutRecipeRepository,
);
when(mockMyRecipesRepository.getMyRecipes()).thenThrow(Exception('Network error'));
await myRecipesCubit!.getMyRecipes();
await Future.delayed(const Duration(milliseconds: 100));
expect(myRecipesCubit!.state, isA<MyRecipesStateError>());
});
test('setActiveState - устанавливает активное состояние', () async {
myRecipesCubit = MyRecipesCubit(
myRecipesRepository: mockMyRecipesRepository,
aboutRecipeRepository: mockAboutRecipeRepository,
);
when(mockMyRecipesRepository.getMyRecipes()).thenAnswer(
(_) async => [testActiveRecipe, testArchivedRecipe],
);
await myRecipesCubit!.setActiveState();
await Future.delayed(const Duration(milliseconds: 100));
expect(myRecipesCubit!.state, isA<MyRecipesStateLoaded>());
final loadedState = myRecipesCubit!.state as MyRecipesStateLoaded;
expect(loadedState.recipes.length, 1);
expect(loadedState.recipes.first.state, 'active');
expect(loadedState.selectedState, 'active');
});
test('setActiveState - обрабатывает ошибку', () async {
myRecipesCubit = MyRecipesCubit(
myRecipesRepository: mockMyRecipesRepository,
aboutRecipeRepository: mockAboutRecipeRepository,
);
when(mockMyRecipesRepository.getMyRecipes()).thenThrow(Exception('Network error'));
await myRecipesCubit!.setActiveState();
await Future.delayed(const Duration(milliseconds: 100));
expect(myRecipesCubit!.state, isA<MyRecipesStateError>());
});
test('setArchivedState - устанавливает архивное состояние', () async {
myRecipesCubit = MyRecipesCubit(
myRecipesRepository: mockMyRecipesRepository,
aboutRecipeRepository: mockAboutRecipeRepository,
);
when(mockMyRecipesRepository.getMyRecipes()).thenAnswer(
(_) async => [testActiveRecipe, testArchivedRecipe],
);
await myRecipesCubit!.setArchivedState();
await Future.delayed(const Duration(milliseconds: 100));
expect(myRecipesCubit!.state, isA<MyRecipesStateLoaded>());
final loadedState = myRecipesCubit!.state as MyRecipesStateLoaded;
expect(loadedState.recipes.length, 1);
expect(loadedState.recipes.first.state, 'archived');
expect(loadedState.selectedState, 'archived');
});
test('setArchivedState - обрабатывает ошибку', () async {
myRecipesCubit = MyRecipesCubit(
myRecipesRepository: mockMyRecipesRepository,
aboutRecipeRepository: mockAboutRecipeRepository,
);
when(mockMyRecipesRepository.getMyRecipes()).thenThrow(Exception('Network error'));
await myRecipesCubit!.setArchivedState();
await Future.delayed(const Duration(milliseconds: 100));
expect(myRecipesCubit!.state, isA<MyRecipesStateError>());
});
test('getRecipeById - получает рецепт по slug', () async {
myRecipesCubit = MyRecipesCubit(
myRecipesRepository: mockMyRecipesRepository,
aboutRecipeRepository: mockAboutRecipeRepository,
);
when(mockAboutRecipeRepository.getRecipe('test-recipe')).thenAnswer((_) async => testRecipe);
final result = await myRecipesCubit!.getRecipeById('test-recipe');
expect(result, isA<Recipe>());
expect(result.id, 1);
expect(result.slug, 'test-recipe');
verify(mockAboutRecipeRepository.getRecipe('test-recipe')).called(1);
});
test('getMyRecipes - фильтрует только активные рецепты', () async {
myRecipesCubit = MyRecipesCubit(
myRecipesRepository: mockMyRecipesRepository,
aboutRecipeRepository: mockAboutRecipeRepository,
);
when(mockMyRecipesRepository.getMyRecipes()).thenAnswer(
(_) async => [testActiveRecipe, testArchivedRecipe, testActiveRecipe.copyWith(id: 3)],
);
await myRecipesCubit!.getMyRecipes();
await Future.delayed(const Duration(milliseconds: 100));
expect(myRecipesCubit!.state, isA<MyRecipesStateLoaded>());
final loadedState = myRecipesCubit!.state as MyRecipesStateLoaded;
expect(loadedState.recipes.length, 2);
expect(loadedState.recipes.every((r) => r.state == 'active'), true);
});
});
}

View 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);
});
});
}

View File

@@ -0,0 +1,255 @@
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/create_recipe/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/recipes.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.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/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<RecipeRepository>(), MockSpec<AboutRecipeRepository>()])
void main() {
group('Recipes Basic Widget Tests', () {
late MockFeedRepository mockFeedRepository;
late MockRecipeRepository mockRecipeRepository;
late MockAboutRecipeRepository mockAboutRecipeRepository;
late RecipesCubit recipesCubit;
const testRecipe = Recipe(
id: 1,
image: 'test-image.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: 'https://example.com/icon.jpg', authorIsMe: false),
);
setUp(() async {
await sl.reset();
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
mockFeedRepository = MockFeedRepository();
mockRecipeRepository = MockRecipeRepository();
mockAboutRecipeRepository = MockAboutRecipeRepository();
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
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 (!recipesCubit.isClosed) {
recipesCubit.close();
}
});
Widget createTestWidget(Widget child, {bool wrapInScaffold = true}) {
final widget = BlocProvider<RecipesCubit>(
create: (context) => recipesCubit,
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 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
final cubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
// Assert
expect(cubit, isA<RecipesCubit>());
expect(cubit.state.recipesList, isEmpty);
cubit.close();
});
testWidgets('should display RecipesWidget with recipe title', (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.text('Test Recipe'), findsOneWidget);
expect(find.byType(RecipesWidget), findsOneWidget);
});
await tester.pump(const Duration(milliseconds: 600));
});
testWidgets('should display RecipesWidget with description when description is present', (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.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
await tester.pumpWidget(createRecipesTestWidget(widget));
await tester.pump();
// Assert
expect(find.byType(Recipes), findsOneWidget);
});
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
recipesCubit.emit(RecipesState.initial().copyWith(isLoading: false, errorMessage: 'Error message'));
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);
});
testWidgets('should display Recipes widget with multiple recipes', (WidgetTester tester) async {
await mockNetworkImagesFor(() async {
// Arrange
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
await tester.pumpWidget(createRecipesTestWidget(widget));
await tester.pump();
// Assert
expect(find.byType(Recipes), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
});
await tester.pump(const Duration(milliseconds: 600));
});
});
}

View 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);
});
});
});
}

View File

@@ -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);
});
});
}

View File

@@ -0,0 +1,127 @@
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/recipe/repositories/my_recipes_repository.dart';
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'my_recipes_repository_test.mocks.dart';
@GenerateNiceMocks([MockSpec<MyRecipesService>()])
void main() {
group('MyRecipesRepository', () {
late MyRecipesRepository myRecipesRepository;
late MockMyRecipesService mockMyRecipesService;
setUp(() {
mockMyRecipesService = MockMyRecipesService();
myRecipesRepository = MyRecipesRepository(myRecipesService: mockMyRecipesService);
});
test('getMyRecipes returns list of recipes', () async {
const expectedPagination = MyRecipePaginationModel(
data: [
MyRecipeModel(
id: 1,
image: 'https://someurl.com/img.jpg',
state: 'published',
cookingTime: 30,
activeCookingTime: 20,
description: 'Test recipe description',
steps: [
Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1 description'),
],
videoUrl: 'https://someurl.com/video.mp4',
title: 'Test Recipe',
slug: 'test-recipe',
calories: 250.0,
likesCount: 5,
viewsCount: 10,
),
],
);
when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expectedPagination);
final result = await myRecipesRepository.getMyRecipes();
expect(result, isA<List<MyRecipeModel>>());
expect(result.length, 1);
expect(result.first.title, 'Test Recipe');
expect(result.first.id, 1);
verify(mockMyRecipesService.getMyRecipes()).called(1);
});
test('getMyRecipes returns empty list', () async {
const expectedPagination = MyRecipePaginationModel(
data: [],
);
when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expectedPagination);
final result = await myRecipesRepository.getMyRecipes();
expect(result, isA<List<MyRecipeModel>>());
expect(result.length, 0);
verify(mockMyRecipesService.getMyRecipes()).called(1);
});
test('getMyRecipes returns multiple recipes', () async {
const expectedPagination = MyRecipePaginationModel(
data: [
MyRecipeModel(
id: 1,
image: 'https://someurl.com/img1.jpg',
state: 'published',
cookingTime: 30,
activeCookingTime: 20,
description: 'First recipe',
steps: null,
videoUrl: null,
title: 'First Recipe',
slug: 'first-recipe',
calories: 250.0,
likesCount: 5,
viewsCount: 10,
),
MyRecipeModel(
id: 2,
image: 'https://someurl.com/img2.jpg',
state: 'draft',
cookingTime: 45,
activeCookingTime: 30,
description: 'Second recipe',
steps: [
Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1'),
],
videoUrl: 'https://someurl.com/video.mp4',
title: 'Second Recipe',
slug: 'second-recipe',
calories: 350.0,
likesCount: 8,
viewsCount: 15,
),
],
);
when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expectedPagination);
final result = await myRecipesRepository.getMyRecipes();
expect(result, isA<List<MyRecipeModel>>());
expect(result.length, 2);
expect(result.first.title, 'First Recipe');
expect(result.last.title, 'Second Recipe');
verify(mockMyRecipesService.getMyRecipes()).called(1);
});
test('getMyRecipes rethrows exception on error', () async {
when(mockMyRecipesService.getMyRecipes()).thenThrow(Exception('Network error'));
expect(() => myRecipesRepository.getMyRecipes(), throwsException);
verify(mockMyRecipesService.getMyRecipes()).called(1);
});
});
}

View File

@@ -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);
});
});
}

View File

@@ -0,0 +1,151 @@
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/recipe/services/my_recipes_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'my_recipes_service_test.mocks.dart';
@GenerateNiceMocks([MockSpec<MyRecipesService>()])
void main() {
group('MyRecipesService', () {
late MockMyRecipesService mockMyRecipesService;
setUp(() {
mockMyRecipesService = MockMyRecipesService();
});
test('getMyRecipes returns pagination with recipes', () async {
const expected = MyRecipePaginationModel(
data: [
MyRecipeModel(
id: 1,
image: 'https://someurl.com/img.jpg',
state: 'published',
cookingTime: 30,
activeCookingTime: 20,
description: 'Test recipe description',
steps: [
Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1 description'),
Step(image: 'https://someurl.com/step2.jpg', description: 'Step 2 description'),
],
videoUrl: 'https://someurl.com/video.mp4',
title: 'Test Recipe',
slug: 'test-recipe',
calories: 250.0,
likesCount: 5,
viewsCount: 10,
),
],
);
when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expected);
final result = await mockMyRecipesService.getMyRecipes();
expect(result, isA<MyRecipePaginationModel>());
expect(result.data.length, 1);
expect(result.data.first.title, 'Test Recipe');
expect(result.data.first.id, 1);
verify(mockMyRecipesService.getMyRecipes()).called(1);
});
test('getMyRecipes returns pagination with multiple recipes', () async {
const expected = MyRecipePaginationModel(
data: [
MyRecipeModel(
id: 1,
image: 'https://someurl.com/img1.jpg',
state: 'published',
cookingTime: 30,
activeCookingTime: 20,
description: 'First recipe description',
steps: [
Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1'),
],
videoUrl: 'https://someurl.com/video1.mp4',
title: 'First Recipe',
slug: 'first-recipe',
calories: 250.0,
likesCount: 5,
viewsCount: 10,
),
MyRecipeModel(
id: 2,
image: 'https://someurl.com/img2.jpg',
state: 'draft',
cookingTime: 45,
activeCookingTime: 30,
description: 'Second recipe description',
steps: null,
videoUrl: null,
title: 'Second Recipe',
slug: 'second-recipe',
calories: 350.0,
likesCount: 8,
viewsCount: 15,
),
],
);
when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expected);
final result = await mockMyRecipesService.getMyRecipes();
expect(result, isA<MyRecipePaginationModel>());
expect(result.data.length, 2);
expect(result.data.first.title, 'First Recipe');
expect(result.data.last.title, 'Second Recipe');
verify(mockMyRecipesService.getMyRecipes()).called(1);
});
test('getMyRecipes returns empty pagination', () async {
const expected = MyRecipePaginationModel(
data: [],
);
when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expected);
final result = await mockMyRecipesService.getMyRecipes();
expect(result, isA<MyRecipePaginationModel>());
expect(result.data.length, 0);
verify(mockMyRecipesService.getMyRecipes()).called(1);
});
test('getMyRecipes returns recipe with null steps and videoUrl', () async {
const expected = MyRecipePaginationModel(
data: [
MyRecipeModel(
id: 3,
image: 'https://someurl.com/img3.jpg',
state: 'draft',
cookingTime: 15,
activeCookingTime: 10,
description: 'Simple recipe without steps',
steps: null,
videoUrl: null,
title: 'Simple Recipe',
slug: 'simple-recipe',
calories: 150.0,
likesCount: 2,
viewsCount: 5,
),
],
);
when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expected);
final result = await mockMyRecipesService.getMyRecipes();
expect(result, isA<MyRecipePaginationModel>());
expect(result.data.length, 1);
expect(result.data.first.steps, isNull);
expect(result.data.first.videoUrl, isNull);
expect(result.data.first.state, 'draft');
verify(mockMyRecipesService.getMyRecipes()).called(1);
});
});
}