feat(dependencies): add rxdart package and improve string extension methods for better handling of empty strings and gender parsing

This commit is contained in:
mso7nueh
2025-12-04 15:45:54 +03:00
parent 0cd8e662d9
commit 953230b387
100 changed files with 7202 additions and 14 deletions

View File

@@ -0,0 +1,174 @@
import 'package:eatvio_app/core/models/pagination_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/presentation/bloc/diets_cubit/diets_state.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 'package:rxdart/rxdart.dart';
import 'diets_cubit_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<FeedRepository>(),
MockSpec<DietRepository>(),
])
void main() {
group('DietsCubit', () {
DietsCubit? dietsCubit;
late MockFeedRepository mockFeedRepository;
late MockDietRepository mockDietRepository;
setUp(() {
mockFeedRepository = MockFeedRepository();
mockDietRepository = MockDietRepository();
// Мокируем streams для подписок
when(mockDietRepository.dietLikeState).thenAnswer((_) => BehaviorSubject<LikesState>());
when(mockDietRepository.dietFavoriteState).thenAnswer((_) => BehaviorSubject<FavoriteState>());
when(mockDietRepository.dietCommentsState).thenAnswer((_) => BehaviorSubject<CommentsState>());
});
tearDown(() async {
if (dietsCubit != null && !dietsCubit!.isClosed) {
await Future.delayed(const Duration(milliseconds: 100));
await dietsCubit!.close();
dietsCubit = null;
}
});
test('начальное состояние - initial', () {
dietsCubit = DietsCubit(
feedRepository: mockFeedRepository,
dietsRepository: mockDietRepository,
);
expect(dietsCubit!.state, isA<DietsState>());
expect(dietsCubit!.state.isLoading, isTrue);
expect(dietsCubit!.state.dietsList, isEmpty);
expect(dietsCubit!.state.errorMessage, isNull);
});
test('changeTypeFilter - изменяет тип фильтра', () {
dietsCubit = DietsCubit(
feedRepository: mockFeedRepository,
dietsRepository: mockDietRepository,
);
dietsCubit!.changeTypeFilter(1);
expect(dietsCubit!.state.filterState.typeIndex, 1);
});
test('changeSortingType - изменяет тип сортировки', () {
dietsCubit = DietsCubit(
feedRepository: mockFeedRepository,
dietsRepository: mockDietRepository,
);
dietsCubit!.changeSortingType(2);
expect(dietsCubit!.state.filterState.sortingIndex, 2);
});
test('changeDurationFilter - изменяет фильтр продолжительности', () {
dietsCubit = DietsCubit(
feedRepository: mockFeedRepository,
dietsRepository: mockDietRepository,
);
dietsCubit!.changeDurationFilter(1);
expect(dietsCubit!.state.filterState.durationIndex, 1);
});
test('loadDiets - успешная загрузка диет', () async {
dietsCubit = DietsCubit(
feedRepository: mockFeedRepository,
dietsRepository: mockDietRepository,
);
const testDiet = Diet(
id: 1,
slug: 'test-diet',
name: 'Test Diet',
image: 'https://example.com/image.jpg',
duration: 7,
state: DietState.active,
type: DietType(veg: true, light: false, carb: false, meat: false),
details: 'Test details',
dailySchedule: {},
author: Author(
authorSlug: 'author',
authorName: 'Author Name',
authorProfileIcon: 'https://example.com/icon.jpg',
authorIsMe: false,
isEntitySubscribedByMe: null,
authorPlaceInRating: null,
),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
wasLikedByMe: false,
isFavorite: false,
dailyNutrition: [],
);
const dietsPagination = DietsPagination(
data: [testDiet],
links: Links(
first: 'https://example.com?page=1',
last: 'https://example.com?page=1',
prev: null,
next: null,
),
meta: Meta(
currentPage: 1,
from: 1,
lastPage: 1,
links: [],
perPage: 10,
to: 1,
total: 1,
),
);
when(mockFeedRepository.getDiets(
query: anyNamed('query'),
types: anyNamed('types'),
duration: anyNamed('duration'),
sorting: anyNamed('sorting'),
)).thenAnswer((_) async => dietsPagination);
await dietsCubit!.loadDiets();
await Future.delayed(const Duration(milliseconds: 200));
expect(dietsCubit!.state.dietsList.length, 1);
expect(dietsCubit!.state.dietsList.first.id, 1);
expect(dietsCubit!.state.errorMessage, isNull);
});
test('loadDiets - обработка ошибки', () async {
dietsCubit = DietsCubit(
feedRepository: mockFeedRepository,
dietsRepository: mockDietRepository,
);
when(mockFeedRepository.getDiets(
query: anyNamed('query'),
types: anyNamed('types'),
duration: anyNamed('duration'),
sorting: anyNamed('sorting'),
)).thenThrow(Exception('Ошибка загрузки'));
await dietsCubit!.loadDiets();
await Future.delayed(const Duration(milliseconds: 200));
expect(dietsCubit!.state.errorMessage, isNotNull);
expect(dietsCubit!.state.isLoading, isFalse);
});
});
}

