feat(feed): fixed comments state updating in feed, feat(consultantRegistration): fixed registration flow for users with unverified phone, feat(tests): fixed tests coverage

This commit is contained in:
misha
2026-04-01 10:37:59 +03:00
parent ee2dd75a94
commit a13b623ca2
20 changed files with 949 additions and 343 deletions

View File

@@ -205,5 +205,54 @@ void main() {
expect(find.text('Test Post Title'), findsOneWidget);
});
});
testWidgets('loaded state tapping like invokes toggleLike', (tester) async {
await mockNetworkImagesFor(() async {
final blogPostCubit = MockBlogPostCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
final blogPostModel = FeedBlogPostModel(
id: 1,
title: 'Test Post Title',
description: 'Test post description',
image: 'https://example.com/image.jpg',
state: 'published',
author: const Author(authorSlug: 'author-1', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
videoUrl: '',
createdAt: DateTime(2024, 1, 15),
updatedAt: DateTime(2024, 1, 15),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
wasLikedByMe: false,
);
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
when(blogPostCubit.state).thenReturn(state);
when(blogPostCubit.stream).thenAnswer((_) => Stream.value(state));
when(blogPostCubit.close()).thenAnswer((_) async => Future<void>.value());
when(blogPostCubit.getBlogPost(any)).thenAnswer((_) async => Future<void>.value());
when(blogPostCubit.toggleLike()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewPost(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.updateFeedBlogPostList(any, any)).thenReturn(null);
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(blogPostCubit, commentsCubit);
await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, const BlogPostScreen(id: 1)));
await tester.pump();
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.favorite_border_rounded));
await tester.pump();
verify(blogPostCubit.toggleLike()).called(1);
});
});
});
}

View File

