fix: issue-372, issue-227, issue-406

This commit is contained in:
misha
2026-09-02 09:32:14 +03:00
parent 10af279d87
commit c6ea1d414d
34 changed files with 1623 additions and 387 deletions

View File

@@ -1,4 +1,5 @@
import 'package:dio/dio.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/plans/enums/chosen_plan_state_enum.dart';
@@ -6,22 +7,36 @@ import 'package:eatvio_app/features/plans/models/chosen_plan_model.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/repositories/plan_repository.dart';
import 'package:eatvio_app/features/profile/models/chosen_diets_pagination_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 'about_plan_cubit_test.mocks.dart';
@GenerateNiceMocks([MockSpec<PlanRepository>()])
@GenerateNiceMocks([MockSpec<PlanRepository>(), MockSpec<ProfileRepository>(), MockSpec<DietRepository>()])
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('AboutPlanCubit', () {
AboutPlanCubit? aboutPlanCubit;
late MockPlanRepository mockPlanRepository;
late MockProfileRepository mockProfileRepository;
late MockDietRepository mockDietRepository;
AboutPlanCubit createCubit() {
return AboutPlanCubit(
planRepository: mockPlanRepository,
profileRepository: mockProfileRepository,
dietRepository: mockDietRepository,
);
}
setUp(() {
mockPlanRepository = MockPlanRepository();
mockProfileRepository = MockProfileRepository();
mockDietRepository = MockDietRepository();
});
tearDown(() async {
@@ -32,7 +47,7 @@ void main() {
});
test('начальное состояние - initial', () {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
expect(aboutPlanCubit!.state, isA<AboutPlanState>());
expect(aboutPlanCubit!.state.isLoading, isFalse);
@@ -40,7 +55,7 @@ void main() {
});
test('getPlan - успешная загрузка плана', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -79,7 +94,7 @@ void main() {
});
test('getPlanWithLoading - устанавливает loading состояние', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -117,7 +132,7 @@ void main() {
});
test('choseOption - изменяет выбранную опцию', () {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
aboutPlanCubit!.choseOption(2);
@@ -125,7 +140,7 @@ void main() {
});
test('likePlan - лайк плана', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -166,7 +181,7 @@ void main() {
});
test('getPlan - обработка ошибки', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
when(mockPlanRepository.getPlan('test-plan')).thenThrow(Exception('Ошибка загрузки'));
@@ -177,7 +192,7 @@ void main() {
});
test('getPlanWithLoading - обработка ошибки', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
when(mockPlanRepository.getPlan('test-plan')).thenThrow(Exception('Ошибка загрузки'));
@@ -188,7 +203,7 @@ void main() {
});
test('likePlan - убирает лайк если уже лайкнуто', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -231,7 +246,7 @@ void main() {
});
test('addToFavorite - добавляет план в избранное', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -273,7 +288,7 @@ void main() {
});
test('removeFromFavorite - удаляет план из избранного', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -315,7 +330,7 @@ void main() {
});
test('chosePlan - успешно выбирает план', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -368,7 +383,7 @@ void main() {
});
test('chosePlan - возвращает ошибку hasPlan при 400 статусе', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -415,7 +430,7 @@ void main() {
});
test('likePlan - лайк плана с index', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -458,7 +473,7 @@ void main() {
});
test('likePlan - убирает лайк с index', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -501,7 +516,7 @@ void main() {
});
test('likePlan - обрабатывает ошибку', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -542,7 +557,7 @@ void main() {
});
test('likePlan - не делает ничего если план не загружен', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
await aboutPlanCubit!.likePlan(index: null, slug: 'test-plan');
@@ -551,7 +566,7 @@ void main() {
});
test('addToFavorite - добавляет план в избранное с index', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -592,7 +607,7 @@ void main() {
});
test('addToFavorite - обрабатывает ошибку', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -632,7 +647,7 @@ void main() {
});
test('addToFavorite - не делает ничего если план не загружен', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
await aboutPlanCubit!.addToFavorite(slug: 'test-plan', index: null);
@@ -641,7 +656,7 @@ void main() {
});
test('removeFromFavorite - удаляет план из избранного с index', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -682,7 +697,7 @@ void main() {
});
test('removeFromFavorite - обрабатывает ошибку', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -722,7 +737,7 @@ void main() {
});
test('removeFromFavorite - не делает ничего если план не загружен', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
await aboutPlanCubit!.removeFromFavorite(slug: 'test-plan', index: null);
@@ -731,7 +746,7 @@ void main() {
});
test('chosePlan - выбирает план с optionIndex 1', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -784,7 +799,7 @@ void main() {
});
test('chosePlan - выбирает план с optionIndex 2', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -837,7 +852,7 @@ void main() {
});
test('chosePlan - возвращает пустой результат когда slug == null', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
final result = await aboutPlanCubit!.chosePlan();
@@ -847,7 +862,7 @@ void main() {
});
test('chosePlan - обрабатывает DioException с другим статусом', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -894,7 +909,7 @@ void main() {
});
test('chosePlan - обрабатывает общее исключение', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -936,7 +951,7 @@ void main() {
});
test('getPlanWithLoading - устанавливает isLoading в true перед загрузкой', () async {
aboutPlanCubit = AboutPlanCubit(planRepository: mockPlanRepository);
aboutPlanCubit = createCubit();
const testPlan = PlanModel(
id: 1,
@@ -977,5 +992,76 @@ void main() {
await future;
expect(aboutPlanCubit!.state.isLoading, isFalse);
});
test('getActivePublicDiet - возвращает активную публичную диету', () async {
aboutPlanCubit = createCubit();
final publicDiet = ChosenDietModel(
chosenDietID: 1,
chosenPlanId: 0,
state: 'started',
startDate: DateTime(2024, 1, 1),
dietId: 10,
);
when(mockProfileRepository.getChosenDiets()).thenAnswer((_) async => [publicDiet]);
final result = await aboutPlanCubit!.getActivePublicDiet();
expect(result, isNotNull);
expect(result!.dietId, 10);
expect(result.chosenPlanId, 0);
});
test('getActivePublicDiet - игнорирует диету, привязанную к плану', () async {
aboutPlanCubit = createCubit();
final personalDiet = ChosenDietModel(
chosenDietID: 1,
chosenPlanId: 5,
state: 'started',
startDate: DateTime(2024, 1, 1),
dietId: 10,
);
when(mockProfileRepository.getChosenDiets()).thenAnswer((_) async => [personalDiet]);
final result = await aboutPlanCubit!.getActivePublicDiet();
expect(result, isNull);
});
test('getActivePublicDiet - возвращает null если нет выбранных диет', () async {
aboutPlanCubit = createCubit();
when(mockProfileRepository.getChosenDiets()).thenAnswer((_) async => []);
final result = await aboutPlanCubit!.getActivePublicDiet();
expect(result, isNull);
});
test('getActivePublicDiet - возвращает null при ошибке', () async {
aboutPlanCubit = createCubit();
when(mockProfileRepository.getChosenDiets()).thenThrow(Exception('error'));
final result = await aboutPlanCubit!.getActivePublicDiet();
expect(result, isNull);
});
test('cancelPublicDiet - успешно отменяет диету', () async {
aboutPlanCubit = createCubit();
when(mockDietRepository.cancelDiet(10)).thenAnswer((_) async {});
final result = await aboutPlanCubit!.cancelPublicDiet(10);
expect(result, isTrue);
verify(mockDietRepository.cancelDiet(10)).called(1);
});
test('cancelPublicDiet - возвращает false при ошибке', () async {
aboutPlanCubit = createCubit();
when(mockDietRepository.cancelDiet(10)).thenThrow(Exception('error'));
final result = await aboutPlanCubit!.cancelPublicDiet(10);
expect(result, isFalse);
});
});
}

View File

@@ -11,6 +11,8 @@ import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit
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/pages/about_plan_screen.dart';
import 'package:eatvio_app/features/plans/presentation/widgets/confirm_start_plan_bottom_sheet.dart';
import 'package:eatvio_app/features/profile/models/chosen_diets_pagination_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -301,5 +303,82 @@ void main() {
expect(find.byType(AboutPlanScreen), findsOneWidget);
expect(find.text('Test Plan'), findsOneWidget);
});
testWidgets('order with public diet shows start plan confirmation', (tester) async {
final planCubit = MockAboutPlanCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
const state = AboutPlanState(isLoading: false, planModel: testPlan, chosenOptionIndex: 0, error: null);
final publicDiet = ChosenDietModel(
chosenDietID: 1,
chosenPlanId: 0,
state: 'started',
startDate: DateTime(2024, 1, 1),
dietId: 10,
);
when(planCubit.state).thenReturn(state);
when(planCubit.stream).thenAnswer((_) => const Stream<AboutPlanState>.empty());
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
when(planCubit.getActivePublicDiet()).thenAnswer((_) async => publicDiet);
when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(planCubit, commentsCubit);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
await tester.pump();
await tester.pump();
});
await tester.tap(find.text('Заказать'));
await tester.pump();
await tester.pump();
expect(find.byType(ConfirmStartPlanBottomSheet), findsOneWidget);
expect(find.text('Вы уверены, что хотите начать прохождение плана?'), findsOneWidget);
verifyNever(planCubit.chosePlan());
});
testWidgets('order without public diet calls chosePlan', (tester) async {
final planCubit = MockAboutPlanCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
const state = AboutPlanState(isLoading: false, planModel: testPlan, chosenOptionIndex: 0, error: null);
when(planCubit.state).thenReturn(state);
when(planCubit.stream).thenAnswer((_) => const Stream<AboutPlanState>.empty());
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
when(planCubit.getActivePublicDiet()).thenAnswer((_) async => null);
when(planCubit.chosePlan()).thenAnswer((_) async => (null, ''));
when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(planCubit, commentsCubit);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
await tester.pump();
await tester.pump();
});
await tester.tap(find.text('Заказать'));
await tester.pump();
await tester.pump();
expect(find.byType(ConfirmStartPlanBottomSheet), findsNothing);
verify(planCubit.chosePlan()).called(1);
});
});
}

