feat(tests): add fixtures

This commit is contained in:
mso7nueh
2025-11-20 16:58:56 +03:00
parent 6fa944b3df
commit 4deda92c5e
24 changed files with 6852 additions and 4358 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,27 @@
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'auth_cubit_test.mocks.dart';
import '../../../../fixtures/doubles/auth_doubles.dart';
import '../../../../fixtures/fixture_reader.dart';
@GenerateNiceMocks([MockSpec<AuthRepository>()])
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('AuthCubit', () {
AuthCubit? authCubit;
late MockAuthRepository mockAuthRepository;
late FixtureAuthRepository authRepository;
late Map<String, dynamic> credentialsFixture;
const testToken = TokenModel(token: 'test_token_12345');
late TokenModel tokenFixture;
setUp(() async {
mockAuthRepository = MockAuthRepository();
tokenFixture = TokenModel.fromJson(FixtureReader.readAsMap('auth/token_response.json'));
credentialsFixture = FixtureReader.readAsMap('auth/login_credentials.json');
authRepository = FixtureAuthRepository(tokenFixture: tokenFixture);
SharedPreferences.setMockInitialValues({});
// Mock Fluttertoast plugin
@@ -50,31 +50,37 @@ void main() {
});
test('начальное состояние - AuthState', () {
authCubit = AuthCubit(authRepository: mockAuthRepository);
authCubit = AuthCubit(authRepository: authRepository);
expect(authCubit!.state, const AuthState());
});
test('getTokenByCredentials - успешная авторизация', () async {
authCubit = AuthCubit(authRepository: mockAuthRepository);
when(mockAuthRepository.getTokenByCredentials(any, any))
.thenAnswer((_) async => testToken);
authCubit = AuthCubit(authRepository: authRepository);
final result = await authCubit!.getTokenByCredentials('test@example.com', 'password123');
final result = await authCubit!.getTokenByCredentials(
credentialsFixture['email'] as String,
credentialsFixture['password'] as String,
);
expect(result, true);
await Future.delayed(const Duration(milliseconds: 100));
expect(authCubit!.state, isA<AuthStateLoaded>());
final loadedState = authCubit!.state as AuthStateLoaded;
expect(loadedState.token, 'test_token_12345');
verify(mockAuthRepository.getTokenByCredentials('test@example.com', 'password123')).called(1);
expect(loadedState.token, tokenFixture.token);
expect(
authRepository.credentialPayloads.single,
credentialsFixture.map((key, value) => MapEntry(key, value.toString())),
);
});
test('getTokenByCredentials - переход в состояние loading', () async {
authCubit = AuthCubit(authRepository: mockAuthRepository);
when(mockAuthRepository.getTokenByCredentials(any, any))
.thenAnswer((_) async => Future.delayed(const Duration(milliseconds: 50)).then((_) => testToken));
authCubit = AuthCubit(authRepository: authRepository);
authRepository.responseDelay = const Duration(milliseconds: 50);
final future = authCubit!.getTokenByCredentials('test@example.com', 'password123');
final future = authCubit!.getTokenByCredentials(
credentialsFixture['email'] as String,
credentialsFixture['password'] as String,
);
await Future.delayed(const Duration(milliseconds: 10));
expect(authCubit!.state, isA<AuthStateLoading>());
@@ -82,9 +88,8 @@ void main() {
});
test('getTokenByCredentials - обработка ошибки авторизации', () async {
authCubit = AuthCubit(authRepository: mockAuthRepository);
when(mockAuthRepository.getTokenByCredentials(any, any))
.thenThrow(Exception('Неверный адрес электронной почты или пароль'));
authCubit = AuthCubit(authRepository: authRepository);
authRepository.error = Exception('Неверный адрес электронной почты или пароль');
final result = await authCubit!.getTokenByCredentials('wrong@example.com', 'wrongpassword');
@@ -93,13 +98,15 @@ void main() {
expect(authCubit!.state, isA<AuthStateError>());
final errorState = authCubit!.state as AuthStateError;
expect(errorState.errorMessage, contains('Exception'));
verify(mockAuthRepository.getTokenByCredentials('wrong@example.com', 'wrongpassword')).called(1);
expect(
authRepository.credentialPayloads.single,
{'email': 'wrong@example.com', 'password': 'wrongpassword'},
);
});
test('getTokenByCredentials - обработка различных типов ошибок', () async {
authCubit = AuthCubit(authRepository: mockAuthRepository);
when(mockAuthRepository.getTokenByCredentials(any, any))
.thenThrow(const FormatException('Invalid credentials format'));
authCubit = AuthCubit(authRepository: authRepository);
authRepository.error = const FormatException('Invalid credentials format');
final result = await authCubit!.getTokenByCredentials('invalid', 'pass');
@@ -111,27 +118,35 @@ void main() {
});
test('getTokenByCredentials - проверка вызова с правильными параметрами', () async {
authCubit = AuthCubit(authRepository: mockAuthRepository);
when(mockAuthRepository.getTokenByCredentials(any, any))
.thenAnswer((_) async => testToken);
authCubit = AuthCubit(authRepository: authRepository);
await authCubit!.getTokenByCredentials('user@test.com', 'securePassword123');
verify(mockAuthRepository.getTokenByCredentials('user@test.com', 'securePassword123')).called(1);
verifyNever(mockAuthRepository.getTokenByCredentials('other@test.com', 'otherPassword'));
expect(
authRepository.credentialPayloads.single,
{'email': 'user@test.com', 'password': 'securePassword123'},
);
expect(
authRepository.credentialPayloads.where((call) => call['email'] == 'other@test.com'),
isEmpty,
);
});
test('getTokenByCredentials - множественные вызовы', () async {
authCubit = AuthCubit(authRepository: mockAuthRepository);
when(mockAuthRepository.getTokenByCredentials(any, any))
.thenAnswer((_) async => testToken);
authCubit = AuthCubit(authRepository: authRepository);
await authCubit!.getTokenByCredentials('first@test.com', 'password1');
await Future.delayed(const Duration(milliseconds: 100));
await authCubit!.getTokenByCredentials('second@test.com', 'password2');
verify(mockAuthRepository.getTokenByCredentials('first@test.com', 'password1')).called(1);
verify(mockAuthRepository.getTokenByCredentials('second@test.com', 'password2')).called(1);
expect(authRepository.credentialPayloads.length, 2);
expect(
authRepository.credentialPayloads,
[
{'email': 'first@test.com', 'password': 'password1'},
{'email': 'second@test.com', 'password': 'password2'},
],
);
});
});
}

View File

@@ -2,24 +2,27 @@
import 'package:eatvio_app/app_keys.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'basic_widget_test.mocks.dart';
import '../../../../fixtures/doubles/auth_doubles.dart';
import '../../../../fixtures/fixture_reader.dart';
@GenerateMocks([AuthRepository])
void main() {
group('Auth Basic Widget Tests', () {
late MockAuthRepository mockAuthRepository;
late FixtureAuthRepository authRepository;
late AuthCubit authCubit;
late Map<String, dynamic> credentialsFixture;
setUp(() {
mockAuthRepository = MockAuthRepository();
authCubit = AuthCubit(authRepository: mockAuthRepository);
credentialsFixture = FixtureReader.readAsMap('auth/login_credentials.json');
authRepository = FixtureAuthRepository(
tokenFixture: TokenModel.fromJson(FixtureReader.readAsMap('auth/token_response.json')),
);
authCubit = AuthCubit(authRepository: authRepository);
});
tearDown(() {
@@ -38,7 +41,7 @@ void main() {
testWidgets('should create AuthCubit successfully', (WidgetTester tester) async {
// Arrange & Act
final cubit = AuthCubit(authRepository: mockAuthRepository);
final cubit = AuthCubit(authRepository: authRepository);
// Assert
expect(cubit, isA<AuthCubit>());
@@ -106,7 +109,7 @@ void main() {
await tester.pumpWidget(createTestWidget(emailWidget));
await tester.pumpAndSettle();
const testEmail = 'test@example.com';
final testEmail = credentialsFixture['email'] as String;
// Act
await tester.enterText(find.byKey(AppKeys.emailTextFieldAuthScreen), testEmail);
@@ -127,7 +130,7 @@ void main() {
await tester.pumpWidget(createTestWidget(passwordWidget));
await tester.pumpAndSettle();
const testPassword = 'password123';
final testPassword = credentialsFixture['password'] as String;
// Act
await tester.enterText(find.byKey(AppKeys.passwordTextFieldAuthScreen), testPassword);

View File

@@ -2,26 +2,27 @@ import 'package:eatvio_app/app_keys.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'advanced_widget_test.mocks.dart';
import '../../fixtures/doubles/blog_post_doubles.dart';
import '../../fixtures/fixture_reader.dart';
@GenerateMocks([BlogPostRepository])
void main() {
group('Blog Post Creation Advanced Widget Tests', () {
late MockBlogPostRepository mockBlogPostRepository;
late FixtureBlogPostRepository blogPostRepository;
late BlogPostCreateCubit blogPostCreateCubit;
late Map<String, dynamic> fullCreateFixture;
late Map<String, dynamic> updateFixture;
setUp(() {
mockBlogPostRepository = MockBlogPostRepository();
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: mockBlogPostRepository);
blogPostRepository = FixtureBlogPostRepository();
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: blogPostRepository);
fullCreateFixture = FixtureReader.readAsMap('blog_post_create/create_request_full.json');
updateFixture = FixtureReader.readAsMap('blog_post_create/update_request.json');
});
tearDown(() {
@@ -40,8 +41,6 @@ void main() {
testWidgets('should handle blog post creation with all fields', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
Widget formWidget = Column(
children: [
TextFormField(
@@ -65,9 +64,9 @@ void main() {
key: AppKeys.blogPostCreateScreenCreateButton,
onPressed: () async {
await blogPostCreateCubit.createBlogPost(
title: 'Test Title',
description: 'Test Description',
videoUrl: 'https://youtube.com/watch?v=test',
title: fullCreateFixture['title'] as String?,
description: fullCreateFixture['description_md'] as String?,
videoUrl: fullCreateFixture['video_url'] as String?,
image: null,
);
},
@@ -80,21 +79,25 @@ void main() {
await tester.pumpWidget(createTestWidget(formWidget));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Test Title');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Test Description');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=test');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), fullCreateFixture['title'] as String);
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), fullCreateFixture['description_md'] as String);
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), fullCreateFixture['video_url'] as String);
await tester.pump();
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
await tester.pump();
// Assert
verify(mockBlogPostRepository.createBlogPost('Test Title', 'https://youtube.com/watch?v=test', 'Test Description', null)).called(1);
expect(
blogPostRepository.createPayloads.single.toJson(),
fullCreateFixture,
);
});
testWidgets('should handle blog post creation with image', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
final expectedPayload = Map<String, dynamic>.from(fullCreateFixture);
expectedPayload['video_url'] = null;
Widget formWidget = Column(
children: [
@@ -137,8 +140,8 @@ void main() {
key: AppKeys.blogPostCreateScreenCreateButton,
onPressed: () async {
await blogPostCreateCubit.createBlogPost(
title: 'Test Title',
description: 'Test Description',
title: fullCreateFixture['title'] as String?,
description: fullCreateFixture['description_md'] as String?,
videoUrl: null,
image: null, // В реальном тесте здесь был бы XFile
);
@@ -152,21 +155,19 @@ void main() {
await tester.pumpWidget(createTestWidget(formWidget));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Test Title');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Test Description');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), fullCreateFixture['title'] as String);
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), fullCreateFixture['description_md'] as String);
await tester.pump();
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
await tester.pump();
// Assert
verify(mockBlogPostRepository.createBlogPost('Test Title', null, 'Test Description', null)).called(1);
expect(blogPostRepository.createPayloads.single.toJson(), expectedPayload);
});
testWidgets('should handle blog post update', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.updateBlogPost(any, any, any, any, any, any)).thenAnswer((_) async {});
final testBlogPost = FeedBlogPostModel(
id: 1,
state: 'published',
@@ -209,12 +210,12 @@ void main() {
key: AppKeys.blogPostCreateScreenCreateButton,
onPressed: () async {
await blogPostCreateCubit.updateBlogPost(
id: testBlogPost.id,
title: 'Updated Title',
description: 'Updated Description',
videoUrl: 'https://youtube.com/watch?v=updated',
id: updateFixture['id'] as int,
title: updateFixture['title'] as String?,
description: updateFixture['description_md'] as String?,
videoUrl: updateFixture['video_url'] as String?,
image: null,
oldImageUrl: testBlogPost.image,
oldImageUrl: updateFixture['old_image_url'] as String?,
);
},
child: const Text('Обновить'),
@@ -226,24 +227,20 @@ void main() {
await tester.pumpWidget(createTestWidget(formWidget));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Updated Title');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Updated Description');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=updated');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), updateFixture['title'] as String);
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), updateFixture['description_md'] as String);
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), updateFixture['video_url'] as String);
await tester.pump();
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
await tester.pump();
// Assert
verify(
mockBlogPostRepository.updateBlogPost(1, 'Updated Title', 'https://youtube.com/watch?v=updated', 'Updated Description', null, 'original_image.jpg'),
).called(1);
expect(blogPostRepository.updatePayloads.single.toJson(), updateFixture);
});
testWidgets('should handle blog post deletion', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.deleteBlogPost(any)).thenAnswer((_) async {});
Widget deleteWidget = ElevatedButton(
key: const Key('deleteBlogPostButton'),
onPressed: () async {
@@ -260,14 +257,12 @@ void main() {
await tester.pump();
// Assert
verify(mockBlogPostRepository.deleteBlogPost(1)).called(1);
expect(blogPostRepository.deleteCalls.single, 1);
});
testWidgets('should handle loading state during blog post creation', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {
await Future.delayed(const Duration(milliseconds: 50));
});
blogPostRepository.createDelay = const Duration(milliseconds: 50);
Widget loadingWidget = BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
builder: (context, state) {
@@ -283,7 +278,12 @@ void main() {
await tester.pumpAndSettle();
// Trigger loading state
blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: 'Test Description', image: null);
blogPostCreateCubit.createBlogPost(
title: fullCreateFixture['title'] as String?,
videoUrl: null,
description: fullCreateFixture['description_md'] as String?,
image: null,
);
await tester.pump();
await tester.pump(); // Need to pump twice to get the loading state
@@ -297,7 +297,7 @@ void main() {
testWidgets('should handle error state during blog post creation', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenThrow(Exception('Network error'));
blogPostRepository.createError = Exception('Network error');
Widget errorWidget = BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
builder: (context, state) {
@@ -313,7 +313,12 @@ void main() {
await tester.pumpAndSettle();
// Trigger error state
blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: 'Test Description', image: null);
blogPostCreateCubit.createBlogPost(
title: fullCreateFixture['title'] as String?,
videoUrl: null,
description: fullCreateFixture['description_md'] as String?,
image: null,
);
await tester.pump();
await tester.pump(); // Need to pump twice to get the error state
@@ -323,8 +328,6 @@ void main() {
testWidgets('should handle success state after blog post creation', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
Widget successWidget = BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
builder: (context, state) {
if (state is BlogPostCreateStateLoaded) {
@@ -339,7 +342,12 @@ void main() {
await tester.pumpAndSettle();
// Trigger success state
blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: 'Test Description', image: null);
blogPostCreateCubit.createBlogPost(
title: fullCreateFixture['title'] as String?,
videoUrl: null,
description: fullCreateFixture['description_md'] as String?,
image: null,
);
await tester.pump();
await tester.pump(); // Need to pump twice to get the success state
@@ -470,7 +478,10 @@ void main() {
await tester.pump();
// Assert
verify(mockBlogPostRepository.createBlogPost('', '', '', null)).called(1);
expect(
blogPostRepository.createPayloads.single.toJson(),
{'title': '', 'description_md': '', 'video_url': '', 'image_name': null},
);
});
testWidgets('should handle blog post creation with long content', (WidgetTester tester) async {
@@ -480,8 +491,6 @@ void main() {
'Оно может содержать несколько абзацев и различные форматирования. '
'Пользователи должны иметь возможность создавать подробные и информативные посты.';
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
Widget longContentWidget = Column(
children: [
TextFormField(
@@ -518,7 +527,15 @@ void main() {
await tester.pump();
// Assert
verify(mockBlogPostRepository.createBlogPost('Test Title', null, longContent, null)).called(1);
expect(
blogPostRepository.createPayloads.single.toJson(),
{
'title': 'Test Title',
'description_md': longContent,
'video_url': null,
'image_name': null,
},
);
});
});
}

View File

@@ -1,23 +1,20 @@
import 'package:eatvio_app/app_keys.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'basic_widget_test.mocks.dart';
import '../../fixtures/doubles/blog_post_doubles.dart';
@GenerateMocks([BlogPostRepository])
void main() {
group('Blog Post Creation Basic Widget Tests', () {
late MockBlogPostRepository mockBlogPostRepository;
late FixtureBlogPostRepository blogPostRepository;
late BlogPostCreateCubit blogPostCreateCubit;
setUp(() {
mockBlogPostRepository = MockBlogPostRepository();
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: mockBlogPostRepository);
blogPostRepository = FixtureBlogPostRepository();
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: blogPostRepository);
});
tearDown(() {
@@ -36,7 +33,7 @@ void main() {
testWidgets('should create BlogPostCreateCubit successfully', (WidgetTester tester) async {
// Arrange & Act
final cubit = BlogPostCreateCubit(blogPostRepository: mockBlogPostRepository);
final cubit = BlogPostCreateCubit(blogPostRepository: blogPostRepository);
// Assert
expect(cubit, isA<BlogPostCreateCubit>());

View File

@@ -2,34 +2,29 @@ import 'package:eatvio_app/app_keys.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'integration_widget_test.mocks.dart';
import '../../fixtures/doubles/blog_post_doubles.dart';
import '../../fixtures/fixture_reader.dart';
@GenerateMocks([BlogPostRepository, FeedCubit])
void main() {
group('Blog Post Creation Integration Widget Tests', () {
late MockBlogPostRepository mockBlogPostRepository;
late MockFeedCubit mockFeedCubit;
late FixtureBlogPostRepository blogPostRepository;
late BlogPostCreateCubit blogPostCreateCubit;
late FeedReloadProbe feedProbe;
late Map<String, dynamic> fullCreateFixture;
late Map<String, dynamic> updateFixture;
setUp(() {
mockBlogPostRepository = MockBlogPostRepository();
mockFeedCubit = MockFeedCubit();
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: mockBlogPostRepository);
// Setup mock stubs for FeedCubit
when(mockFeedCubit.stream).thenAnswer((_) => Stream.value(const FeedState.loading()));
when(mockFeedCubit.close()).thenAnswer((_) async {});
blogPostRepository = FixtureBlogPostRepository();
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: blogPostRepository);
feedProbe = FeedReloadProbe();
fullCreateFixture = FixtureReader.readAsMap('blog_post_create/create_request_full.json');
updateFixture = FixtureReader.readAsMap('blog_post_create/update_request.json');
});
tearDown(() {
@@ -39,11 +34,8 @@ void main() {
Widget createTestWidget(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: MultiBlocProvider(
providers: [
BlocProvider<BlogPostCreateCubit>(create: (context) => blogPostCreateCubit),
BlocProvider<FeedCubit>(create: (context) => mockFeedCubit),
],
home: BlocProvider<BlogPostCreateCubit>.value(
value: blogPostCreateCubit,
child: Scaffold(body: SingleChildScrollView(child: child)),
),
);
@@ -51,13 +43,10 @@ void main() {
testWidgets('should complete full blog post creation flow', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
when(mockFeedCubit.loadFeedBlogPostList()).thenAnswer((_) async {});
Widget fullFlowWidget = BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
listener: (context, state) {
if (state is BlogPostCreateStateLoaded) {
context.read<FeedCubit>().loadFeedBlogPostList();
feedProbe.loadFeedBlogPostList();
}
},
child: Column(
@@ -106,9 +95,9 @@ void main() {
key: AppKeys.blogPostCreateScreenCreateButton,
onPressed: () async {
await blogPostCreateCubit.createBlogPost(
title: 'Integration Test Title',
videoUrl: 'https://youtube.com/watch?v=integration',
description: 'Integration Test Description',
title: fullCreateFixture['title'] as String?,
videoUrl: fullCreateFixture['video_url'] as String?,
description: fullCreateFixture['description_md'] as String?,
image: null,
);
},
@@ -122,26 +111,21 @@ void main() {
await tester.pumpWidget(createTestWidget(fullFlowWidget));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Integration Test Title');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Integration Test Description');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=integration');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), fullCreateFixture['title'] as String);
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), fullCreateFixture['description_md'] as String);
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), fullCreateFixture['video_url'] as String);
await tester.pump();
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
await tester.pump();
// Assert
verify(
mockBlogPostRepository.createBlogPost('Integration Test Title', 'https://youtube.com/watch?v=integration', 'Integration Test Description', null),
).called(1);
verify(mockFeedCubit.loadFeedBlogPostList()).called(1);
expect(blogPostRepository.createPayloads.single.toJson(), fullCreateFixture);
expect(feedProbe.loadRequests, 1);
});
testWidgets('should handle blog post update flow', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.updateBlogPost(any, any, any, any, any, any)).thenAnswer((_) async {});
when(mockFeedCubit.loadFeedBlogPostList()).thenAnswer((_) async {});
final existingBlogPost = FeedBlogPostModel(
id: 1,
state: 'published',
@@ -161,7 +145,7 @@ void main() {
Widget updateFlowWidget = BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
listener: (context, state) {
if (state is BlogPostCreateStateLoaded) {
context.read<FeedCubit>().loadFeedBlogPostList();
feedProbe.loadFeedBlogPostList();
}
},
child: Column(
@@ -190,12 +174,12 @@ void main() {
key: AppKeys.blogPostCreateScreenCreateButton,
onPressed: () async {
await blogPostCreateCubit.updateBlogPost(
id: existingBlogPost.id,
title: 'Updated Title',
videoUrl: 'https://youtube.com/watch?v=updated',
description: 'Updated Description',
id: updateFixture['id'] as int,
title: updateFixture['title'] as String?,
videoUrl: updateFixture['video_url'] as String?,
description: updateFixture['description_md'] as String?,
image: null,
oldImageUrl: existingBlogPost.image,
oldImageUrl: updateFixture['old_image_url'] as String?,
);
},
child: const Text('Обновить'),
@@ -208,26 +192,21 @@ void main() {
await tester.pumpWidget(createTestWidget(updateFlowWidget));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Updated Title');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Updated Description');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=updated');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), updateFixture['title'] as String);
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), updateFixture['description_md'] as String);
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), updateFixture['video_url'] as String);
await tester.pump();
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
await tester.pump();
// Assert
verify(
mockBlogPostRepository.updateBlogPost(1, 'Updated Title', 'https://youtube.com/watch?v=updated', 'Updated Description', null, 'original_image.jpg'),
).called(1);
verify(mockFeedCubit.loadFeedBlogPostList()).called(1);
expect(blogPostRepository.updatePayloads.single.toJson(), updateFixture);
expect(feedProbe.loadRequests, 1);
});
testWidgets('should handle blog post deletion flow', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.deleteBlogPost(any)).thenAnswer((_) async {});
when(mockFeedCubit.loadFeedBlogPostList()).thenAnswer((_) async {});
Widget deleteFlowWidget = Column(
children: [
const Text('Удалить блог пост?'),
@@ -236,7 +215,7 @@ void main() {
key: const Key('deleteBlogPostButton'),
onPressed: () async {
await blogPostCreateCubit.deleteBlogPost(1);
mockFeedCubit.loadFeedBlogPostList();
feedProbe.loadFeedBlogPostList();
},
child: const Text('Удалить'),
),
@@ -251,13 +230,13 @@ void main() {
await tester.pump();
// Assert
verify(mockBlogPostRepository.deleteBlogPost(1)).called(1);
verify(mockFeedCubit.loadFeedBlogPostList()).called(1);
expect(blogPostRepository.deleteCalls.single, 1);
expect(feedProbe.loadRequests, 1);
});
testWidgets('should handle error flow during blog post creation', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenThrow(Exception('Network error'));
blogPostRepository.createError = Exception('Network error');
Widget errorFlowWidget = BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
builder: (context, state) {
@@ -285,7 +264,12 @@ void main() {
await tester.pumpAndSettle();
// Trigger error
await blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: 'Test Description', image: null);
await blogPostCreateCubit.createBlogPost(
title: fullCreateFixture['title'] as String?,
videoUrl: null,
description: fullCreateFixture['description_md'] as String?,
image: null,
);
await tester.pump();
// Assert
@@ -295,9 +279,7 @@ void main() {
testWidgets('should handle loading state during blog post creation', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {
await Future.delayed(const Duration(milliseconds: 50));
});
blogPostRepository.createDelay = const Duration(milliseconds: 50);
Widget loadingFlowWidget = BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
builder: (context, state) {
@@ -313,7 +295,12 @@ void main() {
await tester.pumpAndSettle();
// Trigger loading
blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: 'Test Description', image: null);
blogPostCreateCubit.createBlogPost(
title: fullCreateFixture['title'] as String?,
videoUrl: null,
description: fullCreateFixture['description_md'] as String?,
image: null,
);
await tester.pump();
await tester.pump(); // Need to pump twice to get the loading state
@@ -328,13 +315,10 @@ void main() {
testWidgets('should handle success state after blog post creation', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
when(mockFeedCubit.loadFeedBlogPostList()).thenAnswer((_) async {});
Widget successFlowWidget = BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
listener: (context, state) {
if (state is BlogPostCreateStateLoaded) {
context.read<FeedCubit>().loadFeedBlogPostList();
feedProbe.loadFeedBlogPostList();
}
},
child: BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
@@ -358,13 +342,18 @@ void main() {
await tester.pumpAndSettle();
// Trigger success
await blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: 'Test Description', image: null);
await blogPostCreateCubit.createBlogPost(
title: fullCreateFixture['title'] as String?,
videoUrl: null,
description: fullCreateFixture['description_md'] as String?,
image: null,
);
await tester.pump();
// Assert
expect(find.byIcon(Icons.check_circle), findsOneWidget);
expect(find.text('Блог пост успешно создан!'), findsOneWidget);
verify(mockFeedCubit.loadFeedBlogPostList()).called(1);
expect(feedProbe.loadRequests, 1);
});
testWidgets('should handle form validation flow', (WidgetTester tester) async {
@@ -462,13 +451,10 @@ void main() {
testWidgets('should handle complete blog post creation with all features', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
when(mockFeedCubit.loadFeedBlogPostList()).thenAnswer((_) async {});
Widget completeFlowWidget = BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
listener: (context, state) {
if (state is BlogPostCreateStateLoaded) {
context.read<FeedCubit>().loadFeedBlogPostList();
feedProbe.loadFeedBlogPostList();
}
},
child: Column(
@@ -517,9 +503,9 @@ void main() {
key: AppKeys.blogPostCreateScreenCreateButton,
onPressed: () async {
await blogPostCreateCubit.createBlogPost(
title: 'Complete Test Title',
videoUrl: 'https://youtube.com/watch?v=complete',
description: 'Complete Test Description',
title: fullCreateFixture['title'] as String?,
videoUrl: fullCreateFixture['video_url'] as String?,
description: fullCreateFixture['description_md'] as String?,
image: null,
);
},
@@ -533,9 +519,9 @@ void main() {
await tester.pumpWidget(createTestWidget(completeFlowWidget));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Complete Test Title');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Complete Test Description');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=complete');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), fullCreateFixture['title'] as String);
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), fullCreateFixture['description_md'] as String);
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), fullCreateFixture['video_url'] as String);
await tester.pump();
// Scroll to make sure the button is visible
@@ -546,8 +532,8 @@ void main() {
await tester.pump();
// Assert
verify(mockBlogPostRepository.createBlogPost('Complete Test Title', 'https://youtube.com/watch?v=complete', 'Complete Test Description', null)).called(1);
verify(mockFeedCubit.loadFeedBlogPostList()).called(1);
expect(blogPostRepository.createPayloads.single.toJson(), fullCreateFixture);
expect(feedProbe.loadRequests, 1);
});
});
}

View File

@@ -1,31 +1,26 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart';
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'basic_widget_test.mocks.dart';
import '../../fixtures/doubles/create_diet_doubles.dart';
@GenerateMocks([CreateDietRepository, ProfileRepository, GlossaryRepository])
void main() {
group('Diet Creation Basic Widget Tests', () {
late MockCreateDietRepository mockCreateDietRepository;
late MockProfileRepository mockProfileRepository;
late MockGlossaryRepository mockGlossaryRepository;
late FixtureCreateDietRepository createDietRepository;
late FixtureProfileRepository profileRepository;
late FixtureGlossaryRepository glossaryRepository;
late CreateDietCubit createDietCubit;
setUp(() {
mockCreateDietRepository = MockCreateDietRepository();
mockProfileRepository = MockProfileRepository();
mockGlossaryRepository = MockGlossaryRepository();
createDietRepository = FixtureCreateDietRepository();
profileRepository = FixtureProfileRepository();
glossaryRepository = FixtureGlossaryRepository();
createDietCubit = CreateDietCubit(
createDietRepository: mockCreateDietRepository,
profileRepository: mockProfileRepository,
glossaryRepository: mockGlossaryRepository,
createDietRepository: createDietRepository,
profileRepository: profileRepository,
glossaryRepository: glossaryRepository,
);
});
@@ -46,9 +41,9 @@ void main() {
testWidgets('should create CreateDietCubit successfully', (WidgetTester tester) async {
// Arrange & Act
final cubit = CreateDietCubit(
createDietRepository: mockCreateDietRepository,
profileRepository: mockProfileRepository,
glossaryRepository: mockGlossaryRepository,
createDietRepository: createDietRepository,
profileRepository: profileRepository,
glossaryRepository: glossaryRepository,
);
// Assert

View File

@@ -9,26 +9,20 @@ import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_c
import 'package:eatvio_app/features/create_plan/presentation/widgets/plan_duration_drop_down_widget.dart';
import 'package:eatvio_app/features/create_plan/presentation/widgets/plan_type_choice_widget.dart';
import 'package:eatvio_app/features/create_plan/presentation/widgets/option_widget.dart';
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'advanced_widget_test.mocks.dart';
import '../../fixtures/doubles/create_plan_doubles.dart';
@GenerateMocks([CreatePlanRepository, ProfileRepository, GlossaryRepository])
void main() {
group('Meal Plan Creation Advanced Widget Tests', () {
late MockCreatePlanRepository mockCreatePlanRepository;
late FixtureCreatePlanRepository createPlanRepository;
late CreatePlanCubit createPlanCubit;
setUp(() async {
await sl.reset();
mockCreatePlanRepository = MockCreatePlanRepository();
createPlanCubit = CreatePlanCubit(createPlanRepository: mockCreatePlanRepository);
createPlanRepository = FixtureCreatePlanRepository();
createPlanCubit = CreatePlanCubit(createPlanRepository: createPlanRepository);
sl.registerLazySingleton<CreatePlanCubit>(() => createPlanCubit);
AppConfigRegistration.registerAppConfig(buildType: AppBuildType.tests);
});

View File

@@ -1,24 +1,22 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'basic_widget_test.mocks.dart';
import '../../fixtures/doubles/create_plan_doubles.dart';
import '../../fixtures/fixture_reader.dart';
@GenerateMocks([CreatePlanRepository, ProfileRepository, GlossaryRepository])
void main() {
group('Meal Plan Creation Basic Widget Tests', () {
late MockCreatePlanRepository mockCreatePlanRepository;
late FixtureCreatePlanRepository createPlanRepository;
late CreatePlanCubit createPlanCubit;
late Map<String, dynamic> planFixture;
setUp(() {
mockCreatePlanRepository = MockCreatePlanRepository();
createPlanCubit = CreatePlanCubit(createPlanRepository: mockCreatePlanRepository);
planFixture = FixtureReader.readAsMap('create_plan/create_plan_request.json');
createPlanRepository = FixtureCreatePlanRepository();
createPlanCubit = CreatePlanCubit(createPlanRepository: createPlanRepository);
});
tearDown(() {
@@ -37,7 +35,7 @@ void main() {
testWidgets('should create CreatePlanCubit successfully', (WidgetTester tester) async {
// Arrange & Act
final cubit = CreatePlanCubit(createPlanRepository: mockCreatePlanRepository);
final cubit = CreatePlanCubit(createPlanRepository: createPlanRepository);
// Assert
expect(cubit, isA<CreatePlanCubit>());
@@ -98,7 +96,7 @@ void main() {
await tester.pumpWidget(createTestWidget(formWidget));
await tester.pumpAndSettle();
const testName = 'Тестовый план питания';
final testName = planFixture['name'] as String;
// Act
await tester.enterText(find.byKey(const Key('planNameField')), testName);
@@ -119,7 +117,7 @@ void main() {
await tester.pumpWidget(createTestWidget(formWidget));
await tester.pumpAndSettle();
const testDescription = 'Это тестовое описание плана питания для здорового образа жизни';
final testDescription = planFixture['description'] as String;
// Act
await tester.enterText(find.byKey(const Key('planDescriptionField')), testDescription);

View File

@@ -1,11 +1,8 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart';
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:eatvio_app/generated/l10n.dart';
@@ -13,20 +10,18 @@ import 'package:eatvio_app/core/locator_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'integration_widget_test.mocks.dart';
import '../../fixtures/doubles/create_plan_doubles.dart';
@GenerateMocks([CreatePlanRepository, ProfileRepository, GlossaryRepository])
void main() {
group('Meal Plan Creation Integration Widget Tests', () {
late MockCreatePlanRepository mockCreatePlanRepository;
late FixtureCreatePlanRepository createPlanRepository;
late CreatePlanCubit createPlanCubit;
setUp(() async {
await sl.reset();
mockCreatePlanRepository = MockCreatePlanRepository();
createPlanCubit = CreatePlanCubit(createPlanRepository: mockCreatePlanRepository);
createPlanRepository = FixtureCreatePlanRepository();
createPlanCubit = CreatePlanCubit(createPlanRepository: createPlanRepository);
sl.registerLazySingleton<CreatePlanCubit>(() => createPlanCubit);
});

View File

@@ -0,0 +1,5 @@
{
"email": "test@example.com",
"password": "password123"
}

View File

@@ -0,0 +1,4 @@
{
"token": "test_token_12345"
}

View File

@@ -0,0 +1,7 @@
{
"title": "Complete Test Title",
"description_md": "Complete Test Description",
"video_url": "https://youtube.com/watch?v=complete",
"image_name": null
}

View File

@@ -0,0 +1,4 @@
{
"id": 1
}

View File

@@ -0,0 +1,9 @@
{
"id": 1,
"title": "Updated Title",
"description_md": "Updated Description",
"video_url": "https://youtube.com/watch?v=updated",
"old_image_url": "original_image.jpg",
"image_name": null
}

View File

@@ -0,0 +1,13 @@
{
"name": "Легкий рацион",
"duration": 7,
"details": "Балансированный рацион на неделю",
"state": "draft",
"types": {
"meat": true,
"carb": false,
"veg": true,
"light": true
}
}

View File

@@ -0,0 +1,17 @@
{
"results": [
{
"id": 10,
"name": "Авокадо",
"portion": "100 г",
"nutritional_value_id": 501
},
{
"id": 11,
"name": "Лосось",
"portion": "150 г",
"nutritional_value_id": 502
}
]
}

View File

@@ -0,0 +1,14 @@
{
"name": "План для похудения",
"description": "Описание плана питания для снижения веса",
"duration": 14,
"status": "draft",
"options": [
{
"title": "Базовый план",
"price": 1500,
"description": "Меню + чат с консультантом"
}
]
}

66
test/fixtures/doubles/auth_doubles.dart vendored Normal file
View File

@@ -0,0 +1,66 @@
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
/// Fixture-based replacement for [AuthRepository].
///
/// - Records every credential payload that would be sent to the backend.
/// - Returns data loaded from fixture files so QA/backenders can see
/// exactly what is exchanged.
class FixtureAuthRepository extends AuthRepository {
FixtureAuthRepository({
required this.tokenFixture,
}) : super(authService: _NoopAuthService());
final TokenModel tokenFixture;
final List<CredentialsCall> credentialCalls = <CredentialsCall>[];
List<Map<String, String>> get credentialPayloads => credentialCalls.map((call) => call.toJson()).toList(growable: false);
Duration responseDelay = Duration.zero;
Object? error;
@override
Future<TokenModel> getTokenByCredentials(String email, String password) async {
credentialCalls.add(CredentialsCall(email: email, password: password));
if (responseDelay > Duration.zero) {
await Future.delayed(responseDelay);
}
if (error != null) {
throw error!;
}
return tokenFixture;
}
}
class CredentialsCall {
const CredentialsCall({required this.email, required this.password});
final String email;
final String password;
Map<String, String> toJson() => <String, String>{
'email': email,
'password': password,
};
}
class _NoopAuthService implements AuthService {
@override
Future<TokenModel> getTokenByCredentials({required CredentialsModel credentials}) {
throw UnimplementedError('Use FixtureAuthRepository override instead.');
}
@override
Future<TokenModel> signInViaSocialMedia({required OAuthLoginRequest body}) {
throw UnimplementedError('Not implemented in fixture.');
}
@override
Future<Map<String, Object?>> signUpViaSocialMedia({required OAuthModel body}) {
throw UnimplementedError('Not implemented in fixture.');
}
}

View File

@@ -0,0 +1,143 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
class FixtureBlogPostRepository extends BlogPostRepository {
FixtureBlogPostRepository() : super(blogPostService: _NoopBlogPostService());
final List<BlogPostCreatePayload> createPayloads = <BlogPostCreatePayload>[];
final List<BlogPostUpdatePayload> updatePayloads = <BlogPostUpdatePayload>[];
final List<int> deleteCalls = <int>[];
Duration createDelay = Duration.zero;
Duration updateDelay = Duration.zero;
Duration deleteDelay = Duration.zero;
Object? createError;
Object? updateError;
Object? deleteError;
@override
Future<void> createBlogPost(String? title, String? videoUrl, String? description, XFile? image) async {
createPayloads.add(
BlogPostCreatePayload(
title: title,
description: description,
videoUrl: videoUrl,
imageName: image?.name,
),
);
if (createDelay > Duration.zero) {
await Future.delayed(createDelay);
}
if (createError != null) {
throw createError!;
}
}
@override
Future<void> updateBlogPost(int id, String? title, String? videoUrl, String? description, XFile? image, String? oldImageUrl) async {
updatePayloads.add(
BlogPostUpdatePayload(
id: id,
title: title,
description: description,
videoUrl: videoUrl,
imageName: image?.name,
oldImageUrl: oldImageUrl,
),
);
if (updateDelay > Duration.zero) {
await Future.delayed(updateDelay);
}
if (updateError != null) {
throw updateError!;
}
}
@override
Future<void> deleteBlogPost(int id) async {
deleteCalls.add(id);
if (deleteDelay > Duration.zero) {
await Future.delayed(deleteDelay);
}
if (deleteError != null) {
throw deleteError!;
}
}
}
class BlogPostCreatePayload {
BlogPostCreatePayload({
required this.title,
required this.description,
required this.videoUrl,
required this.imageName,
});
final String? title;
final String? description;
final String? videoUrl;
final String? imageName;
Map<String, dynamic> toJson() => <String, dynamic>{
'title': title,
'description_md': description,
'video_url': videoUrl,
'image_name': imageName,
};
}
class BlogPostUpdatePayload {
BlogPostUpdatePayload({
required this.id,
required this.title,
required this.description,
required this.videoUrl,
required this.imageName,
required this.oldImageUrl,
});
final int id;
final String? title;
final String? description;
final String? videoUrl;
final String? imageName;
final String? oldImageUrl;
Map<String, dynamic> toJson() => <String, dynamic>{
'id': id,
'title': title,
'description_md': description,
'video_url': videoUrl,
'image_name': imageName,
'old_image_url': oldImageUrl,
};
}
class _NoopBlogPostService implements BlogPostService {
@override
Future<void> createBlogPost({required FormData formData}) {
throw UnimplementedError('Fixture overrides repository methods directly.');
}
@override
Future<void> deleteBlogPost({required int id}) {
throw UnimplementedError('Fixture overrides repository methods directly.');
}
@override
Future<void> updateBlogPost({required int id, required FormData formData}) {
throw UnimplementedError('Fixture overrides repository methods directly.');
}
}
class FeedReloadProbe {
int loadRequests = 0;
Future<void> loadFeedBlogPostList() async {
loadRequests += 1;
}
}

View File

@@ -0,0 +1,205 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/profile/services/profile_service.dart';
import 'package:retrofit/dio.dart' show HttpResponse;
import '../../fixtures/fixture_reader.dart';
class FixtureCreateDietRepository extends CreateDietRepository {
FixtureCreateDietRepository()
: super(
createDietService: _NoopCreateDietService(),
);
final List<Map<String, dynamic>> createRequests = <Map<String, dynamic>>[];
final List<Map<String, dynamic>> updateRequests = <Map<String, dynamic>>[];
Map<int, Map<int, Map<int, DailySchedule>>> nutritionFixture = <int, Map<int, Map<int, DailySchedule>>>{
1: {
1: {
0: const DailySchedule(
name: 'Авокадо',
portion: Portion.grams,
quantity: 100,
productNutritionalValueId: '501',
),
},
},
};
@override
Future<CreatedDiet> createDiet({
required String name,
required int duration,
required DietState state,
required String meatType,
required String carbType,
required String vegType,
required String lightType,
required String details,
required File image,
required List<List<List<DailySchedule>>> dailySchedule,
}) async {
createRequests.add(<String, dynamic>{
'name': name,
'duration': duration,
'state': state.toString(),
'meatType': meatType,
'carbType': carbType,
'vegType': vegType,
'lightType': lightType,
'details': details,
'image': image.path,
'days': dailySchedule.length,
});
return const CreatedDiet(id: 1);
}
@override
Future<Map<int, Map<int, Map<int, DailySchedule>>>> getDietPortionsNutrition(int dietId) async {
return nutritionFixture;
}
@override
Future<void> updateDiet({
required int dietId,
required DietState state,
String? name,
int? duration,
String? meatType,
String? carbType,
String? vegType,
String? lightType,
String? details,
File? image,
List<List<List<DailySchedule>>>? dailySchedule,
}) async {
updateRequests.add(<String, dynamic>{
'dietId': dietId,
'state': state.toString(),
'name': name,
'duration': duration,
'meatType': meatType,
'carbType': carbType,
'vegType': vegType,
'lightType': lightType,
'details': details,
'image': image?.path,
'days': dailySchedule?.length,
});
}
}
class FixtureProfileRepository extends ProfileRepository {
FixtureProfileRepository()
: super(
profileService: _NoopProfileService(),
);
@override
Future<ProfileModel> getMyProfile() async {
return const ProfileModel(
id: 99,
name: 'Анна Нутрициолог',
email: 'anna@example.com',
phoneConfirmationState: 'verified',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
slug: 'diet-author',
profilePhotoUrl: 'avatars/anna.png',
roles: ['author'],
badges: [],
subscriptions: [],
subscribers: [],
);
}
}
class FixtureGlossaryRepository extends GlossaryRepository {
FixtureGlossaryRepository()
: super(
glossaryService: _NoopGlossaryService(),
);
@override
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
final results = FixtureReader.readAsMap('create_diet/glossary_search_response.json')['results'] as List<dynamic>;
return results
.map(
(item) => GlossaryModel(
id: item['id'] as int,
name: item['name'] as String,
grInPortion: 100,
energyOn100Gr: 160.0,
proteinsOn100Gr: 2.0,
fatsOn100Gr: 14.0,
carbohydratesOn100Gr: 9.0,
energyOnPortion: 160.0,
proteinsOnPortion: 2.0,
fatsOnPortion: 14.0,
carbohydratesOnPortion: 9.0,
),
)
.toList();
}
}
class _NoopCreateDietService implements CreateDietService {
@override
Future<CreatedDiet> createDraftedDiet({required FormData formData}) {
throw UnimplementedError();
}
@override
Future<CreatedDiet> createPersonalDiet({required FormData formData}) {
throw UnimplementedError();
}
@override
Future<CreatedDiet> createPublishedDiet({required FormData formData}) {
throw UnimplementedError();
}
@override
Future<HttpResponse> getDietPortionsNutrition({required int diet}) {
throw UnimplementedError();
}
@override
Future<void> updateDrafted({required int diet, required FormData formData}) {
throw UnimplementedError();
}
@override
Future<void> updatePersonal({required int diet, required FormData formData}) {
throw UnimplementedError();
}
@override
Future<void> updatePublished({required int diet, required FormData formData}) {
throw UnimplementedError();
}
}
class _NoopProfileService implements ProfileService {
@override
noSuchMethod(Invocation invocation) => throw UnimplementedError('ProfileService call ${invocation.memberName}');
}
class _NoopGlossaryService implements GlossaryService {
@override
noSuchMethod(Invocation invocation) => throw UnimplementedError('GlossaryService call ${invocation.memberName}');
}

View File

@@ -0,0 +1,22 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
class FixtureCreatePlanRepository extends CreatePlanRepository {
FixtureCreatePlanRepository() : super(createPlanService: _NoopCreatePlanService());
final List<Map<String, dynamic>> createPlanRequests = <Map<String, dynamic>>[];
@override
Future<void> createPlan({required Map<String, dynamic> model}) async {
createPlanRequests.add(Map<String, dynamic>.from(model));
}
}
class _NoopCreatePlanService implements CreatePlanService {
@override
Future<void> createPlan({required FormData formData}) {
throw UnimplementedError('Not used in fixture tests');
}
}

30
test/fixtures/fixture_reader.dart vendored Normal file
View File

@@ -0,0 +1,30 @@
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as p;
/// Utility that reads JSON fixtures from `test/fixtures`.
///
/// Using files instead of inline maps makes it explicit which payloads
/// are exchanged with the backend.
class FixtureReader {
const FixtureReader._();
static String readAsString(String relativePath) {
final file = File(_resolve(relativePath));
return file.readAsStringSync();
}
static Map<String, dynamic> readAsMap(String relativePath) {
return jsonDecode(readAsString(relativePath)) as Map<String, dynamic>;
}
static List<dynamic> readAsList(String relativePath) {
return jsonDecode(readAsString(relativePath)) as List<dynamic>;
}
static String _resolve(String relativePath) {
return p.normalize(p.join('test', 'fixtures', relativePath));
}
}

View File

@@ -0,0 +1,7 @@
{
"slug": "diet-author",
"first_name": "Анна",
"last_name": "Нутрициолог",
"avatar": "avatars/anna.png"
}