@@ -1,4 +1,9 @@
import 'dart:io';
import 'package:dio/dio.dart';
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/models/pagination_model.dart';
import 'package:eatvio_app/features/create_recipe/models/ingredient.dart';
import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart';
@@ -8,31 +13,123 @@ import 'package:eatvio_app/features/create_recipe/models/step.dart';
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:mocktail/mocktail.dart';
import 'package:retrofit/retrofit.dart';
class MockCreateRecipeService extends Mock implements CreateRecipeService {}
final mockHttpResponse = HttpResponse(
null,
Response(requestOptions: RequestOptions(path: ''), statusCode: 200),
);
final mockHttpResponse = HttpResponse(null, Response(requestOptions: RequestOptions(path: ''), statusCode: 200));
void main() {
setUpAll(() {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
if (sl.isRegistered<AppConfig>()) {
sl.unregister<AppConfig>();
}
sl.registerSingleton<AppConfig>(AppConfig(buildType: AppBuildType.tests));
registerFallbackValue(FormData());
});
group('RecipeRepository', () {
late RecipeRepository repository;
late MockCreateRecipeService mockService;
late Directory tempDir;
late File recipeImage;
late File stepImage;
setUp(() {
mockService = MockCreateRecipeService();
repository = RecipeRepository(recipeService: mockService);
});
setUp(() async {
tempDir = await Directory.systemTemp.createTemp('eatvio_recipe_repo_test_');
recipeImage = File('${tempDir.path}/recipe.png');
stepImage = File('${tempDir.path}/step.png');
// Valid 1x1 transparent PNG to satisfy ImageCompressUtility decoding.
const png1x1 = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
0x42,
0x60,
0x82,
];
await recipeImage.writeAsBytes(png1x1, flush: true);
await stepImage.writeAsBytes(png1x1, flush: true);
});
tearDown(() async {
if (tempDir.existsSync()) {
await tempDir.delete(recursive: true);
}
});
test('inputProduct - возвращает Map с продуктами', () async {
const name = 'test-product';
const expected = {
@@ -177,12 +274,12 @@ void main() {
test('updateRecipe - вызывает updateRecipe с правильными параметрами', () async {
const slug = 'recipe-slug';
const recipe = RecipeModel(
final recipe = RecipeModel(
type: 'main',
title: 'Updated Recipe',
description: 'Updated description',
videoUrl: null,
image: '/test/path/image.jpg',
image: recipeImage.path,
ingredients: null,
steps: null,
tags: null,
@@ -192,27 +289,29 @@ void main() {
energyPerServing: 300,
);
// Мокируем ImageCompressUtility.compressFile
// Это сложно, так как это статический метод, но мы можем проверить, что вызывается updateRecipe
when(() => mockService.updateRecipe(slug: slug, formData: any(named: 'formData')))
.thenAnswer((_) async => Future.value(mockHttpResponse));
when(
() => mockService.updateRecipe(
slug: slug,
formData: any(named: 'formData'),
),
).thenAnswer((_) async => Future.value(mockHttpResponse));
// Этот тест может упасть из-за ImageCompressUtility, но мы проверяем логику
try {
await repository.publishRecipe(recipe, true, slug);
verify(() => mockService.updateRecipe(slug: slug, formData: any(named: 'formData'))).called(1);
} catch (e) {
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
}
await repository.publishRecipe(recipe, true, slug);
verify(
() => mockService.updateRecipe(
slug: slug,
formData: any(named: 'formData'),
),
).called(1);
});
test('publishRecipe - обрабатывает рецепт с тегами', () async {
const recipe = RecipeModel(
final recipe = RecipeModel(
type: 'main',
title: 'Test Recipe',
description: 'Test description',
videoUrl: null,
image: '/test/path/image.jpg',
image: recipeImage.path,
ingredients: null,
steps: null,
tags: ['tag1', 'tag2'],
@@ -222,24 +321,19 @@ void main() {
energyPerServing: 300,
);
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
.thenAnswer((_) async => Future.value(mockHttpResponse));
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
try {
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
} catch (e) {
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
}
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
});
test('publishRecipe - обрабатывает рецепт с videoUrl', () async {
const recipe = RecipeModel(
final recipe = RecipeModel(
type: 'main',
title: 'Test Recipe',
description: 'Test description',
videoUrl: 'https://example.com/video.mp4',
image: '/test/path/image.jpg',
image: recipeImage.path,
ingredients: null,
steps: null,
tags: null,
@@ -249,24 +343,19 @@ void main() {
energyPerServing: 300,
);
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
.thenAnswer((_) async => Future.value(mockHttpResponse));
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
try {
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
} catch (e) {
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
}
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
});
test('publishRecipe - обрабатывает рецепт без videoUrl (null)', () async {
const recipe = RecipeModel(
final recipe = RecipeModel(
type: 'main',
title: 'Test Recipe',
description: 'Test description',
videoUrl: null,
image: '/test/path/image.jpg',
image: recipeImage.path,
ingredients: null,
steps: null,
tags: null,
@@ -276,24 +365,19 @@ void main() {
energyPerServing: 300,
);
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
.thenAnswer((_) async => Future.value(mockHttpResponse));
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
try {
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
} catch (e) {
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
}
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
});
test('publishRecipe - обрабатывает рецепт с пустым videoUrl', () async {
const recipe = RecipeModel(
final recipe = RecipeModel(
type: 'main',
title: 'Test Recipe',
description: 'Test description',
videoUrl: '',
image: '/test/path/image.jpg',
image: recipeImage.path,
ingredients: null,
steps: null,
tags: null,
@@ -303,24 +387,19 @@ void main() {
energyPerServing: 300,
);
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
.thenAnswer((_) async => Future.value(mockHttpResponse));
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
try {
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
} catch (e) {
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
}
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
});
test('publishRecipe - пробрасывает исключение при ошибке', () async {
const recipe = RecipeModel(
final recipe = RecipeModel(
type: 'main',
title: 'Test Recipe',
description: 'Test description',
videoUrl: null,
image: '/test/path/image.jpg',
image: recipeImage.path,
ingredients: null,
steps: null,
tags: null,
@@ -330,37 +409,21 @@ void main() {
energyPerServing: 300,
);
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
.thenThrow(Exception('Error'));
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenThrow(Exception('Error'));
try {
await repository.publishRecipe(recipe);
fail('Should have thrown exception');
} catch (e) {
expect(e, isA<Exception>());
}
expect(() => repository.publishRecipe(recipe), throwsA(isA<Exception>()));
});
test('publishRecipe - обрабатывает рецепт с ингредиентами', () async {
const recipe = RecipeModel(
final recipe = RecipeModel(
type: 'main',
title: 'Test Recipe',
description: 'Test description',
videoUrl: null,
image: '/test/path/image.jpg',
image: recipeImage.path,
ingredients: [
Ingredient(
name: 'Apple',
unit: '100',
portion: 'grams',
productId: 1,
),
Ingredient(
name: 'Banana',
unit: '50',
portion: 'grams',
productId: 2,
),
const Ingredient(name: 'Apple', unit: '100', portion: 'grams', productId: 1),
const Ingredient(name: 'Banana', unit: '50', portion: 'grams', productId: 2),
],
steps: null,
tags: null,
@@ -370,31 +433,21 @@ void main() {
energyPerServing: 300,
);
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
.thenAnswer((_) async => Future.value(mockHttpResponse));
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
try {
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
} catch (e) {
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
}
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
});
test('publishRecipe - обрабатывает рецепт с null ингредиентами', () async {
const recipe = RecipeModel(
final recipe = RecipeModel(
type: 'main',
title: 'Test Recipe',
description: 'Test description',
videoUrl: null,
image: '/test/path/image.jpg',
image: recipeImage.path,
ingredients: [
Ingredient(
name: 'Apple',
unit: '100',
portion: 'grams',
productId: 1,
),
const Ingredient(name: 'Apple', unit: '100', portion: 'grams', productId: 1),
null, // null ингредиент должен быть отфильтрован
],
steps: null,
@@ -405,34 +458,23 @@ void main() {
energyPerServing: 300,
);
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
.thenAnswer((_) async => Future.value(mockHttpResponse));
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
try {
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
} catch (e) {
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
}
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
});
test('publishRecipe - обрабатывает рецепт со шагами без изображений', () async {
const recipe = RecipeModel(
final recipe = RecipeModel(
type: 'main',
title: 'Test Recipe',
description: 'Test description',
videoUrl: null,
image: '/test/path/image.jpg',
image: recipeImage.path,
ingredients: null,
steps: [
Step(
description: 'Step 1 description',
image: null,
),
Step(
description: 'Step 2 description',
image: '',
),
const Step(description: 'Step 1 description', image: null),
const Step(description: 'Step 2 description', image: ''),
],
tags: null,
totalCookingTime: 30,
@@ -441,35 +483,24 @@ void main() {
energyPerServing: 300,
);
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
.thenAnswer((_) async => Future.value(mockHttpResponse));
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
try {
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
} catch (e) {
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
}
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
});
test('publishRecipe - обрабатывает рецепт со смешанными шагами (с null)', () async {
const recipe = RecipeModel(
final recipe = RecipeModel(
type: 'main',
title: 'Test Recipe',
description: 'Test description',
videoUrl: null,
image: '/test/path/image.jpg',
image: recipeImage.path,
ingredients: null,
steps: [
Step(
description: 'Step 1 description',
image: null,
),
const Step(description: 'Step 1 description', image: null),
null, // null шаг должен быть отфильтрован
Step(
description: 'Step 3 description',
image: '',
),
const Step(description: 'Step 3 description', image: ''),
],
tags: null,
totalCookingTime: 30,
@@ -478,24 +509,19 @@ void main() {
energyPerServing: 300,
);
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
.thenAnswer((_) async => Future.value(mockHttpResponse));
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
try {
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
} catch (e) {
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
}
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
});
test('publishRecipe - удаляет пустые значения из formMap', () async {
const recipe = RecipeModel(
final recipe = RecipeModel(
type: 'main',
title: 'Test Recipe',
description: '', // Пустое описание должно быть удалено
videoUrl: '', // Пустой videoUrl должен быть удален
image: '/test/path/image.jpg',
image: recipeImage.path,
ingredients: null,
steps: null,
tags: null,
@@ -505,25 +531,20 @@ void main() {
energyPerServing: null,
);
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
.thenAnswer((_) async => Future.value(mockHttpResponse));
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
try {
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
} catch (e) {
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
}
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
});
test('updateRecipe - пробрасывает исключение при ошибке', () async {
const slug = 'recipe-slug';
const recipe = RecipeModel(
final recipe = RecipeModel(
type: 'main',
title: 'Test Recipe',
description: 'Test description',
videoUrl: null,
image: '/test/path/image.jpg',
image: recipeImage.path,
ingredients: null,
steps: null,
tags: null,
@@ -533,16 +554,132 @@ void main() {
energyPerServing: 300,
);
when(() => mockService.updateRecipe(slug: slug, formData: any(named: 'formData')))
.thenThrow(Exception('Error'));
when(
() => mockService.updateRecipe(
slug: slug,
formData: any(named: 'formData'),
),
).thenThrow(Exception('Error'));
try {
await repository.publishRecipe(recipe, true, slug);
fail('Should have thrown exception');
} catch (e) {
expect(e, isA<Exception>());
}
expect(() => repository.publishRecipe(recipe, true, slug), throwsA(isA<Exception>()));
});
test('publishRecipe - собирает steps с изображением (MultipartFile)', () async {
final recipe = RecipeModel(
type: 'main',
title: 'Test Recipe',
description: 'Test description',
videoUrl: null,
image: recipeImage.path,
ingredients: null,
steps: [Step(description: 'Step with image', image: stepImage.path)],
tags: null,
totalCookingTime: 30,
activeCookTime: 15,
servings: 4,
energyPerServing: 300,
);
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
await repository.publishRecipe(recipe);
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
});
test('saveAsDraft - создает draft (createDraftedRecipe)', () async {
final recipe = RecipeModel(
type: 'main',
title: 'Draft title',
description: 'Draft desc',
videoUrl: null,
image: recipeImage.path,
ingredients: null,
steps: null,
tags: const ['tag1'],
totalCookingTime: 30,
activeCookTime: 15,
servings: 2,
energyPerServing: 200,
);
when(() => mockService.createDraftedRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
await repository.saveAsDraft(recipe);
verify(() => mockService.createDraftedRecipe(formData: any(named: 'formData'))).called(1);
verifyNever(
() => mockService.updateRecipe(
slug: any(named: 'slug'),
formData: any(named: 'formData'),
),
);
});
test('saveAsDraft - обновляет draft (updateRecipe)', () async {
const slug = 'draft-slug';
final recipe = RecipeModel(
type: 'main',
title: 'Draft title',
description: 'Draft desc',
videoUrl: null,
image: recipeImage.path,
ingredients: null,
steps: null,
tags: null,
totalCookingTime: 30,
activeCookTime: 15,
servings: 2,
energyPerServing: 200,
);
when(
() => mockService.updateRecipe(
slug: slug,
formData: any(named: 'formData'),
),
).thenAnswer((_) async => Future.value(mockHttpResponse));
await repository.saveAsDraft(recipe, true, slug);
verify(
() => mockService.updateRecipe(
slug: slug,
formData: any(named: 'formData'),
),
).called(1);
});
test('saveAsDraft - update=true без slug кидает исключение', () async {
final recipe = RecipeModel(
type: 'main',
title: 'Draft title',
description: 'Draft desc',
videoUrl: null,
image: recipeImage.path,
ingredients: null,
steps: null,
tags: null,
totalCookingTime: 30,
activeCookTime: 15,
servings: 2,
energyPerServing: 200,
);
// slug! должен упасть
expect(() => repository.saveAsDraft(recipe, true, null), throwsA(anything));
});
test('draftExistingRecipe - вызывает draftRecipe', () async {
const id = 123;
when(() => mockService.draftRecipe(id: id)).thenAnswer((_) async => Future.value());
await repository.draftExistingRecipe(id);
verify(() => mockService.draftRecipe(id: id)).called(1);
});
test('draftExistingRecipe - пробрасывает исключение при ошибке', () async {
const id = 123;
when(() => mockService.draftRecipe(id: id)).thenThrow(Exception('Error'));
expect(() => repository.draftExistingRecipe(id), throwsA(isA<Exception>()));
});
});
}

View File

@@ -3,6 +3,7 @@ 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/dev_tools/presentation/dev_tools_screen.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -16,6 +17,7 @@ void main() {
setUp(() async {
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
sl.registerLazySingleton<Dio>(() => Dio(BaseOptions(baseUrl: 'http://localhost')));
});
Widget wrap(Widget child) {
@@ -39,5 +41,29 @@ void main() {
expect(find.byType(DevToolsScreenContent), findsOneWidget);
});
testWidgets('updates base url from input by done button', (tester) async {
await tester.pumpWidget(wrap(const DevToolsScreen()));
await tester.pump();
await tester.enterText(find.byType(TextFormField).first, 'HTTP://EXAMPLE.COM');
await tester.tap(find.byIcon(Icons.done));
await tester.pump();
expect(find.byType(DevToolsScreenContent), findsOneWidget);
});
testWidgets('toggle base url button is tappable', (tester) async {
await tester.pumpWidget(wrap(const DevToolsScreen()));
await tester.pump();
final toggleButton = find.text('Set production base url');
expect(toggleButton, findsOneWidget);
await tester.tap(toggleButton);
await tester.pump();
expect(find.byType(DevToolsScreenContent), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,107 @@
import 'package:eatvio_app/features/dev_tools/test_cases.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('DevTools TestCases bulk', () {
test('all case names are readable', () {
final cases = <TestCases>[
CreateRecipeTask(),
Test850task(),
GetChatRoomPreviews(),
GetChatRoom(),
SendMessage(),
GetChatMessages(),
GetChatRoomIdByUserSlug(),
GetBlogPostList(),
GetRecipes(),
GetDiets(),
GetPlans(),
GetBlogPostComments(),
SendBlogPostComment(),
GetDietsComments(),
SendDietsComment(),
GetPlansComments(),
SendPlansComment(),
GetRecipesComments(),
AuthServiceTest(),
DiaryServiceTest(),
BlockedUserServiceTest(),
PlanServiceTest(),
AboutRecipeServiceTest(),
BlogPostServiceTest(),
CalendarServiceTest(),
CardsServiceTest(),
ConsultantServiceTest(),
ConsultationServiceTest(),
CreateDietServiceTest(),
CreatePlanServiceTest(),
GlossaryServiceTest(),
MyDietsServiceTest(),
MyPlansServiceTest(),
MyRecipesServiceTest(),
NotificationsServiceTest(),
PlansServiceTest(),
ProfileServiceTest(),
RegistrationServiceTest(),
ResetPasswordServiceTest(),
SettingsServiceTest(),
WsServiceTest(),
];
for (final c in cases) {
expect(c.name, isNotEmpty);
}
});
test('run() does not throw for all cases', () async {
final cases = <TestCases>[
CreateRecipeTask(),
Test850task(),
GetChatRoomPreviews(),
GetChatRoom(),
SendMessage(),
GetChatMessages(),
GetChatRoomIdByUserSlug(),
GetBlogPostList(),
GetRecipes(),
GetDiets(),
GetPlans(),
GetBlogPostComments(),
SendBlogPostComment(),
GetDietsComments(),
SendDietsComment(),
GetPlansComments(),
SendPlansComment(),
GetRecipesComments(),
AuthServiceTest(),
DiaryServiceTest(),
BlockedUserServiceTest(),
PlanServiceTest(),
AboutRecipeServiceTest(),
BlogPostServiceTest(),
CalendarServiceTest(),
CardsServiceTest(),
ConsultantServiceTest(),
ConsultationServiceTest(),
CreateDietServiceTest(),
CreatePlanServiceTest(),
GlossaryServiceTest(),
MyDietsServiceTest(),
MyPlansServiceTest(),
MyRecipesServiceTest(),
NotificationsServiceTest(),
PlansServiceTest(),
ProfileServiceTest(),
RegistrationServiceTest(),
ResetPasswordServiceTest(),
SettingsServiceTest(),
WsServiceTest(),
];
for (final c in cases) {
final result = await c.run(baseUrl: 'http://localhost');
expect(result, isA<TestResult>());
}
});
});
}

View File

@@ -0,0 +1,121 @@
import 'package:eatvio_app/features/notifications/models/notification_model.dart';
import 'package:flutter_test/flutter_test.dart';
/// Smoke coverage for [fromJson] factories not exercised elsewhere — improves line coverage on large model file.
void main() {
const c = '2024-06-01T12:00:00Z';
final base = <String, dynamic>{'created_at': c};
final planAndUser = <String, dynamic>{'created_at': c, 'plan_name': 'p', 'user_name': 'u', 'renewal_date': '2024-06-02'};
final scoreLike = <String, dynamic>{
'created_at': c,
'score_key': 'k',
'scorable_id': 1,
'scorable_type': 'Type',
'score_value': 2,
};
final commentReply = <String, dynamic>{
'created_at': c,
'entity_name': 'e',
'commented_entity_id': 1,
'commented_entity_type': 'diet',
'author': 'a',
'author_id': 2,
'parrent_comment_id': 3,
};
final entityCommented = <String, dynamic>{
'created_at': c,
'entity_name': 'e',
'entity_id': 1,
'entity_type': 'diet',
'entity_traslated_type': 't',
'entity_url': 'u',
'author_name': 'a',
'author_id': 2,
'author_slug': 's',
};
group('CommentedEntityType.fromJson', () {
test('maps known and legacy keys', () {
expect(CommentedEntityType.fromJson('diet'), CommentedEntityType.diet);
expect(CommentedEntityType.fromJson('Modules\\UserGeneratedPublicContent\\Models\\Diet'), CommentedEntityType.diet);
expect(CommentedEntityType.fromJson('blog-post'), CommentedEntityType.blogPost);
expect(CommentedEntityType.fromJson('Modules\\UserGeneratedPublicContent\\Models\\BlogPost'), CommentedEntityType.blogPost);
expect(CommentedEntityType.fromJson('other'), CommentedEntityType.unknown);
});
});
group('notification fromJson bulk smoke', () {
test('executes remaining model factories with minimal JSON', () {
PlanSubscriptionExpiredNotification.fromJson(planAndUser, NotificationType.planSubscriptionExpired);
PlanSubscriptionRenewedNotification.fromJson(planAndUser, NotificationType.planSubscriptionRenewed);
ChosenPlanStartedToConsultantNotification.fromJson(base, NotificationType.chosenPlanStartedToConsultant);
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(base, NotificationType.chosenPlanTomorrowReminderToConsultant);
PlanSubscriptionPaidToAuthorNotification.fromJson(base, NotificationType.planSubscriptionPaidToAuthor);
ChosenPlanFinishedToConsultantNotification.fromJson(base, NotificationType.chosenPlanFinishedToConsultant);
ChosenPlanLastDayFinishedForUser.fromJson(base, NotificationType.chosenPlanLastDayFinishedForUser);
ChosenPlanConsultantLastDayStarted.fromJson(base, NotificationType.chosenPlanConsultantLastDayStarted);
ComplaintToModerationForUser.fromJson(base, NotificationType.complaintToModerationForUser);
CommentReplied.fromJson(commentReply, NotificationType.commentReplied);
ScoreAdded.fromJson(scoreLike, NotificationType.scoreAdded);
ScoreSubstractedNotification.fromJson(scoreLike, NotificationType.scoreSubstracted);
EntityCommented.fromJson(entityCommented, NotificationType.entityCommented);
UnknownNotification.fromJson(base, NotificationType.unknown);
ConsultantConfirmPlanSubscriptionToSubscriberNotification.fromJson(
base,
NotificationType.consultantConfirmPlanSubscriptionToSubscriber,
);
ClientPaidPlanSubscriptionToAuthorNotification.fromJson(base, NotificationType.clientPaidPlanSubscriptionToAuthor);
ConsultantLeftDiaryCommentNotification.fromJson(base, NotificationType.consultantLeftDiaryComment);
ConsultantConfirmPlanSubscriptionToAuthorNotification.fromJson(base, NotificationType.consultantConfirmPlanSubscriptionToAuthor);
ClientPaidPlanSubscriptionToSubscriberNotification.fromJson(base, NotificationType.clientPaidPlanSubscriptionToSubscriber);
ClientStartPlanSubscriptionToAuthorNotification.fromJson(base, NotificationType.clientStartPlanSubscriptionToAuthor);
ClientCancelPlanSubscriptionToAuthorNotification.fromJson(base, NotificationType.clientCancelPlanSubscriptionToAuthor);
ClientCancelPlanSubscriptionToSubscriberNotification.fromJson(base, NotificationType.clientCancelPlanSubscriptionToSubscriber);
UserDailyReportCompletedNotification.fromJson(base, NotificationType.userDailyReportCompleted);
StayOnTrackToCustomerNotification.fromJson(base, NotificationType.stayOnTrackToCustomer);
PhoneChangedNotification.fromJson(base, NotificationType.phoneChanged);
SubscriptionNotification.fromJson(base, NotificationType.subscription);
BadgeAwardedNotification.fromJson(base, NotificationType.badgeAwarded);
BadgeRevokedNotification.fromJson(base, NotificationType.badgeRevoked);
ConsultantRequestStartedNotification.fromJson(base, NotificationType.consultantRequestStarted);
ClientCloseComplaintSuccessNotification.fromJson(base, NotificationType.clientCloseComplaintSuccess);
ClientRefundSuccessNotification.fromJson(base, NotificationType.clientRefundSuccess);
ConsultantRefundSuccessNotification.fromJson(base, NotificationType.consultantRefundSuccess);
ClientRejectSuccessNotification.fromJson(base, NotificationType.clientRejectSuccess);
ConsultantRejectSuccessNotification.fromJson(base, NotificationType.consultantRejectSuccess);
ProductStatusUpdatedForAuthorNotification.fromJson(base, NotificationType.productStatusUpdatedForAuthor);
DiaryReminderForUsersNotification.fromJson(base, NotificationType.diaryReminderForUsers);
DiaryReminderForUsersNotification.fromJson({'created_at': c, 'data': <String, dynamic>{}}, NotificationType.diaryReminderForUsers);
PersonalDietIsDefinedToCustomerNotification.fromJson(base, NotificationType.personalDietIsDefinedToCustomer);
ConsultantEducationUpdatedNotification.fromJson(base, NotificationType.consultantEducationUpdated);
ComplaintRegisteredNotification.fromJson(base, NotificationType.complaintRegistered);
ContentUnpublishedNotification.fromJson(base, NotificationType.contentUnpublished);
FinanceQuestionCreatedNotification.fromJson(base, NotificationType.financeQuestionCreated);
NewChatMessageNotification.fromJson(base, NotificationType.newChatMessage);
CompleteRegistrationNotification.fromJson(base, NotificationType.completeRegistration);
ChosenDietExpiredNotification.fromJson(base, NotificationType.chosenDietExpired);
MotivateUserNotification.fromJson(base, NotificationType.motivateUser);
CommentComplaintNotification.fromJson(base, NotificationType.commentComplaint);
CommentDeletedNotification.fromJson(base, NotificationType.commentDeleted);
UserSubscribedNotification.fromJson(base, NotificationType.userSubscribed);
});
test('executes additional factories used by legacy and plan flows', () {
ClientStartPlanSubscriptionToSubscriberNotification.fromJson(base, NotificationType.clientStartPlanSubscriptionToSubscriber);
ChosenPlanFinishedToClientNotification.fromJson(base, NotificationType.chosenPlanFinishedToClient);
PlanSubscriptionCanceledToCustomerNotification.fromJson(base, NotificationType.planSubscriptionCanceledToCustomer);
PlanSubscriptionCanceledToAuthorNotification.fromJson(planAndUser, NotificationType.planSubscriptionCanceledToAuthor);
ChosenPlanStartedToClientNotification.fromJson(planAndUser, NotificationType.chosenPlanStartedToClient);
ChosenPlanTomorrowReminderToClientNotification.fromJson(planAndUser, NotificationType.chosenPlanTomorrowReminderToClient);
DiaryReminderNotification.fromJson(base, NotificationType.diaryReminderNotification);
ConsultantLeftCommentNotification.fromJson(base, NotificationType.consultantLeftComment);
PersonalizedDietIsDefinedNotification.fromJson(base, NotificationType.personalizedDietIsDefined);
PlanSubscriptionStartedToAuthorNotification.fromJson(planAndUser, NotificationType.planSubscriptionStartedToAuthor);
});
});
}

View File

@@ -71,7 +71,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
expect(aboutPlanCubit!.state.planModel, isNotNull);
expect(aboutPlanCubit!.state.planModel!.id, 1);
@@ -157,7 +157,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => {});
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
await aboutPlanCubit!.likePlan(index: null, slug: 'test-plan');
await Future.delayed(const Duration(milliseconds: 1200));
@@ -170,7 +170,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenThrow(Exception('Ошибка загрузки'));
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
expect(aboutPlanCubit!.state.error, isNotNull);
expect(aboutPlanCubit!.state.isLoading, isFalse);
@@ -220,7 +220,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => {});
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
await aboutPlanCubit!.likePlan(index: null, slug: 'test-plan');
expect(aboutPlanCubit!.state.planModel!.wasLikedByMe, isFalse);
@@ -263,7 +263,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.addToFavorite('test-plan')).thenAnswer((_) async => Future<void>.value());
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
await aboutPlanCubit!.addToFavorite(slug: 'test-plan', index: null);
expect(aboutPlanCubit!.state.planModel!.isFavorite, true);
@@ -305,7 +305,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.removeFromFavorite('test-plan')).thenAnswer((_) async => Future<void>.value());
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
await aboutPlanCubit!.removeFromFavorite(slug: 'test-plan', index: null);
expect(aboutPlanCubit!.state.planModel!.isFavorite, false);
@@ -357,7 +357,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.chosePlan('test-plan', any)).thenAnswer((_) async => chosenPlan);
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
aboutPlanCubit!.choseOption(0);
final result = await aboutPlanCubit!.chosePlan();
@@ -405,7 +405,7 @@ void main() {
),
);
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
aboutPlanCubit!.choseOption(0);
final result = await aboutPlanCubit!.chosePlan();
@@ -447,7 +447,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => {});
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
await aboutPlanCubit!.likePlan(index: 5, slug: 'test-plan');
expect(aboutPlanCubit!.state.planModel!.wasLikedByMe, isTrue);
@@ -490,7 +490,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => {});
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
await aboutPlanCubit!.likePlan(index: 5, slug: 'test-plan');
expect(aboutPlanCubit!.state.planModel!.wasLikedByMe, isFalse);
@@ -533,7 +533,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.likePlan('test-plan')).thenThrow(Exception('Ошибка'));
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
await aboutPlanCubit!.likePlan(index: 5, slug: 'test-plan');
// Проверяем, что состояние изменилось сразу после вызова
@@ -583,7 +583,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.addToFavorite('test-plan')).thenAnswer((_) async => Future<void>.value());
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
await aboutPlanCubit!.addToFavorite(slug: 'test-plan', index: 5);
expect(aboutPlanCubit!.state.planModel!.isFavorite, true);
@@ -624,7 +624,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.addToFavorite('test-plan')).thenThrow(Exception('Ошибка'));
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
await aboutPlanCubit!.addToFavorite(slug: 'test-plan', index: 5);
// Проверяем, что состояние изменилось сразу после вызова
@@ -673,7 +673,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.removeFromFavorite('test-plan')).thenAnswer((_) async => Future<void>.value());
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
await aboutPlanCubit!.removeFromFavorite(slug: 'test-plan', index: 5);
expect(aboutPlanCubit!.state.planModel!.isFavorite, false);
@@ -714,7 +714,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.removeFromFavorite('test-plan')).thenThrow(Exception('Ошибка'));
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
await aboutPlanCubit!.removeFromFavorite(slug: 'test-plan', index: 5);
// Проверяем, что состояние изменилось сразу после вызова
@@ -773,7 +773,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.chosePlan('test-plan', any)).thenAnswer((_) async => chosenPlan);
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
aboutPlanCubit!.choseOption(1);
final result = await aboutPlanCubit!.chosePlan();
@@ -826,7 +826,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.chosePlan('test-plan', any)).thenAnswer((_) async => chosenPlan);
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
aboutPlanCubit!.choseOption(2);
final result = await aboutPlanCubit!.chosePlan();
@@ -884,7 +884,7 @@ void main() {
),
);
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
aboutPlanCubit!.choseOption(0);
final result = await aboutPlanCubit!.chosePlan();
@@ -926,7 +926,7 @@ void main() {
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
when(mockPlanRepository.chosePlan('test-plan', any)).thenThrow(Exception('Общая ошибка'));
await aboutPlanCubit!.getPlan('test-plan');
await aboutPlanCubit!.getPlan(slug: 'test-plan');
aboutPlanCubit!.choseOption(0);
final result = await aboutPlanCubit!.chosePlan();

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bookmark_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/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
@@ -167,7 +168,7 @@ void main() {
when(planCubit.state).thenReturn(state);
when(planCubit.stream).thenAnswer((_) => const Stream<AboutPlanState>.empty());
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
when(planCubit.getPlan(any)).thenAnswer((_) async => Future<void>.value());
when(planCubit.getPlan(slug: anyNamed('slug'), index: anyNamed('index'))).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
@@ -183,6 +184,95 @@ void main() {
expect(find.byType(AboutPlanScreen), findsOneWidget);
});
testWidgets('error state try again calls getPlan', (tester) async {
final planCubit = MockAboutPlanCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
const state = AboutPlanState(isLoading: false, error: 'Error loading plan', planModel: testPlan, chosenOptionIndex: 0);
when(planCubit.state).thenReturn(state);
when(planCubit.stream).thenAnswer((_) => const Stream<AboutPlanState>.empty());
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
when(planCubit.getPlan(slug: anyNamed('slug'), index: anyNamed('index'))).thenAnswer((_) async => Future<void>.value());
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 tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
await tester.pump();
await tester.tap(find.text('Попробовать снова'));
await tester.pump();
verify(planCubit.getPlan(slug: 'test', index: null)).called(1);
});
testWidgets('loading state back button does not throw', (tester) async {
final planCubit = MockAboutPlanCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
const state = AboutPlanState(isLoading: true, error: null, planModel: null, chosenOptionIndex: 0);
when(planCubit.state).thenReturn(state);
when(planCubit.stream).thenAnswer((_) => const Stream<AboutPlanState>.empty());
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
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 tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
await tester.pump();
await tester.tap(find.byType(BackButton));
await tester.pump();
expect(find.byType(AboutPlanScreen), findsOneWidget);
});
testWidgets('loaded state bookmark tap calls addToFavorite when not favorite', (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.addToFavorite(slug: anyNamed('slug'), index: anyNamed('index'))).thenAnswer((_) async => Future<void>.value());
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-plan', navigateToComments: false, index: 7, highlightedCommentId: null)));
await tester.pump();
await tester.pump();
});
await tester.tap(find.byType(BookmarkButton));
await tester.pump();
verify(planCubit.addToFavorite(slug: 'test-plan', index: 7)).called(1);
});
testWidgets('renders with navigateToComments true', (tester) async {
final planCubit = MockAboutPlanCubit();
final feedCubit = MockFeedCubit();

View File

@@ -194,6 +194,63 @@ void main() {
expect(find.byType(AboutRecipeScreen), findsOneWidget);
});
testWidgets('error state try again calls getRecipe', (tester) async {
final recipeCubit = MockAboutRecipeCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
const state = AboutRecipeState.error(message: 'Error loading recipe');
when(recipeCubit.state).thenReturn(state);
when(recipeCubit.stream).thenAnswer((_) => const Stream<AboutRecipeState>.empty());
when(recipeCubit.close()).thenAnswer((_) async => Future<void>.value());
when(recipeCubit.getRecipe(any, index: anyNamed('index'))).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewRecipe(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(recipeCubit, commentsCubit);
await tester.pumpWidget(wrap(recipeCubit, feedCubit, commentsCubit, const AboutRecipeScreen(recipeSlug: 'test', index: 3, navigateToComments: false)));
await tester.pump();
await tester.tap(find.text('Попробовать снова'));
await tester.pump();
verify(recipeCubit.getRecipe('test', index: 3)).called(1);
});
testWidgets('loading state back button does not throw', (tester) async {
final recipeCubit = MockAboutRecipeCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
const state = AboutRecipeState.loading();
when(recipeCubit.state).thenReturn(state);
when(recipeCubit.stream).thenAnswer((_) => const Stream<AboutRecipeState>.empty());
when(recipeCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewRecipe(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(recipeCubit, commentsCubit);
await tester.pumpWidget(wrap(recipeCubit, feedCubit, commentsCubit, const AboutRecipeScreen(recipeSlug: 'test', index: null, navigateToComments: false)));
await tester.pump();
await tester.tap(find.byType(BackButton));
await tester.pump();
expect(find.byType(AboutRecipeScreen), findsOneWidget);
});
testWidgets('renders with navigateToComments true', (tester) async {
final recipeCubit = MockAboutRecipeCubit();
final feedCubit = MockFeedCubit();