View File

@@ -0,0 +1,61 @@
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('DietsState', () {
test('initial создает начальное состояние', () {
final state = DietsState.initial();
expect(state.isLoading, isTrue);
expect(state.dietsList, isEmpty);
expect(state.errorMessage, isNull);
expect(state.filterState.durationIndex, 0);
expect(state.filterState.sortingIndex, 0);
expect(state.filterState.typeIndex, 0);
});
test('создает DietsState с данными', () {
const diet = Diet(
id: 1,
slug: 'test-diet',
name: 'Test Diet',
image: 'https://example.com/image.jpg',
duration: 7,
state: DietState.active,
type: DietType(veg: true, light: false, carb: false, meat: false),
details: 'Test details',
dailySchedule: {},
author: Author(
authorSlug: 'author',
authorName: 'Author Name',
authorProfileIcon: 'https://example.com/icon.jpg',
authorIsMe: false,
isEntitySubscribedByMe: null,
authorPlaceInRating: null,
),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
wasLikedByMe: false,
isFavorite: false,
dailyNutrition: [],
);
const state = DietsState(
filterState: DietsFilterState(
durationIndex: 1,
sortingIndex: 1,
typeIndex: 1,
),
dietsList: [diet],
isLoading: false,
errorMessage: null,
);
expect(state.dietsList.length, 1);
expect(state.isLoading, isFalse);
});
});
}

View File

@@ -0,0 +1,158 @@
import 'package:eatvio_app/core/models/pagination_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/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/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:rxdart/rxdart.dart';
import 'plans_cubit_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<FeedRepository>(),
MockSpec<PlanRepository>(),
])
void main() {
group('PlansCubit', () {
PlansCubit? plansCubit;
late MockFeedRepository mockFeedRepository;
late MockPlanRepository mockPlanRepository;
setUp(() {
mockFeedRepository = MockFeedRepository();
mockPlanRepository = MockPlanRepository();
// Мокируем streams для подписок
when(mockPlanRepository.planLikeState).thenAnswer((_) => BehaviorSubject<LikesState>());
when(mockPlanRepository.planFavoriteState).thenAnswer((_) => BehaviorSubject<FavoriteState>());
when(mockPlanRepository.planCommentsState).thenAnswer((_) => BehaviorSubject<CommentsState>());
});
tearDown(() async {
if (plansCubit != null && !plansCubit!.isClosed) {
await Future.delayed(const Duration(milliseconds: 100));
await plansCubit!.close();
plansCubit = null;
}
});
test('начальное состояние - initial', () {
plansCubit = PlansCubit(
feedRepository: mockFeedRepository,
planRepository: mockPlanRepository,
);
expect(plansCubit!.state, isA<PlansState>());
expect(plansCubit!.state.isLoading, isTrue);
expect(plansCubit!.state.plans, isEmpty);
expect(plansCubit!.state.errorMessage, isNull);
});
test('changeDurationFilter - изменяет фильтр продолжительности', () {
plansCubit = PlansCubit(
feedRepository: mockFeedRepository,
planRepository: mockPlanRepository,
);
plansCubit!.changeDurationFilter(1);
expect(plansCubit!.state.filterState.durationIndex, 1);
});
test('changeTypeFilter - изменяет тип фильтра', () {
plansCubit = PlansCubit(
feedRepository: mockFeedRepository,
planRepository: mockPlanRepository,
);
plansCubit!.changeTypeFilter(1);
expect(plansCubit!.state.filterState.typeIndex, 1);
});
test('changePriceFilter - изменяет фильтр цены', () {
plansCubit = PlansCubit(
feedRepository: mockFeedRepository,
planRepository: mockPlanRepository,
);
plansCubit!.changePriceFilter(100, 500);
expect(plansCubit!.state.filterState.priceFilter, (100, 500));
});
test('loadPlans - успешная загрузка планов', () async {
plansCubit = PlansCubit(
feedRepository: mockFeedRepository,
planRepository: mockPlanRepository,
);
const testPlan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://example.com/image.jpg',
duration: 7,
state: 'active',
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test description',
packetsCount: 3,
minPriceOfOptions: 1000.0,
maxPriceOfOptions: 2000.0,
author: Author(
authorSlug: 'author',
authorName: 'Author Name',
authorProfileIcon: 'https://example.com/icon.jpg',
authorIsMe: false,
isEntitySubscribedByMe: null,
authorPlaceInRating: null,
),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
wasLikedByMe: false,
isFavorite: false,
);
const plansPagination = PlanPaginationModel(
data: [testPlan],
links: Links(
first: 'https://example.com?page=1',
last: 'https://example.com?page=1',
prev: null,
next: null,
),
meta: Meta(
currentPage: 1,
from: 1,
lastPage: 1,
links: [],
perPage: 10,
to: 1,
total: 1,
),
);
when(mockFeedRepository.getPlans(
query: anyNamed('query'),
types: anyNamed('types'),
duration: anyNamed('duration'),
minPrice: anyNamed('minPrice'),
maxPrice: anyNamed('maxPrice'),
)).thenAnswer((_) async => plansPagination);
await plansCubit!.loadPlans();
await Future.delayed(const Duration(milliseconds: 200));
expect(plansCubit!.state.plans.length, 1);
expect(plansCubit!.state.plans.first.id, 1);
expect(plansCubit!.state.errorMessage, isNull);
});
});
}