View File

@@ -0,0 +1,56 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/plans/presentation/widgets/confirm_start_plan_bottom_sheet.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ConfirmStartPlanBottomSheet', () {
Widget createTestWidget(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: Scaffold(body: child),
);
}
testWidgets('показывает текст подтверждения начала плана', (tester) async {
await tester.pumpWidget(
createTestWidget(
ConfirmStartPlanBottomSheet(onConfirm: () async {}),
),
);
await tester.pump();
expect(find.text('Вы уверены, что хотите начать прохождение плана?'), findsOneWidget);
expect(find.byType(CustomOutlinedButton), findsOneWidget);
});
testWidgets('по нажатию подтверждения вызывает onConfirm', (tester) async {
var confirmed = false;
await tester.pumpWidget(
createTestWidget(
ConfirmStartPlanBottomSheet(
onConfirm: () async {
confirmed = true;
},
),
),
);
await tester.pump();
await tester.tap(find.byType(CustomOutlinedButton));
await tester.pump();
expect(confirmed, isTrue);
});
});
}

View File

@@ -3,10 +3,12 @@ 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/diets/repositories/diet_repository.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/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -18,17 +20,25 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'widgets_test.mocks.dart';
@GenerateNiceMocks([MockSpec<PlanRepository>()])
@GenerateNiceMocks([MockSpec<PlanRepository>(), MockSpec<ProfileRepository>(), MockSpec<DietRepository>()])
void main() {
group('Plans Widgets Tests', () {
late MockPlanRepository mockPlanRepository;
late MockProfileRepository mockProfileRepository;
late MockDietRepository mockDietRepository;
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);
mockProfileRepository = MockProfileRepository();
mockDietRepository = MockDietRepository();
aboutPlanCubit = AboutPlanCubit(
planRepository: mockPlanRepository,
profileRepository: mockProfileRepository,
dietRepository: mockDietRepository,
);
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});