chore(version): update app version to 1.3.5+85 and improve test coverage reporting in CI

This commit is contained in:
mso7nueh
2025-12-08 11:19:08 +03:00
parent abe4985f0e
commit c2a506cdbf
148 changed files with 12395 additions and 1889 deletions

View File

@@ -0,0 +1,376 @@
import 'package:eatvio_app/app_keys.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/di/app_config_registration.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/loader.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/presentation/pages/blog_post_create_screen.dart';
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_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:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:network_image_mock/network_image_mock.dart';
import 'blog_post_create_screen_test.mocks.dart';
@GenerateMocks([BlogPostRepository, BlogPostCreateCubit, FeedCubit])
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('BlogPostCreateScreen', () {
late MockBlogPostRepository mockBlogPostRepository;
late BlogPostCreateCubit blogPostCreateCubit;
late MockFeedCubit mockFeedCubit;
setUp(() async {
await sl.reset();
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
AppConfigRegistration.registerAppConfig(buildType: AppBuildType.tests);
mockBlogPostRepository = MockBlogPostRepository();
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: mockBlogPostRepository);
mockFeedCubit = MockFeedCubit();
when(mockFeedCubit.state).thenReturn(const FeedState.loading());
when(mockFeedCubit.stream).thenAnswer((_) => Stream.value(const FeedState.loading()));
sl.registerLazySingleton<FeedCubit>(() => mockFeedCubit);
sl.registerLazySingleton<BlogPostCreateCubit>(() => blogPostCreateCubit);
});
tearDown(() {
blogPostCreateCubit.close();
});
Widget createTestWidget({FeedBlogPostModel? feedBlogPostModel, bool canPop = false}) {
final router = GoRouter(
initialLocation: '/blog-post-create',
routes: [
GoRoute(
path: '/home',
builder: (context, state) => Scaffold(
body: Center(child: Text('Home')),
),
),
GoRoute(
path: '/blog-post-create',
builder: (context, state) => MultiBlocProvider(
providers: [
BlocProvider<BlogPostCreateCubit>.value(
value: blogPostCreateCubit,
),
BlocProvider<FeedCubit>.value(
value: mockFeedCubit,
),
],
child: BlogPostCreateScreen(feedBlogPostModel: feedBlogPostModel),
),
),
],
);
return MaterialApp.router(
routerConfig: router,
theme: ThemeHelper().themeData(),
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru'), Locale('en')],
locale: const Locale('ru'),
);
}
testWidgets('displays all form fields when creating new blog post', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
expect(find.text('Заголовок'), findsOneWidget);
expect(find.text('Описание'), findsOneWidget);
expect(find.text('Изображение поста'), findsOneWidget);
expect(find.text('Ссылка на видео'), findsOneWidget);
expect(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), findsOneWidget);
expect(find.byKey(AppKeys.blogPostCreateScreenContentTextField), findsOneWidget);
expect(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), findsOneWidget);
});
testWidgets('pre-fills form fields when editing existing blog post', (tester) async {
final existingPost = FeedBlogPostModel(
id: 1,
title: 'Test Title',
description: 'Test Description',
videoUrl: 'https://youtube.com/watch?v=test',
image: '', // Changed: removed image so video field is visible
state: 'published',
author: const Author(
authorSlug: 'test-author',
authorName: 'Test Author',
authorProfileIcon: 'avatar.jpg',
authorIsMe: true,
),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
totalLikes: 0,
totalViews: 0,
totalComments: 0,
wasLikedByMe: false,
);
await tester.pumpWidget(createTestWidget(feedBlogPostModel: existingPost));
await tester.pumpAndSettle();
final titleField = tester.widget<CustomTextFormField>(find.byKey(AppKeys.blogPostCreateScreenTitleTextField));
expect(titleField.controller?.text, 'Test Title');
final descriptionField = tester.widget<CustomTextFormField>(find.byKey(AppKeys.blogPostCreateScreenContentTextField));
expect(descriptionField.controller?.text, 'Test Description');
// Only check video field if it's visible (when image is null)
final videoFieldFinder = find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField);
if (videoFieldFinder.evaluate().isNotEmpty) {
final videoField = tester.widget<CustomTextFormField>(videoFieldFinder);
expect(videoField.controller?.text, 'https://youtube.com/watch?v=test');
}
});
testWidgets('hides video field when image is present', (tester) async {
await mockNetworkImagesFor(() async {
final existingPost = FeedBlogPostModel(
id: 1,
title: 'Test Title',
description: 'Test Description',
videoUrl: '',
image: 'http://localhost/storage/test_image.jpg',
state: 'published',
author: const Author(
authorSlug: 'test-author',
authorName: 'Test Author',
authorProfileIcon: 'http://localhost/storage/avatar.jpg',
authorIsMe: true,
),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
totalLikes: 0,
totalViews: 0,
totalComments: 0,
wasLikedByMe: false,
);
await tester.pumpWidget(createTestWidget(feedBlogPostModel: existingPost));
await tester.pumpAndSettle();
expect(find.text('Ссылка на видео'), findsNothing);
expect(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), findsNothing);
});
});
testWidgets('hides image field when video URL is present', (tester) async {
final existingPost = FeedBlogPostModel(
id: 1,
title: 'Test Title',
description: 'Test Description',
videoUrl: 'https://youtube.com/watch?v=test',
image: '',
state: 'published',
author: const Author(
authorSlug: 'test-author',
authorName: 'Test Author',
authorProfileIcon: 'avatar.jpg',
authorIsMe: true,
),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
totalLikes: 0,
totalViews: 0,
totalComments: 0,
wasLikedByMe: false,
);
await tester.pumpWidget(createTestWidget(feedBlogPostModel: existingPost));
await tester.pumpAndSettle();
expect(find.text('Изображение поста'), findsNothing);
});
testWidgets('allows entering text in title field', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'New Title');
await tester.pump();
final titleField = tester.widget<CustomTextFormField>(find.byKey(AppKeys.blogPostCreateScreenTitleTextField));
expect(titleField.controller?.text, 'New Title');
});
testWidgets('allows entering text in description field', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'New Description');
await tester.pump();
final descriptionField = tester.widget<CustomTextFormField>(find.byKey(AppKeys.blogPostCreateScreenContentTextField));
expect(descriptionField.controller?.text, 'New Description');
});
testWidgets('allows entering text in video URL field', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=new');
await tester.pump();
final videoField = tester.widget<CustomTextFormField>(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField));
expect(videoField.controller?.text, 'https://youtube.com/watch?v=new');
});
testWidgets('displays create button', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
expect(find.text('Создать'), findsOneWidget);
});
testWidgets('shows loading state when creating blog post', (tester) async {
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {
await Future.delayed(const Duration(milliseconds: 100));
return;
});
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Test Title');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Test Description');
await tester.pump();
// Tap create button - find by text and scroll if needed
final createButton = find.text('Создать');
await tester.ensureVisible(createButton);
await tester.tap(createButton);
await tester.pump();
// Check for Loader widget (not CircularProgressIndicator)
expect(find.byType(Loader), findsWidgets);
// Wait for timer to complete
await tester.pump(const Duration(milliseconds: 150));
await tester.pumpAndSettle();
});
testWidgets('calls FeedCubit.loadFeedBlogPostList when blog post is created successfully', (tester) async {
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {
return;
});
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
final titleField = find.byKey(AppKeys.blogPostCreateScreenTitleTextField);
final contentField = find.byKey(AppKeys.blogPostCreateScreenContentTextField);
await tester.ensureVisible(titleField);
await tester.ensureVisible(contentField);
await tester.pumpAndSettle();
await tester.enterText(titleField, 'Test Title');
await tester.enterText(contentField, 'Test Description');
await tester.pump();
final createButton = find.text('Создать');
await tester.ensureVisible(createButton);
await tester.tap(createButton, warnIfMissed: false);
await tester.pump();
// Wait for async operation
await tester.pump(const Duration(milliseconds: 200));
await tester.pumpAndSettle();
verify(mockFeedCubit.loadFeedBlogPostList()).called(1);
});
testWidgets('calls updateBlogPost when editing existing post', (tester) async {
await mockNetworkImagesFor(() async {
when(mockBlogPostRepository.updateBlogPost(any, any, any, any, any, any)).thenAnswer((_) async {
return;
});
final existingPost = FeedBlogPostModel(
id: 1,
title: 'Test Title',
description: 'Test Description',
videoUrl: '',
image: 'http://localhost/storage/test_image.jpg',
state: 'published',
author: const Author(
authorSlug: 'test-author',
authorName: 'Test Author',
authorProfileIcon: 'http://localhost/storage/avatar.jpg',
authorIsMe: true,
),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
totalLikes: 0,
totalViews: 0,
totalComments: 0,
wasLikedByMe: false,
);
await tester.pumpWidget(createTestWidget(feedBlogPostModel: existingPost, canPop: true));
await tester.pumpAndSettle();
final titleField = find.byKey(AppKeys.blogPostCreateScreenTitleTextField);
expect(titleField, findsOneWidget);
await tester.enterText(titleField, 'Updated Title');
await tester.pump();
// Find and tap create button - ensure it's visible
final createButton = find.text('Создать');
await tester.ensureVisible(createButton);
await tester.tap(createButton, warnIfMissed: false);
await tester.pump();
// Wait for async operation
await tester.pump(const Duration(milliseconds: 200));
await tester.pumpAndSettle();
verify(mockBlogPostRepository.updateBlogPost(
any,
any,
any,
any,
any,
any,
)).called(1);
});
});
testWidgets('displays AppBar', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
expect(find.byType(AppBar), findsOneWidget);
});
testWidgets('has scrollable body', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
expect(find.byType(SingleChildScrollView), findsOneWidget);
});
});
}