View File

@@ -0,0 +1,64 @@
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_state.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('PlansState', () {
test('initial создает начальное состояние', () {
final state = PlansState.initial();
expect(state.isLoading, isTrue);
expect(state.plans, isEmpty);
expect(state.errorMessage, isNull);
expect(state.minPlanPrice, 0);
expect(state.maxPlanPrice, 0);
});
test('создает PlansState с данными', () {
const plan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://example.com/image.jpg',
duration: 7,
state: 'active',
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test details',
packetsCount: 3,
author: Author(
authorSlug: 'author',
authorName: 'Author Name',
authorProfileIcon: 'https://example.com/icon.jpg',
authorIsMe: false,
isEntitySubscribedByMe: null,
authorPlaceInRating: null,
),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
isFavorite: false,
minPriceOfOptions: 1000.0,
maxPriceOfOptions: 2000.0,
wasLikedByMe: false,
);
const state = PlansState(
isLoading: false,
plans: [plan],
errorMessage: null,
filterState: PlansFilterState(
priceFilter: (1000, 2000),
durationIndex: 1,
typeIndex: 1,
),
minPlanPrice: 1000,
maxPlanPrice: 2000,
);
expect(state.plans.length, 1);
expect(state.isLoading, isFalse);
});
});
}

View File

@@ -0,0 +1,76 @@
import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'profile_cubit_test.mocks.dart';
@GenerateNiceMocks([MockSpec<ProfileRepository>()])
void main() {
group('FeedProfileCubit', () {
FeedProfileCubit? cubit;
late MockProfileRepository mockRepository;
setUp(() {
mockRepository = MockProfileRepository();
});
tearDown(() async {
if (cubit != null && !cubit!.isClosed) {
await cubit!.close();
cubit = null;
}
});
test('начальное состояние - с null профилем', () {
cubit = FeedProfileCubit(profileRepository: mockRepository);
expect(cubit!.state.profile, isNull);
});
test('getProfile - успешная загрузка профиля', () async {
cubit = FeedProfileCubit(profileRepository: mockRepository);
const testProfile = ProfileModel(
id: 1,
slug: 'test-user',
name: 'Test User',
email: 'test@example.com',
phoneConfirmationState: 'confirmed',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: 'https://example.com/icon.jpg',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
when(mockRepository.getMyProfile())
.thenAnswer((_) async => testProfile);
await cubit!.getProfile();
expect(cubit!.state.profile, isNotNull);
expect(cubit!.state.profile!.id, 1);
});
test('getProfile - обработка ошибки', () async {
cubit = FeedProfileCubit(profileRepository: mockRepository);
when(mockRepository.getMyProfile())
.thenThrow(Exception('Ошибка загрузки'));
await cubit!.getProfile();
// Состояние не должно измениться при ошибке
expect(cubit!.state.profile, isNull);
});
});
}

View File

@@ -0,0 +1,123 @@
import 'package:eatvio_app/features/create_recipe/models/recipe_tag_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/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_pagination_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/repositories/feed_repository.dart';
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:rxdart/rxdart.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;
setUp(() {
mockFeedRepository = MockFeedRepository();
mockRecipeRepository = MockRecipeRepository();
mockAboutRecipeRepository = MockAboutRecipeRepository();
// Мокируем streams для подписок
when(mockAboutRecipeRepository.recipeLikeState).thenAnswer((_) => BehaviorSubject<LikesState>());
when(mockAboutRecipeRepository.recipeFavoriteState).thenAnswer((_) => BehaviorSubject<FavoriteState>());
when(mockAboutRecipeRepository.recipeCommentsState).thenAnswer((_) => BehaviorSubject<CommentsState>());
});
tearDown(() async {
if (recipesCubit != null && !recipesCubit!.isClosed) {
await Future.delayed(const Duration(milliseconds: 100));
await recipesCubit!.close();
recipesCubit = null;
}
});
test('начальное состояние - initial', () {
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
expect(recipesCubit!.state, isA<RecipesState>());
expect(recipesCubit!.state.isLoading, isTrue);
expect(recipesCubit!.state.recipesList, isEmpty);
expect(recipesCubit!.state.selectedTags, isEmpty);
});
test('loadRecipesList - успешная загрузка рецептов', () async {
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
const testRecipe = Recipe(
id: 1,
image: 'https://example.com/image.jpg',
state: 'active',
cookingTime: 30,
activeCookingTime: 15,
ingredients: [],
description: 'Test description',
steps: [],
videoUrl: null,
createdAt: null,
updatedAt: null,
title: 'Test Recipe',
type: 'main',
slug: 'test-recipe',
commentsCount: 5,
wasLikedByMe: false,
isFavorite: false,
calories: 300.0,
servings: 4,
likesCount: 10,
viewsCount: 100,
tags: [],
author: Author(
authorSlug: 'author',
authorName: 'Author Name',
authorProfileIcon: 'https://example.com/icon.jpg',
authorIsMe: false,
isEntitySubscribedByMe: null,
authorPlaceInRating: null,
),
);
const recipesList = RecipesPagination(recipesList: [testRecipe]);
final testTags = [
RecipeTag(id: 1, name: 'Tag 1', recipesCount: 10, createdAt: DateTime(2024, 1, 1), updatedAt: DateTime(2024, 1, 2)),
RecipeTag(id: 2, name: 'Tag 2', recipesCount: 20, createdAt: DateTime(2024, 1, 1), updatedAt: DateTime(2024, 1, 2)),
];
when(mockFeedRepository.getRecipes(tags: anyNamed('tags'), query: anyNamed('query'))).thenAnswer((_) async => recipesList.recipesList);
when(mockRecipeRepository.getRecipesTags()).thenAnswer((_) async => testTags);
await recipesCubit!.loadRecipesList();
await Future.delayed(const Duration(milliseconds: 200));
expect(recipesCubit!.state.recipesList.length, 1);
expect(recipesCubit!.state.recipesTags.length, 2);
expect(recipesCubit!.state.errorMessage, isNull);
});
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, 2));
recipesCubit!.updateTags(tag);
expect(recipesCubit!.state.selectedTags.contains(1), isTrue);
expect(recipesCubit!.state.selectedTagsCounter, 1);
recipesCubit!.updateTags(tag);
expect(recipesCubit!.state.selectedTags.contains(1), isFalse);
expect(recipesCubit!.state.selectedTagsCounter, 0);
});
});
}

View File

@@ -0,0 +1,77 @@
import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.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_state.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('RecipesState', () {
test('initial создает начальное состояние', () {
final state = RecipesState.initial();
expect(state.isLoading, isTrue);
expect(state.recipesList, isEmpty);
expect(state.recipesTags, isEmpty);
expect(state.selectedTags, isEmpty);
expect(state.selectedTagsCounter, 0);
expect(state.errorMessage, isNull);
});
test('создает RecipesState с данными', () {
const recipe = Recipe(
id: 1,
image: 'https://example.com/image.jpg',
state: 'active',
cookingTime: 30,
activeCookingTime: 15,
ingredients: [],
description: 'Test description',
steps: [],
videoUrl: null,
createdAt: null,
updatedAt: null,
title: 'Test Recipe',
type: 'main',
slug: 'test-recipe',
commentsCount: 5,
wasLikedByMe: false,
isFavorite: false,
calories: 300.0,
servings: 4,
likesCount: 10,
viewsCount: 100,
tags: [],
author: Author(
authorSlug: 'author',
authorName: 'Author Name',
authorProfileIcon: 'https://example.com/icon.jpg',
authorIsMe: false,
isEntitySubscribedByMe: null,
authorPlaceInRating: null,
),
);
final tag = RecipeTag(
id: 1,
name: 'Breakfast',
recipesCount: 10,
createdAt: DateTime(2024, 1, 1),
updatedAt: DateTime(2024, 1, 2),
);
final state = RecipesState(
recipesList: [recipe],
recipesTags: [tag],
selectedTags: {1},
selectedTagsCounter: 1,
isLoading: false,
errorMessage: null,
);
expect(state.recipesList.length, 1);
expect(state.recipesTags.length, 1);
expect(state.selectedTags.length, 1);
});
});
}