diff --git a/test/features/auth/presentation/pages/auth_screen_smoke_test.dart b/test/features/auth/presentation/pages/auth_screen_smoke_test.dart new file mode 100644 index 00000000..030613cd --- /dev/null +++ b/test/features/auth/presentation/pages/auth_screen_smoke_test.dart @@ -0,0 +1,59 @@ +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/theme/theme_helper.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/presentation/pages/auth_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'auth_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('AuthScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ); + } + + testWidgets('renders base auth UI', (tester) async { + final cubit = MockAuthCubit(); + when(cubit.state).thenReturn(const AuthState()); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async {}); + sl.registerFactory(() => cubit); + + await tester.pumpWidget(wrap(const AuthScreen())); + await tester.pump(); + + expect(find.byType(AuthScreen), findsOneWidget); + // Minimal/stable assertions. + expect(find.text(S.current.authentificate), findsOneWidget); + }); + }); +} diff --git a/test/features/auth/presentation/pages/forget_password_screen_smoke_test.dart b/test/features/auth/presentation/pages/forget_password_screen_smoke_test.dart new file mode 100644 index 00000000..714f75c7 --- /dev/null +++ b/test/features/auth/presentation/pages/forget_password_screen_smoke_test.dart @@ -0,0 +1,65 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'forget_password_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('ForgetPasswordScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) { + return MaterialApp(theme: ThemeHelper().themeData(), home: child); + } + + testWidgets('renders initial email step (isAuth=false)', (tester) async { + final cubit = MockResetPasswordCubit(); + when(cubit.state).thenReturn(const ResetPasswordState.empty()); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async {}); + sl.registerFactory(() => cubit); + + await tester.pumpWidget(wrap(const ForgetPasswordScreen(isAuth: false))); + await tester.pump(); + + expect(find.byType(ForgetPasswordScreen), findsOneWidget); + expect(find.text('Восстановление пароля'), findsOneWidget); + expect(find.text('E-mail'), findsOneWidget); + expect(find.text('Сбросить пароль'), findsOneWidget); + }); + + testWidgets('renders code + new password step', (tester) async { + final cubit = MockResetPasswordCubit(); + when(cubit.state).thenReturn(const ResetPasswordState.codeSendSuccess()); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async {}); + sl.registerFactory(() => cubit); + + await tester.pumpWidget(wrap(const ForgetPasswordScreen(isAuth: true))); + await tester.pump(); + + expect(find.text('Смена пароля'), findsOneWidget); + expect(find.text('Код'), findsOneWidget); + expect(find.text('Пароль'), findsOneWidget); + expect(find.text('Подтвердите пароль'), findsOneWidget); + expect(find.text('Сменить пароль'), findsOneWidget); + }); + }); +} diff --git a/test/features/auth/presentation/pages/register_via_social_screen_smoke_test.dart b/test/features/auth/presentation/pages/register_via_social_screen_smoke_test.dart new file mode 100644 index 00000000..4696da59 --- /dev/null +++ b/test/features/auth/presentation/pages/register_via_social_screen_smoke_test.dart @@ -0,0 +1,66 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/core/di/cubit_registrations.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/register_via_social_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'register_via_social_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('RegisterViaSocialScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + + await CubitRegistrations.sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) { + return MaterialApp(theme: ThemeHelper().themeData(), home: child); + } + + testWidgets('renders and pre-fills fields from AuthResultEntity', (tester) async { + final cubit = MockOAuthCubit(); + when(cubit.state).thenReturn(const OAuthState()); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async {}); + CubitRegistrations.sl.registerFactory(() => cubit); + + const authResult = AuthResultEntity( + accessToken: 'token', + userId: '1', + clientId: 'client', + refreshToken: 'refresh', + firstName: 'Ivan', + lastName: 'Petrov', + email: 'ivan@example.com', + phone: '+7 (999) 111 22 33', + birthDate: '1990-01-01', + gender: GenderEnum.male, + ); + + await tester.pumpWidget(wrap(const RegisterViaSocialScreen(authResult: authResult, provider: 'vk'))); + await tester.pump(); + + expect(find.byType(RegisterViaSocialScreen), findsOneWidget); + expect(find.text('Регистрация'), findsOneWidget); + expect(find.text('Ivan'), findsOneWidget); + expect(find.text('Petrov'), findsOneWidget); + expect(find.text('ivan@example.com'), findsOneWidget); + }); + }); +} diff --git a/test/features/blog_post/presentation/pages/blog_post_screen_smoke_test.dart b/test/features/blog_post/presentation/pages/blog_post_screen_smoke_test.dart new file mode 100644 index 00000000..fba2161e --- /dev/null +++ b/test/features/blog_post/presentation/pages/blog_post_screen_smoke_test.dart @@ -0,0 +1,109 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +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/features/blog_post/presentation/cubit/blog_post_cubit.dart'; +import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart'; +import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.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 'blog_post_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec(), MockSpec()]) +void main() { + group('BlogPostScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() { + FlutterError.onError = (FlutterErrorDetails details) { + if (details.exception is FlutterError && details.exception.toString().contains('overflowed')) { + return; + } + FlutterError.presentError(details); + }; + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + void registerMocks(BlogPostCubit blogPostCubit, CommentsCubit commentsCubit) { + sl.registerLazySingleton(() => blogPostCubit); + sl.registerLazySingleton(() => commentsCubit); + } + + Widget wrap(BlogPostCubit blogPostCubit, FeedCubit feedCubit, CommentsCubit commentsCubit, Widget child) { + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => MediaQuery( + data: const MediaQueryData(size: Size(2000, 1200)), + child: MultiBlocProvider( + providers: [ + BlocProvider.value(value: blogPostCubit), + BlocProvider.value(value: feedCubit), + BlocProvider.value(value: commentsCubit), + ], + child: child, + ), + ), + ), + GoRoute(path: '/feed-screen', builder: (_, _) => const SizedBox.shrink()), + ], + ); + + return MaterialApp.router( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + routerConfig: router, + ); + } + + testWidgets('renders loading state', (tester) async { + final blogPostCubit = MockBlogPostCubit(); + final feedCubit = MockFeedCubit(); + final commentsCubit = MockCommentsCubit(); + final state = BlogPostState(isLoading: true, blogPostModel: null, error: null); + + when(blogPostCubit.state).thenReturn(state); + when(blogPostCubit.stream).thenAnswer((_) => const Stream.empty()); + when(blogPostCubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.viewPost(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(blogPostCubit, commentsCubit); + + await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, const BlogPostScreen(id: 1))); + await tester.pump(); + + expect(find.byType(BlogPostScreen), findsOneWidget); + }); + + }); +} diff --git a/test/features/blog_post_create/presentation/pages/blog_post_create_screen_test.dart b/test/features/blog_post_create/presentation/pages/blog_post_create_screen_test.dart index a8416d3d..13b86880 100644 --- a/test/features/blog_post_create/presentation/pages/blog_post_create_screen_test.dart +++ b/test/features/blog_post_create/presentation/pages/blog_post_create_screen_test.dart @@ -6,6 +6,7 @@ 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/bloc/blog_post_create_cubit/blog_post_create_state.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'; @@ -57,27 +58,21 @@ void main() { routes: [ GoRoute( path: '/home', - builder: (context, state) => const Scaffold( - body: Center(child: Text('Home')), - ), + builder: (context, state) => const Scaffold(body: Center(child: Text('Home'))), ), GoRoute( path: '/blog-post-create', builder: (context, state) => MultiBlocProvider( providers: [ - BlocProvider.value( - value: blogPostCreateCubit, - ), - BlocProvider.value( - value: mockFeedCubit, - ), + BlocProvider.value(value: blogPostCreateCubit), + BlocProvider.value(value: mockFeedCubit), ], child: BlogPostCreateScreen(feedBlogPostModel: feedBlogPostModel), ), ), ], ); - + return MaterialApp.router( routerConfig: router, theme: ThemeHelper().themeData(), @@ -113,12 +108,7 @@ void main() { 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, - ), + author: const Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: 'avatar.jpg', authorIsMe: true), createdAt: DateTime.now(), updatedAt: DateTime.now(), totalLikes: 0, @@ -136,7 +126,6 @@ void main() { final descriptionField = tester.widget(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(videoFieldFinder); @@ -183,12 +172,7 @@ void main() { videoUrl: 'https://youtube.com/watch?v=test', image: '', state: 'published', - author: const Author( - authorSlug: 'test-author', - authorName: 'Test Author', - authorProfileIcon: 'avatar.jpg', - authorIsMe: true, - ), + author: const Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: 'avatar.jpg', authorIsMe: true), createdAt: DateTime.now(), updatedAt: DateTime.now(), totalLikes: 0, @@ -256,16 +240,13 @@ void main() { 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(); }); @@ -280,11 +261,11 @@ void main() { 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(); @@ -294,7 +275,6 @@ void main() { await tester.tap(createButton, warnIfMissed: false); await tester.pump(); - // Wait for async operation await tester.pump(const Duration(milliseconds: 200)); await tester.pumpAndSettle(); @@ -333,28 +313,19 @@ void main() { 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); + verify(mockBlogPostRepository.updateBlogPost(any, any, any, any, any, any)).called(1); }); }); @@ -371,6 +342,24 @@ void main() { expect(find.byType(SingleChildScrollView), findsOneWidget); }); + + testWidgets('renders error state', (tester) async { + + blogPostCreateCubit.emit(const BlogPostCreateState.error(errorMessage: 'Error creating post')); + + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + expect(find.byType(BlogPostCreateScreen), findsOneWidget); + }); + + testWidgets('renders loading state', (tester) async { + blogPostCreateCubit.emit(const BlogPostCreateState.loading()); + + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + expect(find.byType(BlogPostCreateScreen), findsOneWidget); + }); }); } - diff --git a/test/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget_test.dart b/test/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget_test.dart new file mode 100644 index 00000000..b88cc940 --- /dev/null +++ b/test/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget_test.dart @@ -0,0 +1,147 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart'; +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +import 'bottom_navigation_bar_widget_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('BottomNavigationBarWidget smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + const mockUserProfile = ProfileModel( + id: 1, + name: 'Test User', + email: 'test@example.com', + phoneConfirmationState: 'confirmed', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + slug: 'test-user', + profilePhotoUrl: 'https://example.com/avatar.jpg', + roles: ['user'], + badges: [], + subscriptions: [], + subscribers: [], + ); + + const mockConsultantProfile = ProfileModel( + id: 1, + name: 'Test Consultant', + email: 'consultant@example.com', + phoneConfirmationState: 'confirmed', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + slug: 'test-consultant', + profilePhotoUrl: 'https://example.com/avatar.jpg', + roles: ['consultant'], + badges: [], + subscriptions: [], + subscribers: [], + ); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ); + } + + testWidgets('renders bottom navigation bar for user', (tester) async { + mockNetworkImagesFor(() async { + final notificationsCubit = MockNotificationsCubit(); + final chatsCubit = MockChatsCubit(); + + when(notificationsCubit.state).thenReturn(const NotificationsState()); + when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(notificationsCubit.close()).thenAnswer((_) async => Future.value()); + when(chatsCubit.state).thenReturn(ChatsState()); + when(chatsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(chatsCubit.close()).thenAnswer((_) async => Future.value()); + + await tester.pumpWidget( + MultiBlocProvider( + providers: [ + BlocProvider.value(value: notificationsCubit), + BlocProvider.value(value: chatsCubit), + ], + child: wrap( + const Scaffold( + body: BottomNavigationBarWidget(profileModel: mockUserProfile, child: Text('Test Content')), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(BottomNavigationBarWidget), findsOneWidget); + }); + }); + + testWidgets('renders bottom navigation bar for consultant', (tester) async { + mockNetworkImagesFor(() async { + final notificationsCubit = MockNotificationsCubit(); + final chatsCubit = MockChatsCubit(); + + when(notificationsCubit.state).thenReturn(const NotificationsState()); + when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(notificationsCubit.close()).thenAnswer((_) async => Future.value()); + when(chatsCubit.state).thenReturn(ChatsState()); + when(chatsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(chatsCubit.close()).thenAnswer((_) async => Future.value()); + + await tester.pumpWidget( + MultiBlocProvider( + providers: [ + BlocProvider.value(value: notificationsCubit), + BlocProvider.value(value: chatsCubit), + ], + child: wrap( + const Scaffold( + body: BottomNavigationBarWidget(profileModel: mockConsultantProfile, child: Text('Test Content')), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(BottomNavigationBarWidget), findsOneWidget); + }); + }); + }); +} diff --git a/test/features/calendar/presentation/pages/calendar_screen_smoke_test.dart b/test/features/calendar/presentation/pages/calendar_screen_smoke_test.dart new file mode 100644 index 00000000..33719fee --- /dev/null +++ b/test/features/calendar/presentation/pages/calendar_screen_smoke_test.dart @@ -0,0 +1,85 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/calendar/models/calendar_model.dart'; +import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; +import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; +import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'calendar_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('CalendarScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + await initializeDateFormatting('ru', null); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + final mockCalendarList = [ + const CalendarModel(date: '2025-01-01', isCurrentDayIncludedInTheChosenDietProgression: true, diary: {'id': 1, 'state': 'finished'}), + ]; + + Widget wrap(Widget child) { + return MaterialApp(theme: ThemeHelper().themeData(), home: child); + } + + testWidgets('renders loading state', (tester) async { + final calendarRepository = MockCalendarRepository(); + final cubit = CalendarCubit(calendarRepository: calendarRepository); + + when(calendarRepository.getCalendar()).thenAnswer((_) async => mockCalendarList); + + sl.registerLazySingleton(() => calendarRepository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const CalendarScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(CalendarScreen), findsOneWidget); + }); + + testWidgets('renders loaded state with calendar list', (tester) async { + final calendarRepository = MockCalendarRepository(); + final cubit = CalendarCubit(calendarRepository: calendarRepository); + + when(calendarRepository.getCalendar()).thenAnswer((_) async => mockCalendarList); + + sl.registerLazySingleton(() => calendarRepository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const CalendarScreen())); + await tester.pumpAndSettle(); + + expect(find.text('Календарь'), findsOneWidget); + }); + + testWidgets('renders error state', (tester) async { + final calendarRepository = MockCalendarRepository(); + final cubit = CalendarCubit(calendarRepository: calendarRepository); + + when(calendarRepository.getCalendar()).thenThrow(Exception('Error loading calendar')); + + sl.registerLazySingleton(() => calendarRepository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const CalendarScreen())); + await tester.pumpAndSettle(); + + expect(find.textContaining('Error loading calendar'), findsOneWidget); + }); + }); +} diff --git a/test/features/chat/presentation/pages/chat_screen_loaded_smoke_test.dart b/test/features/chat/presentation/pages/chat_screen_loaded_smoke_test.dart new file mode 100644 index 00000000..c36e6893 --- /dev/null +++ b/test/features/chat/presentation/pages/chat_screen_loaded_smoke_test.dart @@ -0,0 +1,199 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; +import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; +import 'package:eatvio_app/features/chat/models/chat_room_settings_model.dart'; +import 'package:eatvio_app/features/chat/models/chat_user_model.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +import 'chat_screen_loaded_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('ChatScreen loaded smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + await initializeDateFormatting('ru', null); + await initializeDateFormatting('ru_RU', null); + await initializeDateFormatting('RU', null); + VisibilityDetectorController.instance.updateInterval = Duration.zero; + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + const fakeSvg = ''' + + + +'''; + + final fakeBundle = _FakeAssetBundle({'assets/icons/error_icon.svg': fakeSvg}); + + Widget wrap(Widget child, ChatsCubit chatsCubit) { + return DefaultAssetBundle( + bundle: fakeBundle, + child: MaterialApp( + theme: ThemeHelper().themeData(), + home: BlocProvider.value(value: chatsCubit, child: child), + ), + ); + } + + testWidgets('renders loaded header (chosenPlan null)', (tester) async { + final chatRoomCubit = MockChatRoomCubit(); + final chatsCubit = MockChatsCubit(); + + final now = DateTime(2025, 1, 2, 10, 30); + final yesterday = DateTime(2025, 1, 1, 9, 15); + + const chatRoom = ChatRoomModel( + id: '1', + chatRoomSettings: ChatRoomSettingsModel(name: 'room', state: 'active'), + users: Users( + sender: ChatUserModel(id: 1, name: 'Me', slug: 'me', profilePhotoUrl: null), + receiver: ChatUserModel(id: 2, name: 'User Two', slug: 'user-two', profilePhotoUrl: null), + ), + chosenPlanId: null, + chosenPlan: null, + ); + + final state = ChatRoomState.loaded( + chatRoom: chatRoom, + chatMessages: [ + ChatMessageModel( + id: 2, + data: 'hello', + mediaData: const [], + authorId: 1, // current user (sender) + updatedAt: now, + isRead: true, + isCurrentUserAuthor: true, + roomId: '1', + messageState: MessageState.sent, + uuid: 'u2', + ), + ChatMessageModel( + id: 4, + data: '', + mediaData: const ['chat_files/file.pdf'], + authorId: 1, // current user + updatedAt: now.subtract(const Duration(minutes: 1)), + isRead: true, + isCurrentUserAuthor: true, + roomId: '1', + messageState: MessageState.sent, + uuid: 'u4', + ), + ChatMessageModel( + id: 3, + data: 'sending...', + mediaData: const [], + authorId: 1, // current user + updatedAt: now.subtract(const Duration(minutes: 2)), + isRead: true, + isCurrentUserAuthor: true, + roomId: '1', + messageState: MessageState.loading, + uuid: 'u3', + ), + ChatMessageModel( + id: 5, + data: 'tap to retry', + mediaData: const [], + authorId: 1, // current user + updatedAt: now.subtract(const Duration(minutes: 3)), + isRead: true, + isCurrentUserAuthor: true, + roomId: '1', + messageState: MessageState.error, + uuid: 'u5', + ), + ChatMessageModel( + id: 1, + data: 'hi', + mediaData: const [], + authorId: 2, // other user (receiver) + updatedAt: yesterday, + isRead: true, + isCurrentUserAuthor: false, + roomId: '1', + messageState: MessageState.sent, + uuid: 'u1', + ), + ], + hasCard: false, + isTyping: false, + isLoading: false, + ); + + when(chatRoomCubit.state).thenReturn(state); + when(chatRoomCubit.stream).thenAnswer((_) => const Stream.empty()); + when(chatRoomCubit.close()).thenAnswer((_) async => Future.value()); + when(chatRoomCubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'))).thenAnswer((_) async => Future.value()); + when(chatRoomCubit.setTimer()).thenReturn(null); + + + sl.registerFactory(() => chatRoomCubit); + + when(chatsCubit.state).thenReturn(ChatsState()); + when(chatsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(chatsCubit.close()).thenAnswer((_) async => Future.value()); + + await tester.pumpWidget(wrap(const ChatScreen(uuid: 'uuid', sendInitialMessage: false), chatsCubit)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.byType(ChatScreen), findsOneWidget); + expect(find.text('User Two'), findsOneWidget); + expect(find.text('hello'), findsOneWidget); + expect(find.text('hi'), findsOneWidget); + expect(find.text('sending...'), findsOneWidget); + expect(find.text('tap to retry'), findsOneWidget); + expect(find.text('file.pdf'), findsOneWidget); + VisibilityDetectorController.instance.notifyNow(); + await tester.pump(); + }); + }); +} + +class _FakeAssetBundle extends CachingAssetBundle { + _FakeAssetBundle(this.assets); + final Map assets; + + @override + Future loadString(String key, {bool cache = true}) async { + final value = assets[key]; + if (value == null) { + throw FlutterError('Asset not found: $key'); + } + return value; + } + + @override + Future load(String key) async { + final value = assets[key]; + if (value == null) { + throw FlutterError('Asset not found: $key'); + } + final bytes = Uint8List.fromList(value.codeUnits); + return ByteData.sublistView(bytes); + } +} diff --git a/test/features/chat/presentation/pages/chat_screen_loading_smoke_test.dart b/test/features/chat/presentation/pages/chat_screen_loading_smoke_test.dart new file mode 100644 index 00000000..3a59cd8e --- /dev/null +++ b/test/features/chat/presentation/pages/chat_screen_loading_smoke_test.dart @@ -0,0 +1,47 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'chat_screen_loading_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('ChatScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) => MaterialApp(theme: ThemeHelper().themeData(), home: child); + + testWidgets('renders loading scaffold', (tester) async { + final cubit = MockChatRoomCubit(); + when(cubit.state).thenReturn(const ChatRoomState.loading()); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(cubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'))).thenAnswer((_) async => Future.value()); + when(cubit.setTimer()).thenReturn(null); + sl.registerFactory(() => cubit); + + await tester.pumpWidget(wrap(const ChatScreen(uuid: 'uuid', sendInitialMessage: false))); + await tester.pump(); + + expect(find.byType(ChatScreen), findsOneWidget); + expect(find.byType(Loader), findsOneWidget); + }); + }); +} diff --git a/test/features/chat/presentation/pages/chats_screen_smoke_test.dart b/test/features/chat/presentation/pages/chats_screen_smoke_test.dart new file mode 100644 index 00000000..ac3953b7 --- /dev/null +++ b/test/features/chat/presentation/pages/chats_screen_smoke_test.dart @@ -0,0 +1,53 @@ +import 'package:eatvio_app/core/enums/base_state.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.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:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'chats_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('ChatsScreen smoke', () { + Widget wrap(Widget child, ChatsCubit cubit) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: BlocProvider.value(value: cubit, child: child), + ); + } + + testWidgets('renders success state (empty list)', (tester) async { + final cubit = MockChatsCubit(); + when(cubit.state).thenReturn(ChatsState(state: BaseState.success, chatRooms: const [])); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + await tester.pumpWidget(wrap(const ChatsScreen(), cubit)); + await tester.pump(); + expect(find.byType(ChatsScreen), findsOneWidget); + }); + + testWidgets('renders error state', (tester) async { + final cubit = MockChatsCubit(); + when(cubit.state).thenReturn(ChatsState(state: BaseState.error, errorMessage: 'err')); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + await tester.pumpWidget(wrap(const ChatsScreen(), cubit)); + await tester.pump(); + expect(find.byType(ChatsScreen), findsOneWidget); + }); + }); +} diff --git a/test/features/chat/presentation/pages/empty_chat_screen_smoke_test.dart b/test/features/chat/presentation/pages/empty_chat_screen_smoke_test.dart new file mode 100644 index 00000000..df2d8889 --- /dev/null +++ b/test/features/chat/presentation/pages/empty_chat_screen_smoke_test.dart @@ -0,0 +1,40 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart'; +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +void main() { + group('EmptyChatScreen smoke', () { + Widget wrap(Widget child) => MaterialApp(theme: ThemeHelper().themeData(), home: child); + + testWidgets('renders', (tester) async { + const user = ProfileModel( + id: 1, + name: 'User', + email: 'u@e.com', + phoneConfirmationState: 'confirmed', + slug: 'user', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + profilePhotoUrl: 'http://localhost/avatar.png', + roles: [], + badges: [], + subscriptions: [], + subscribers: [], + ); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget(wrap(const EmptyChatScreen(user: user))); + await tester.pump(); + }); + + expect(find.byType(EmptyChatScreen), findsOneWidget); + expect(find.text('Сообщение'), findsOneWidget); + }); + }); +} diff --git a/test/features/chat/presentation/pages/photo_view_screen_smoke_test.dart b/test/features/chat/presentation/pages/photo_view_screen_smoke_test.dart new file mode 100644 index 00000000..4381d1d7 --- /dev/null +++ b/test/features/chat/presentation/pages/photo_view_screen_smoke_test.dart @@ -0,0 +1,152 @@ +import 'dart:async'; +import 'dart:io'; + +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class _FakeHttpClient implements HttpClient { + bool _autoUncompress = true; + + @override + bool get autoUncompress => _autoUncompress; + + @override + set autoUncompress(bool value) { + _autoUncompress = value; + } + + @override + Future getUrl(Uri url) async => _FakeHttpClientRequest(); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeHttpClientRequest implements HttpClientRequest { + @override + Future close() async => _FakeHttpClientResponse(); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeHttpClientResponse extends Stream> implements HttpClientResponse { + static final List _png = [ + 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, + ]; + + @override + int get statusCode => 200; + + @override + int get contentLength => _png.length; + + @override + HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed; + + @override + StreamSubscription> listen(void Function(List)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) { + return Stream>.fromIterable([_png]).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('PhotoViewScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + testWidgets('renders with a network image url', (tester) async { + await HttpOverrides.runZoned(() async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeHelper().themeData(), + home: const PhotoViewScreen(imageUrls: ['https://example.com/a.png'], index: 0), + ), + ); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + expect(find.byType(PhotoViewScreen), findsOneWidget); + }, createHttpClient: (context) => _FakeHttpClient()); + }); + }); +} diff --git a/test/features/chat/presentation/widgets/chat_bottom_sheets_smoke_test.dart b/test/features/chat/presentation/widgets/chat_bottom_sheets_smoke_test.dart new file mode 100644 index 00000000..1646550f --- /dev/null +++ b/test/features/chat/presentation/widgets/chat_bottom_sheets_smoke_test.dart @@ -0,0 +1,205 @@ + +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/chose_plan_date_widget.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:go_router/go_router.dart'; + +class FakeChatRoomCubit extends Cubit implements ChatRoomCubit { + FakeChatRoomCubit(super.initialState); + + bool finishResult = true; + bool complainResult = true; + + @override + Future choseStartPlanDate(DateTime date) async => true; + + @override + Future finishChosenPlan() async => finishResult; + + @override + Future complainAboutConsultation({required String description}) async => complainResult; + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeAssetBundle extends CachingAssetBundle { + _FakeAssetBundle(this.assets); + final Map assets; + + @override + Future loadString(String key, {bool cache = true}) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return value; + } + + @override + Future load(String key) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return ByteData.sublistView(Uint8List.fromList(value.codeUnits)); + } +} + +void main() { + group('Chat bottom sheets smoke', () { + const fakeSvg = ''' + + + +'''; + + final bundle = _FakeAssetBundle({'assets/icons/error_icon.svg': fakeSvg, 'assets/icons/question_mark.svg': fakeSvg}); + + Widget wrap(Widget child) { + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (_, _) => Scaffold(body: SingleChildScrollView(child: child)), + ), + ], + ); + return DefaultAssetBundle( + bundle: bundle, + child: Portal( + child: MaterialApp.router( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + routerConfig: router, + ), + ), + ); + } + + testWidgets('ChosePlanDateWidget renders', (tester) async { + final cubit = FakeChatRoomCubit(const ChatRoomState.loading()); + await tester.pumpWidget(wrap(ChosePlanDateWidget(chatCubit: cubit, hasPersonalDiet: false))); + await tester.pump(); + expect(find.text(S.current.confirm), findsOneWidget); + }); + + testWidgets('ChosePlanDateWidget toggles tooltip when hasPersonalDiet', (tester) async { + final cubit = FakeChatRoomCubit(const ChatRoomState.loading()); + await tester.pumpWidget(wrap(ChosePlanDateWidget(chatCubit: cubit, hasPersonalDiet: true))); + await tester.pump(); + + + expect(find.text(S.current.chosenPlanStartDateDescr), findsOneWidget); + await tester.tap(find.byType(GestureDetector).first); + await tester.pump(); + expect(find.text(S.current.chosenPlanStartDateDescr), findsOneWidget); + }); + + testWidgets('CompletePlanBottomSheet shows failure UI when finish fails', (tester) async { + final cubit = FakeChatRoomCubit(const ChatRoomState.loading())..finishResult = false; + await tester.pumpWidget(wrap(CompletePlanBottomSheet(cubit: cubit))); + await tester.pump(); + await tester.tap(find.text(S.current.confirm)); + await tester.pump(); + expect(find.text('Произошла ошибка, попробуйте позже'), findsOneWidget); + expect(find.text(S.current.ok), findsOneWidget); + }); + + testWidgets('CompletePlanBottomSheet closes on success', (tester) async { + final cubit = FakeChatRoomCubit(const ChatRoomState.loading())..finishResult = true; + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => Scaffold( + body: Center( + child: TextButton(onPressed: () => context.push('/sheet'), child: const Text('open')), + ), + ), + ), + GoRoute( + path: '/sheet', + builder: (_, _) => Scaffold( + body: SingleChildScrollView(child: CompletePlanBottomSheet(cubit: cubit)), + ), + ), + ], + ); + await tester.pumpWidget( + DefaultAssetBundle( + bundle: bundle, + child: Portal( + child: MaterialApp.router( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + routerConfig: router, + ), + ), + ), + ); + await tester.pump(); + await tester.tap(find.text('open')); + await tester.pumpAndSettle(); + + expect(find.byType(CompletePlanBottomSheet), findsOneWidget); + await tester.tap(find.text(S.current.confirm)); + await tester.pumpAndSettle(); + expect(find.byType(CompletePlanBottomSheet), findsNothing); + }); + + testWidgets('PlanComplainBottomSheet validates text length', (tester) async { + final cubit = FakeChatRoomCubit(const ChatRoomState.loading()); + await tester.pumpWidget(wrap(PlanComplainBottomSheet(cubit: cubit))); + await tester.pump(); + await tester.tap(find.text(S.current.confirm)); + await tester.pump(); + + await tester.enterText(find.byType(TextFormField), 'a'); + await tester.pump(); + expect(find.text(S.current.atLeast3Symbols), findsOneWidget); + }); + + testWidgets('PlanComplainBottomSheet shows success UI when submit succeeds', (tester) async { + final cubit = FakeChatRoomCubit(const ChatRoomState.loading())..complainResult = true; + await tester.pumpWidget(wrap(PlanComplainBottomSheet(cubit: cubit))); + await tester.pump(); + await tester.enterText(find.byType(TextFormField), 'abc'); + await tester.pump(); + await tester.tap(find.text(S.current.confirm)); + await tester.pump(); + expect(find.text('Жалоба отправлена на рассмотрение. Обычно это занимает до трёх рабочих дней.'), findsOneWidget); + expect(find.text(S.current.ok), findsOneWidget); + }); + + testWidgets('PlanComplainBottomSheet shows failure UI when submit fails', (tester) async { + final cubit = FakeChatRoomCubit(const ChatRoomState.loading())..complainResult = false; + await tester.pumpWidget(wrap(PlanComplainBottomSheet(cubit: cubit))); + await tester.pump(); + await tester.enterText(find.byType(TextFormField), 'abc'); + await tester.pump(); + await tester.tap(find.text(S.current.confirm)); + await tester.pump(); + expect(find.text('Произошла ошибка, попробуйте позже'), findsOneWidget); + expect(find.text(S.current.ok), findsOneWidget); + }); + }); +} diff --git a/test/features/chat/presentation/widgets/chat_bubble_widget_smoke_test.dart b/test/features/chat/presentation/widgets/chat_bubble_widget_smoke_test.dart new file mode 100644 index 00000000..ba51e3c1 --- /dev/null +++ b/test/features/chat/presentation/widgets/chat_bubble_widget_smoke_test.dart @@ -0,0 +1,219 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class _FakeHttpClient implements HttpClient { + bool _autoUncompress = true; + + @override + bool get autoUncompress => _autoUncompress; + + @override + set autoUncompress(bool value) { + _autoUncompress = value; + } + + @override + Future getUrl(Uri url) async => _FakeHttpClientRequest(); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeHttpClientRequest implements HttpClientRequest { + @override + Future close() async => _FakeHttpClientResponse(); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeHttpClientResponse extends Stream> implements HttpClientResponse { + + static final List _png = [ + 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, + ]; + + @override + int get statusCode => 200; + + @override + int get contentLength => _png.length; + + @override + HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed; + + @override + StreamSubscription> listen(void Function(List)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) { + return Stream>.fromIterable([_png]).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeAssetBundle extends CachingAssetBundle { + _FakeAssetBundle(this.assets); + final Map assets; + + @override + Future loadString(String key, {bool cache = true}) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return value; + } + + @override + Future load(String key) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return ByteData.sublistView(Uint8List.fromList(value.codeUnits)); + } +} + +void main() { + group('ChatBubbleWidget smoke', () { + const fakeSvg = ''' + + + +'''; + + final bundle = _FakeAssetBundle({'assets/icons/error_icon.svg': fakeSvg}); + + Widget wrap(Widget child) { + return DefaultAssetBundle( + bundle: bundle, + child: MaterialApp( + theme: ThemeHelper().themeData(), + home: MediaQuery( + data: const MediaQueryData(size: Size(1200, 800)), + child: Scaffold(body: child), + ), + ), + ); + } + + testWidgets('renders with network image attachment', (tester) async { + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(2000, 1200); + addTearDown(tester.view.reset); + + await HttpOverrides.runZoned(() async { + await tester.pumpWidget( + wrap( + ChatBubbleWidget( + sendTime: DateTime(2025, 1, 1, 10, 0), + text: 'text', + isCurrentUser: true, + isRead: true, + mediaData: const ['https://example.com/a.png'], + messageState: MessageState.sent, + messageUuid: 'u', + userSlug: 'slug', + ), + ), + ); + await tester.pump(const Duration(milliseconds: 200)); + expect(find.byType(ChatBubbleWidget), findsOneWidget); + expect(find.text('text'), findsOneWidget); + }, createHttpClient: (_) => _FakeHttpClient()); + }); + + testWidgets('renders error state icon for current user', (tester) async { + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(2000, 1200); + addTearDown(tester.view.reset); + + await tester.pumpWidget( + wrap( + ChatBubbleWidget( + sendTime: DateTime(2025, 1, 1, 10, 0), + text: 'tap to retry', + isCurrentUser: true, + isRead: true, + mediaData: const [], + messageState: MessageState.error, + messageUuid: 'u', + userSlug: 'slug', + ), + ), + ); + await tester.pump(); + expect(find.byType(ChatBubbleWidget), findsOneWidget); + expect(find.text('tap to retry'), findsOneWidget); + }); + }); +} diff --git a/test/features/chat/presentation/widgets/chat_list_widgets_smoke_test.dart b/test/features/chat/presentation/widgets/chat_list_widgets_smoke_test.dart new file mode 100644 index 00000000..06c4cd71 --- /dev/null +++ b/test/features/chat/presentation/widgets/chat_list_widgets_smoke_test.dart @@ -0,0 +1,95 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; +import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/chat_entry_widget.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/chat_search_bar_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +void main() { + group('Chat list widgets smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) { + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (_, _) => Scaffold(body: child), + ), + GoRoute(path: '/chat', name: 'chatScreen', builder: (_, _) => const SizedBox.shrink()), + ], + ); + return MaterialApp.router( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + routerConfig: router, + ); + } + + testWidgets('ChatSearchBarWidget clears text and calls onSuffixIconTap', (tester) async { + final controller = TextEditingController(text: 'abc'); + var called = 0; + + await tester.pumpWidget(wrap(ChatSearchBarWidget(controller: controller, hintText: 'Search', onSuffixIconTap: () => called++))); + await tester.pump(); + + expect(find.byType(TextFormField), findsOneWidget); + await tester.tap(find.byIcon(Icons.clear)); + await tester.pump(); + expect(controller.text, ''); + expect(called, 1); + }); + + testWidgets('ChatEntryWidget renders unread badge and attachment preview', (tester) async { + final chatRoom = ChatRoom( + id: '1', + lastMessage: ChatMessageModel( + id: 1, + data: 'ignored', + mediaData: const ['chat_files/file.pdf'], + authorId: 2, + updatedAt: DateTime(2025, 1, 1), + isRead: false, + isCurrentUserAuthor: false, + roomId: '1', + ), + users: const Users( + fromChatUser: ChatUser(id: 1, name: 'Me', profilePhotoPath: null, profilePhotoUrl: 'http://localhost/a.png'), + toChatUser: ChatUser(id: 2, name: 'Other User', profilePhotoPath: null, profilePhotoUrl: 'http://localhost/b.png'), + ), + unreadMessagesCount: 3, + chosenPlan: null, + ); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget(wrap(ChatEntryWidget(chatRoom: chatRoom))); + await tester.pump(const Duration(milliseconds: 200)); + expect(find.text('Other User'), findsOneWidget); + expect(find.text('3'), findsOneWidget); + expect(find.text('📎 Вложение'), findsOneWidget); + }); + }); + }); +} diff --git a/test/features/chat/presentation/widgets/chat_misc_widgets_smoke_test.dart b/test/features/chat/presentation/widgets/chat_misc_widgets_smoke_test.dart new file mode 100644 index 00000000..98b71536 --- /dev/null +++ b/test/features/chat/presentation/widgets/chat_misc_widgets_smoke_test.dart @@ -0,0 +1,105 @@ + +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/message_action_bottom_sheet.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class _FakeAssetBundle extends CachingAssetBundle { + _FakeAssetBundle(this.assets); + final Map assets; + + @override + Future loadString(String key, {bool cache = true}) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return value; + } + + @override + Future load(String key) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return ByteData.sublistView(Uint8List.fromList(value.codeUnits)); + } +} + +void main() { + group('Chat misc widgets smoke', () { + const fakeSvg = ''' + + + +'''; + + final bundle = _FakeAssetBundle({ + 'assets/icons/right_arrow.svg': fakeSvg, + 'assets/icons/resend.svg': fakeSvg, + 'assets/icons/trash_can.svg': fakeSvg, + 'assets/icons/reversed_exclamation_mark.svg': fakeSvg, + 'assets/icons/star.svg': fakeSvg, + }); + + Widget wrap(Widget child) { + return DefaultAssetBundle( + bundle: bundle, + child: MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: MediaQuery( + data: const MediaQueryData(size: Size(500, 900)), + child: Scaffold(body: child), + ), + ), + ); + } + + testWidgets('AboutOrderButton renders and taps', (tester) async { + var tapped = false; + await tester.pumpWidget(wrap(AboutOrderButton(onTap: () => tapped = true))); + await tester.pump(); + expect(find.byType(AboutOrderButton), findsOneWidget); + await tester.tap(find.byType(AboutOrderButton)); + await tester.pump(); + expect(tapped, isTrue); + }); + + testWidgets('MessageActionsBottomSheet renders', (tester) async { + await tester.pumpWidget(wrap(MessageActionsBottomSheet(resend: () {}, delete: () {}))); + await tester.pump(); + expect(find.byType(MessageActionsBottomSheet), findsOneWidget); + expect(find.text(S.current.resend), findsOneWidget); + expect(find.text(S.current.delete), findsOneWidget); + }); + + testWidgets('NotificationBanner renders for multiple banner types', (tester) async { + await tester.pumpWidget( + wrap( + const SingleChildScrollView( + child: Column( + children: [ + NotificationBanner(bannerType: BannerType.date, hasPersonalDiet: false), + NotificationBanner(bannerType: BannerType.card), + NotificationBanner(bannerType: BannerType.collectCharge, planName: 'Plan', planPrice: 100, chosenPlanId: 1), + NotificationBanner(bannerType: BannerType.finishPlan, planName: 'Plan', planPrice: 100, chosenPlanId: 1), + NotificationBanner(bannerType: BannerType.setPersonalDiet, hasPersonalDiet: true), + ], + ), + ), + ), + ); + await tester.pump(); + expect(find.byType(NotificationBanner), findsWidgets); + }); + }); +} diff --git a/test/features/chat/presentation/widgets/order_card_smoke_test.dart b/test/features/chat/presentation/widgets/order_card_smoke_test.dart new file mode 100644 index 00000000..459d3ee7 --- /dev/null +++ b/test/features/chat/presentation/widgets/order_card_smoke_test.dart @@ -0,0 +1,204 @@ +import 'dart:async'; +import 'dart:io'; + +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/order_card.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/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class _FakeHttpClient implements HttpClient { + bool _autoUncompress = true; + + @override + bool get autoUncompress => _autoUncompress; + + @override + set autoUncompress(bool value) { + _autoUncompress = value; + } + + @override + Future getUrl(Uri url) async => _FakeHttpClientRequest(); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeHttpClientRequest implements HttpClientRequest { + @override + Future close() async => _FakeHttpClientResponse(); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeHttpClientResponse extends Stream> implements HttpClientResponse { + static final List _png = [ + 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, + ]; + + @override + int get statusCode => 200; + + @override + int get contentLength => _png.length; + + @override + HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed; + + @override + StreamSubscription> listen(void Function(List)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) { + return Stream>.fromIterable([_png]).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('OrderCard smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: Scaffold(body: SingleChildScrollView(child: child)), + ); + } + + const author = Author( + authorSlug: 'a', + authorName: 'Author', + authorProfileIcon: 'http://localhost/a.png', + authorIsMe: false, + isEntitySubscribedByMe: false, + authorPlaceInRating: 1, + ); + + const plan = PlanModel( + id: 1, + slug: 'slug', + name: 'Plan', + image: 'img.png', + duration: 7, + state: 'active', + type: PlanType(fit: false, slim: false, behavior: false, wellness: false), + details: '', + packetsCount: 1, + packet1Options: PacketOptions(items: ['a'], price: 100, short: 'S', description: '', hasPersonalDiet: false), + packet2Options: null, + packet3Options: null, + author: author, + totalLikes: 0, + totalViews: 0, + totalComments: 0, + isFavorite: false, + minPriceOfOptions: 100, + maxPriceOfOptions: 100, + wasLikedByMe: false, + ); + + testWidgets('renders and runs cancel action', (tester) async { + var cancelled = 0; + await HttpOverrides.runZoned(() async { + await tester.pumpWidget(wrap(OrderCard(plan: plan, isCustomerView: true, optionType: 'options1', onCancel: () async => cancelled++))); + await tester.pump(const Duration(milliseconds: 200)); + expect(find.text('Plan'), findsOneWidget); + expect(find.text(S.current.cancel_order), findsOneWidget); + + await tester.tap(find.text(S.current.cancel_order)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 50)); + expect(cancelled, 1); + }, createHttpClient: (_) => _FakeHttpClient()); + }); + }); +} diff --git a/test/features/complaining/presentation/complaining_bottom_sheet_smoke_test.dart b/test/features/complaining/presentation/complaining_bottom_sheet_smoke_test.dart new file mode 100644 index 00000000..55591318 --- /dev/null +++ b/test/features/complaining/presentation/complaining_bottom_sheet_smoke_test.dart @@ -0,0 +1,66 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; +import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; +import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +import 'complaining_bottom_sheet_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('ComplainingBottomSheet smoke', () { + late MockComplainingRepository repository; + + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + + repository = MockComplainingRepository(); + sl.registerLazySingleton(() => repository); + sl.registerFactory(() => ComplainingCubit(repository: sl())); + }); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: Scaffold(body: child), + ); + } + + testWidgets('renders category selection', (tester) async { + await tester.pumpWidget( + wrap( + const ComplainingBottomSheet( + complainedContentType: ComplainedContent.plan, + slug: 'test-slug', + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(ComplainingBottomSheet), findsOneWidget); + + }); + }); +} + diff --git a/test/features/consultant/become_consultant/presentation/pages/certificates_and_diplomas_screen_smoke_test.dart b/test/features/consultant/become_consultant/presentation/pages/certificates_and_diplomas_screen_smoke_test.dart new file mode 100644 index 00000000..e7762f29 --- /dev/null +++ b/test/features/consultant/become_consultant/presentation/pages/certificates_and_diplomas_screen_smoke_test.dart @@ -0,0 +1,86 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/certificates_and_diplomas_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'certificates_and_diplomas_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('CertificatesAndDiplomasScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: Scaffold(body: child), + ); + } + + testWidgets('renders certificates and diplomas screen', (tester) async { + final cubit = MockRegisterConsultantCubit(); + const consultant = ConsultantProfileModel(hasMedicalEducation: null, socialLinks: null, photos: null, certificates: null); + const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => Stream.value(state)); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(cubit.updateConsultantProfileModel(any)).thenReturn(null); + + await tester.pumpWidget(BlocProvider.value(value: cubit, child: wrap(const CertificatesAndDiplomasScreen()))); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(CertificatesAndDiplomasScreen), findsOneWidget); + expect(find.text('Добавьте дипломы\nи сертификаты'), findsOneWidget); + }); + + testWidgets('renders with existing certificates', (tester) async { + final cubit = MockRegisterConsultantCubit(); + const consultant = ConsultantProfileModel( + hasMedicalEducation: null, + socialLinks: null, + photos: null, + certificates: [CertificatesModel(title: 'Test Certificate', date: '2025-01-01')], + ); + const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => Stream.value(state)); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(cubit.updateConsultantProfileModel(any)).thenReturn(null); + + await tester.pumpWidget(BlocProvider.value(value: cubit, child: wrap(const CertificatesAndDiplomasScreen()))); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(CertificatesAndDiplomasScreen), findsOneWidget); + }); + }); +} diff --git a/test/features/consultant/become_consultant/presentation/pages/social_networks_screen_smoke_test.dart b/test/features/consultant/become_consultant/presentation/pages/social_networks_screen_smoke_test.dart new file mode 100644 index 00000000..c58c49fa --- /dev/null +++ b/test/features/consultant/become_consultant/presentation/pages/social_networks_screen_smoke_test.dart @@ -0,0 +1,90 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/social_networks_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'social_networks_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('SocialNetworksScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: Scaffold(body: child), + ); + } + + testWidgets('renders social networks screen', (tester) async { + final cubit = MockRegisterConsultantCubit(); + const consultant = ConsultantProfileModel( + hasMedicalEducation: null, + socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null), + photos: null, + certificates: null, + ); + const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => Stream.value(state)); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(cubit.updateConsultantProfileModel(any)).thenReturn(null); + + await tester.pumpWidget(BlocProvider.value(value: cubit, child: wrap(const SocialNetworksScreen()))); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(SocialNetworksScreen), findsOneWidget); + }); + + testWidgets('renders with existing social links', (tester) async { + final cubit = MockRegisterConsultantCubit(); + const consultant = ConsultantProfileModel( + hasMedicalEducation: null, + socialLinks: SocialLinksModel(vkLink: 'https://vk.com/test', tgLink: 'https://t.me/test', fbLink: null, instaLink: null, personalSiteLink: null), + photos: null, + certificates: null, + ); + const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => Stream.value(state)); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(cubit.updateConsultantProfileModel(any)).thenReturn(null); + + await tester.pumpWidget(BlocProvider.value(value: cubit, child: wrap(const SocialNetworksScreen()))); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(SocialNetworksScreen), findsOneWidget); + }); + }); +} diff --git a/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart b/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart new file mode 100644 index 00000000..d6d42689 --- /dev/null +++ b/test/features/consultations/presentation/widgets/consultation_card_widget_test.dart @@ -0,0 +1,103 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_card_widget.dart'; +import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +void main() { + group('ConsultationCardWidget smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + final mockConsultation = ConsultationModel( + id: 1, + chosenPlanState: ChosenPlanStateEnum.stateInProgress, + planTitle: 'Test Plan', + userName: 'John Doe', + userSlug: 'john-doe', + consultantPayment: 1000.0, + duration: 7, + currentDiaryDay: 3, + userProfilePhotoUrl: 'https://example.com/avatar.jpg', + consultationOptionPrice: 500, + startDate: DateTime(2025, 1, 1), + endDate: DateTime(2025, 1, 8), + consultantId: 1, + diaries: [ + DiaryModel(id: 1, state: DiaryStateEnum.finished, date: DateTime(2025, 1, 1)), + DiaryModel(id: 2, state: DiaryStateEnum.commented, date: DateTime(2025, 1, 2)), + DiaryModel(id: 3, state: DiaryStateEnum.init, date: DateTime(2025, 1, 3)), + ], + personalDietId: null, + ); + + Widget wrap(Widget child) { + return MaterialApp.router( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + routerConfig: GoRouter( + initialLocation: '/', + routes: [ + GoRoute(path: '/', builder: (context, state) => child), + GoRoute( + path: '/profile-screen', + name: 'profileScreen', + builder: (context, state) => const Scaffold(body: Text('Profile')), + ), + GoRoute( + path: '/user-details-screen', + name: 'userDetailsScreen', + builder: (context, state) => const Scaffold(body: Text('User Details')), + ), + GoRoute( + path: '/diary-screen', + name: 'diaryScreen', + builder: (context, state) => const Scaffold(body: Text('Diary')), + ), + ], + ), + ); + } + + testWidgets('renders consultation card with user info', (tester) async { + await mockNetworkImagesFor(() async { + await tester.pumpWidget(wrap(ConsultationCardWidget(consultationsModel: mockConsultation))); + await tester.pumpAndSettle(); + + expect(find.text('John Doe'), findsOneWidget); + expect(find.text('Test Plan'), findsOneWidget); + }); + }); + + testWidgets('renders diary days', (tester) async { + await mockNetworkImagesFor(() async { + await tester.pumpWidget(wrap(ConsultationCardWidget(consultationsModel: mockConsultation))); + await tester.pumpAndSettle(); + + + expect(find.byType(InkWell), findsWidgets); + }); + }); + }); +} diff --git a/test/features/create_diet/presentation/cubit/create_diet_cubit_test.dart b/test/features/create_diet/presentation/cubit/create_diet_cubit_test.dart new file mode 100644 index 00000000..11654e2e --- /dev/null +++ b/test/features/create_diet/presentation/cubit/create_diet_cubit_test.dart @@ -0,0 +1,135 @@ +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/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_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +import 'create_diet_cubit_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec(), MockSpec()]) +void main() { + group('CreateDietCubit unit', () { + late CreateDietCubit cubit; + late MockCreateDietRepository dietRepository; + late MockProfileRepository profileRepository; + late MockGlossaryRepository glossaryRepository; + + setUp(() { + dietRepository = MockCreateDietRepository(); + profileRepository = MockProfileRepository(); + glossaryRepository = MockGlossaryRepository(); + cubit = CreateDietCubit(createDietRepository: dietRepository, profileRepository: profileRepository, glossaryRepository: glossaryRepository); + }); + + tearDown(() { + cubit.close(); + }); + + test('initial state is correct', () { + expect(cubit.state.isLoading, false); + expect(cubit.state.isDietBeingEdited, false); + expect(cubit.state.dietData.name, null); + expect(cubit.state.dietData.dailySchedule, null); + }); + + test('setName updates name and clears error', () { + cubit.setName('Test Diet'); + expect(cubit.state.dietData.name, 'Test Diet'); + expect(cubit.state.dietNameError, null); + }); + + test('setDetails updates details and clears error', () { + cubit.setDetails('Test description'); + expect(cubit.state.dietData.details, 'Test description'); + expect(cubit.state.dietDescriptionError, null); + }); + + test('setDuration updates duration and creates daily schedule', () { + cubit.setDuration(0); + expect(cubit.state.durationIndex, 0); + expect(cubit.state.dietData.duration, 7); + expect(cubit.state.dietData.dailySchedule?.length, 7); + expect(cubit.state.dietDurationError, null); + }); + + test('setDuration adjusts daily schedule when changing duration', () { + cubit.setDuration(0); // 7 days + expect(cubit.state.dietData.dailySchedule?.length, 7); + + cubit.setDuration(1); // 14 days + expect(cubit.state.dietData.dailySchedule?.length, 14); + + cubit.setDuration(0); // back to 7 days + expect(cubit.state.dietData.dailySchedule?.length, 7); + }); + + test('setDietState updates state and clears error', () { + cubit.setDietState(DietState.active); + expect(cubit.state.dietState, DietState.active); + expect(cubit.state.dietData.state, DietState.active); + expect(cubit.state.dietStateError, null); + }); + + test('setDietType updates type', () { + cubit.setDietType(key: 'veg', value: true); + expect(cubit.state.dietData.type['veg'], true); + + cubit.setDietType(key: 'veg', value: false); + expect(cubit.state.dietData.type['veg'], false); + }); + + test('addPortion adds portion to daily schedule', () { + cubit.setDuration(0); // Create 7 days schedule + const portion = DailySchedule(name: 'Apple', portion: Portion.grams, quantity: 100, dailyNutrition: null, productNutritionalValueId: null); + + cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion); + expect(cubit.state.dietData.dailySchedule?[0][0].length, 1); + expect(cubit.state.dietData.dailySchedule?[0][0][0].name, 'Apple'); + }); + + test('removePortion removes portion from daily schedule', () { + cubit.setDuration(0); + const portion = DailySchedule(name: 'Apple', portion: Portion.grams, quantity: 100, dailyNutrition: null, productNutritionalValueId: null); + + cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion); + expect(cubit.state.dietData.dailySchedule?[0][0].length, 1); + + cubit.removePortion(day: 0, meal: 0, portionIndex: 0); + expect(cubit.state.dietData.dailySchedule?[0][0].length, 0); + }); + + test('updatePortion updates portion in daily schedule', () { + cubit.setDuration(0); + const portion1 = DailySchedule(name: 'Apple', portion: Portion.grams, quantity: 100, dailyNutrition: null, productNutritionalValueId: null); + const portion2 = DailySchedule(name: 'Banana', portion: Portion.grams, quantity: 150, dailyNutrition: null, productNutritionalValueId: null); + + cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion1); + expect(cubit.state.dietData.dailySchedule?[0][0][0].name, 'Apple'); + + cubit.updatePortion(dayIndex: 0, mealIndex: 0, portionIndex: 0, portion: portion2); + expect(cubit.state.dietData.dailySchedule?[0][0][0].name, 'Banana'); + }); + + test('copyDay and pasteCopiedDay copy and paste day', () { + cubit.setDuration(0); + const portion = DailySchedule(name: 'Apple', portion: Portion.grams, quantity: 100, dailyNutrition: null, productNutritionalValueId: null); + + cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion); + cubit.copyDay(0); + expect(cubit.state.copiedDay, isNotNull); + + cubit.pasteCopiedDay(1); + expect(cubit.state.dietData.dailySchedule?[1][0].length, 1); + expect(cubit.state.dietData.dailySchedule?[1][0][0].name, 'Apple'); + }); + + test('setDailySchedule updates daily schedule', () { + final schedule = List.generate(7, (day) => List.generate(5, (meal) => [])); + cubit.setDailySchedule(schedule); + expect(cubit.state.dietData.dailySchedule, schedule); + expect(cubit.state.dietDurationError, null); + }); + }); +} diff --git a/test/features/create_plan/presentation/cubit/create_plan_cubit_test.dart b/test/features/create_plan/presentation/cubit/create_plan_cubit_test.dart new file mode 100644 index 00000000..ab1608d1 --- /dev/null +++ b/test/features/create_plan/presentation/cubit/create_plan_cubit_test.dart @@ -0,0 +1,151 @@ +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:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +import 'create_plan_cubit_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('CreatePlanCubit unit', () { + late CreatePlanCubit cubit; + late MockCreatePlanRepository repository; + + setUp(() { + repository = MockCreatePlanRepository(); + cubit = CreatePlanCubit(createPlanRepository: repository); + }); + + tearDown(() { + cubit.close(); + }); + + test('initial state is correct', () { + expect(cubit.state.isLoading, false); + expect(cubit.state.planCreated, false); + expect(cubit.state.errorMessage, null); + expect(cubit.state.planData.name, null); + expect(cubit.state.planData.options.length, 3); + }); + + test('setName updates name and validates', () { + cubit.setName('Test Plan'); + expect(cubit.state.planData.name, 'Test Plan'); + expect(cubit.state.validationState.titleHasError, false); + + cubit.setName('AB'); + expect(cubit.state.validationState.titleHasError, true); + }); + + test('setDetails updates details and validates', () { + cubit.setDetails('Test description'); + expect(cubit.state.planData.details, 'Test description'); + expect(cubit.state.validationState.descriptionHasError, false); + + cubit.setDetails('AB'); + expect(cubit.state.validationState.descriptionHasError, true); + }); + + test('setDuration updates duration and validates', () { + cubit.setDuration(0); + expect(cubit.state.durationIndex, 0); + expect(cubit.state.planData.duration, 7); + expect(cubit.state.validationState.durationHasError, false); + }); + + test('setPlanType updates type and validates', () { + cubit.setPlanType([true, false, false, false]); + expect(cubit.state.planData.type, [true, false, false, false]); + expect(cubit.state.validationState.typeHasError, false); + + cubit.setPlanType([false, false, false, false]); + expect(cubit.state.planData.type, [false, false, false, false]); + }); + + test('setSelectedOptionIndex updates selected index', () { + cubit.setSelectedOptionIndex(1); + expect(cubit.state.selectedOptionIndex, 1); + + cubit.setSelectedOptionIndex(null); + expect(cubit.state.selectedOptionIndex, null); + }); + + test('setOptionTitle updates option title and validates', () { + cubit.setSelectedOptionIndex(0); + cubit.setOptionTitle('Option 1'); + expect(cubit.state.planData.options[0].short, 'Option 1'); + }); + + test('setPrice updates price and validates', () { + cubit.setSelectedOptionIndex(0); + cubit.setPrice(1500.0); + expect(cubit.state.planData.options[0].price, 1500.0); + expect(cubit.state.validationState.optionsValidationState[0].priceHasError, false); + + cubit.setPrice(1000.0); + expect(cubit.state.validationState.optionsValidationState[0].priceHasError, true); + }); + + test('setOptionDescription updates description and validates', () { + cubit.setSelectedOptionIndex(0); + cubit.setOptionDescription('Test description'); + expect(cubit.state.planData.options[0].description, 'Test description'); + }); + + test('setOptionContent adds and updates items', () { + cubit.setSelectedOptionIndex(0); + cubit.setOptionContent('Item 1', 0); + expect(cubit.state.planData.options[0].items.length, 1); + expect(cubit.state.planData.options[0].items[0], 'Item 1'); + + cubit.setOptionContent('Item 1 Updated', 0); + expect(cubit.state.planData.options[0].items[0], 'Item 1 Updated'); + }); + + test('removeOptionContentItem removes item', () { + cubit.setSelectedOptionIndex(0); + cubit.setOptionContent('Item 1', 0); + cubit.setOptionContent('Item 2', 1); + expect(cubit.state.planData.options[0].items.length, 2); + + cubit.removeOptionContentItem(0); + expect(cubit.state.planData.options[0].items.length, 1); + expect(cubit.state.planData.options[0].items[0], 'Item 2'); + }); + + test('togglePersonalizedDietOptionState toggles hasPersonalDiet', () { + cubit.setSelectedOptionIndex(0); + final initialValue = cubit.state.planData.options[0].hasPersonalDiet; + + cubit.togglePersonalizedDietOptionState(); + expect(cubit.state.planData.options[0].hasPersonalDiet, !initialValue); + + cubit.togglePersonalizedDietOptionState(); + expect(cubit.state.planData.options[0].hasPersonalDiet, initialValue); + }); + + test('validateSelectedOptionItems validates items', () { + cubit.setSelectedOptionIndex(0); + expect(cubit.validateSelectedOptionItems(), false); + expect(cubit.state.validationState.optionsValidationState[0].isItemsEmpty, true); + + cubit.setOptionContent('Valid item', 0); + expect(cubit.validateSelectedOptionItems(), true); + expect(cubit.state.validationState.optionsValidationState[0].isItemsEmpty, false); + + cubit.setOptionContent('AB', 1); + expect(cubit.validateSelectedOptionItems(), false); + }); + + test('validatePriceOfAllFilledOptions validates all options', () { + // Set prices for all options + cubit.setSelectedOptionIndex(0); + cubit.setPrice(1500.0); + cubit.setSelectedOptionIndex(1); + cubit.setPrice(1500.0); + cubit.setSelectedOptionIndex(2); + cubit.setPrice(1500.0); + expect(cubit.validatePriceOfAllFilledOptions(), true); + }); + }); +} diff --git a/test/features/diary/presentation/pages/diary_screen_smoke_test.dart b/test/features/diary/presentation/pages/diary_screen_smoke_test.dart new file mode 100644 index 00000000..9df724ce --- /dev/null +++ b/test/features/diary/presentation/pages/diary_screen_smoke_test.dart @@ -0,0 +1,238 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; +import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart'; +import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/meal_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'diary_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('DiaryScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ); + } + + DiaryStateLoaded loadedState({required bool hasChosenPlan}) { + final diary = DiaryDayEntity( + id: 1, + date: '2025-01-01', + wakeup: '07:00', + retire: '23:00', + closed: 0, + comment: '', + typeOfDiary: TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: hasChosenPlan, diaryState: null), + meals: const [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])], + chosenPlanId: hasChosenPlan ? 10 : null, + dayOfChosenDiet: null, + diet: null, + ); + const nutrition = NutritionDataModel( + dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + ); + return DiaryState.loaded( + diary: diary, + nutrition: nutrition, + selectedDay: DateTime(2025, 1, 1), + selectedMeal: 1, + isConsultantView: false, + isDiaryFinished: false, + isDiaryCommented: false, + isWakeUpAndRetireEnabled: true, + isMoodEnabled: true, + ) + as DiaryStateLoaded; + } + + testWidgets('renders loading state', (tester) async { + final diaryCubit = MockDiaryCubit(); + when(diaryCubit.state).thenReturn(const DiaryState.loading()); + when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); + when(diaryCubit.close()).thenAnswer((_) async => Future.value()); + when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + + final commentsCubit = MockConsultantCommentsCubit(); + when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + + sl.registerFactory(() => diaryCubit); + sl.registerFactory(() => commentsCubit); + + await tester.pumpWidget(wrap(const DiaryScreen())); + await tester.pump(); + + expect(find.byType(DiaryScreen), findsOneWidget); + }); + + testWidgets('renders loaded state (chosen plan)', (tester) async { + final diaryCubit = MockDiaryCubit(); + when(diaryCubit.state).thenReturn(loadedState(hasChosenPlan: true)); + when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); + when(diaryCubit.close()).thenAnswer((_) async => Future.value()); + when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + + final commentsCubit = MockConsultantCommentsCubit(); + when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: false)); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + + sl.registerFactory(() => diaryCubit); + sl.registerFactory(() => commentsCubit); + + await tester.pumpWidget(wrap(const DiaryScreen())); + await tester.pumpAndSettle(const Duration(milliseconds: 300)); + + expect(find.byType(DiaryScreen), findsOneWidget); + + expect(find.text('Дополнительная информация для консультанта'), findsOneWidget); + }); + + testWidgets('renders error state and reload button', (tester) async { + final diaryCubit = MockDiaryCubit(); + when(diaryCubit.state).thenReturn(const DiaryState.error(errorMessage: 'err')); + when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); + when(diaryCubit.close()).thenAnswer((_) async => Future.value()); + when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + + final commentsCubit = MockConsultantCommentsCubit(); + when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + + sl.registerFactory(() => diaryCubit); + sl.registerFactory(() => commentsCubit); + + await tester.pumpWidget(wrap(const DiaryScreen())); + await tester.pump(); + + expect(find.byType(DiaryScreen), findsOneWidget); + expect(find.byType(DataErrorWidget), findsOneWidget); + }); + + testWidgets('renders tomorrow-with-diet state', (tester) async { + final diaryCubit = MockDiaryCubit(); + const futureDiary = DiaryFutureDietDayEntity( + date: '2025-01-02', + typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null), + meals: [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])], + diet: null, + dayOfChosenDiet: 1, + ); + when(diaryCubit.state).thenReturn(DiaryState.loadedTomorrowWithDiet(diary: futureDiary, selectedDay: DateTime(2025, 1, 2), selectedMeal: 1)); + when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); + when(diaryCubit.close()).thenAnswer((_) async => Future.value()); + when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + + final commentsCubit = MockConsultantCommentsCubit(); + when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + + sl.registerFactory(() => diaryCubit); + sl.registerFactory(() => commentsCubit); + + await tester.pumpWidget(wrap(const DiaryScreen())); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.byType(DiaryScreen), findsOneWidget); + expect(find.byType(MealWidget), findsOneWidget); + }); + + testWidgets('renders consultant view blocks', (tester) async { + final diaryCubit = MockDiaryCubit(); + final state = (loadedState(hasChosenPlan: true)).copyWith( + isConsultantView: true, + isDiaryFinished: true, + consultantReview: ConsultantReviewModel( + planName: 'Plan', + planDuration: 30, + currentPlanDay: 1, + consultantComments: ConsultantComments(dayComment: 'Nice', mealsComments: const [], portionsComments: const []), + ), + ); + when(diaryCubit.state).thenReturn(state); + when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); + when(diaryCubit.close()).thenAnswer((_) async => Future.value()); + when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + + final commentsCubit = MockConsultantCommentsCubit(); + when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: false)); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.submitComments(any)).thenAnswer((_) async => true); + + sl.registerFactory(() => diaryCubit); + sl.registerFactory(() => commentsCubit); + + await tester.pumpWidget(wrap(const DiaryScreen())); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.text('Nice'), findsOneWidget); + expect(find.text(S.current.send), findsOneWidget); + }); + + testWidgets('renders user diary screen app bar title', (tester) async { + final diaryCubit = MockDiaryCubit(); + final state = loadedState(hasChosenPlan: false); + when(diaryCubit.state).thenReturn(state); + when(diaryCubit.stream).thenAnswer((_) => const Stream.empty()); + when(diaryCubit.close()).thenAnswer((_) async => Future.value()); + when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true); + + final commentsCubit = MockConsultantCommentsCubit(); + when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + + sl.registerFactory(() => diaryCubit); + sl.registerFactory(() => commentsCubit); + + await tester.pumpWidget(wrap(const UserDiaryScreen(diaryId: 1, currentPlanDay: 1))); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.byType(AppBar), findsWidgets); + + expect(find.byType(Text), findsWidgets); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/add_product_modal_smoke_test.dart b/test/features/diary/presentation/widgets/add_product_modal_smoke_test.dart new file mode 100644 index 00000000..75ba5a0b --- /dev/null +++ b/test/features/diary/presentation/widgets/add_product_modal_smoke_test.dart @@ -0,0 +1,107 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/add_product_modal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit(super.initialState); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('AddProductModal smoke', () { + DiaryState makeLoadedState({required bool withPortion}) { + const nutrition = NutritionDataModel( + dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + dailyNutritionRecommendation: NutritionModel(energy: 1, proteins: 1, fats: 1, carbohydrates: 1), + ); + + final diary = DiaryDayEntity( + id: 1, + date: '2025-01-01', + wakeup: '06:30:00', + retire: '22:30:00', + closed: 0, + comment: '', + typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: false, diaryState: null), + meals: [ + MealModel( + id: 1, + time: '08:00', + typeNumber: 1, + satiety: 0, + mood: 0, + portions: withPortion + ? [ + const PortionModel( + id: 10, + productNutritionalValueId: null, + productCaloriesInfo: null, + productName: 'Apple', + quantity: 100, + portion: 'gr', + fromDiet: false, + portionState: PortionState.eaten, + ), + ] + : const [], + photos: const [], + ), + ], + chosenPlanId: null, + dayOfChosenDiet: null, + diet: null, + ); + + return DiaryState.loaded(diary: diary, nutrition: nutrition, selectedDay: DateTime(2025, 1, 1), selectedMeal: 1); + } + + Widget wrap(Widget child, DiaryCubit cubit) { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: BlocProvider.value( + value: cubit, + child: Scaffold(body: child), + ), + ); + } + + testWidgets('renders for adding a product (portion=null)', (tester) async { + final state = makeLoadedState(withPortion: false); + final cubit = FakeDiaryCubit(state); + + await tester.pumpWidget(wrap(AddProductModal(state: state as DiaryStateLoaded, diaryCubit: cubit), cubit)); + await tester.pump(); + + expect(find.text('Добавление блюда'), findsOneWidget); + expect(find.text('Добавить'), findsOneWidget); + + await cubit.close(); + }); + + testWidgets('renders for editing a product (portion!=null)', (tester) async { + final state = makeLoadedState(withPortion: true); + final cubit = FakeDiaryCubit(state); + + final loaded = state as DiaryStateLoaded; + final portion = loaded.diary.meals.first.portions.first; + + await tester.pumpWidget(wrap(AddProductModal(state: loaded, diaryCubit: cubit, portion: portion), cubit)); + await tester.pump(); + + expect(find.text('Добавление блюда'), findsOneWidget); + expect(find.text('Сохранить'), findsOneWidget); + expect(find.byIcon(Icons.delete_outline_rounded), findsOneWidget); + + await cubit.close(); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/calendar_widget_smoke_test.dart b/test/features/diary/presentation/widgets/calendar_widget_smoke_test.dart new file mode 100644 index 00000000..1cb59eb0 --- /dev/null +++ b/test/features/diary/presentation/widgets/calendar_widget_smoke_test.dart @@ -0,0 +1,45 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/calendar_widget.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'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit(super.initialState); + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('CalendarWidget smoke', () { + Widget wrap(Widget child, DiaryCubit cubit) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: BlocProvider.value( + value: cubit, + child: Scaffold(body: child), + ), + ); + } + + testWidgets('renders', (tester) async { + final cubit = FakeDiaryCubit(const DiaryState.loading()); + await tester.pumpWidget(wrap(const CalendarWidget(), cubit)); + await tester.pump(); + // Just make sure it builds. + expect(find.byType(CalendarWidget), findsOneWidget); + await cubit.close(); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/calories_chart_widget_smoke_test.dart b/test/features/diary/presentation/widgets/calories_chart_widget_smoke_test.dart new file mode 100644 index 00000000..93c430b0 --- /dev/null +++ b/test/features/diary/presentation/widgets/calories_chart_widget_smoke_test.dart @@ -0,0 +1,69 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/calories_chart_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit(super.initialState); + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('CaloriesChartWidget smoke', () { + Widget wrap(Widget child, DiaryCubit cubit) { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: BlocProvider.value( + value: cubit, + child: Scaffold(body: child), + ), + ); + } + + testWidgets('renders chart and opens help dialog', (tester) async { + final diary = DiaryDayEntity( + id: 1, + date: '2025-01-01', + wakeup: '06:30:00', + retire: '22:30:00', + closed: 0, + comment: '', + typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: false, diaryState: null), + meals: const [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])], + chosenPlanId: null, + dayOfChosenDiet: null, + diet: null, + ); + + const nutrition = NutritionDataModel( + dailyNutritionIntake: NutritionModel(energy: 1200, proteins: 50, fats: 30, carbohydrates: 150), + dailyNutritionRecommendation: NutritionModel(energy: 2000, proteins: 100, fats: 70, carbohydrates: 250), + ); + + final state = DiaryState.loaded(diary: diary, nutrition: nutrition, selectedDay: DateTime(2025, 1, 1), selectedMeal: 1); + final cubit = FakeDiaryCubit(state); + + await tester.pumpWidget(wrap(const CaloriesChartWidget(), cubit)); + + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + expect(find.text('Калории'), findsWidgets); + expect(find.text('Белки'), findsWidgets); + expect(find.text('Жиры'), findsWidgets); + expect(find.text('Углеводы'), findsWidgets); + + await tester.tap(find.byIcon(Icons.help_outline_rounded)); + await tester.pumpAndSettle(); + expect(find.byType(AlertDialog), findsOneWidget); + + await cubit.close(); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/comment_widgets_smoke_test.dart b/test/features/diary/presentation/widgets/comment_widgets_smoke_test.dart new file mode 100644 index 00000000..f24627c6 --- /dev/null +++ b/test/features/diary/presentation/widgets/comment_widgets_smoke_test.dart @@ -0,0 +1,65 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/comment_for_consultant_widget.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/day_comment_widget.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'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit() : super(const DiaryState.loading()); + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class FakeConsultantCommentsCubit extends Cubit implements ConsultantCommentsCubit { + FakeConsultantCommentsCubit(super.initialState); + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('Diary comment widgets smoke', () { + Widget wrap(Widget child, {DiaryCubit? diaryCubit, ConsultantCommentsCubit? commentsCubit}) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: MultiBlocProvider( + providers: [ + if (diaryCubit != null) BlocProvider.value(value: diaryCubit), + if (commentsCubit != null) BlocProvider.value(value: commentsCubit), + ], + child: Scaffold(body: child), + ), + ); + } + + testWidgets('CommentForConsultantWidget renders and accepts initial comment', (tester) async { + final cubit = FakeDiaryCubit(); + await tester.pumpWidget(wrap(const CommentForConsultantWidget(comment: 'Hi'), diaryCubit: cubit)); + await tester.pump(); + expect(find.text('Дополнительная информация для консультанта'), findsOneWidget); + expect(find.text('Hi'), findsOneWidget); + }); + + testWidgets('DayCommentWidget shows validation error state', (tester) async { + final cubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: true)); + await tester.pumpWidget(wrap(const DayCommentWidget(), commentsCubit: cubit)); + await tester.pump(); + expect(find.byType(DayCommentWidget), findsOneWidget); + expect(find.text('Минимум 3 символа'), findsOneWidget); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/diary_chosen_widgets_smoke_test.dart b/test/features/diary/presentation/widgets/diary_chosen_widgets_smoke_test.dart new file mode 100644 index 00000000..dd7bc0f9 --- /dev/null +++ b/test/features/diary/presentation/widgets/diary_chosen_widgets_smoke_test.dart @@ -0,0 +1,112 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/add_product_button_widget.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/chosen_diet_widget.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/chosen_plan_widget.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'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit(super.initialState); + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('Diary chosen widgets smoke', () { + Widget wrap(Widget child, DiaryCubit cubit) { + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (_, _) => BlocProvider.value( + value: cubit, + child: Scaffold(body: child), + ), + ), + GoRoute(path: '/about-diet', builder: (_, _) => const SizedBox.shrink()), + ], + ); + return MaterialApp.router( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + routerConfig: router, + ); + } + + DiaryStateLoaded loadedState({required bool hasDiet, required bool hasPlan}) { + final diary = DiaryDayEntity( + id: 1, + date: '2025-01-01', + wakeup: '07:00', + retire: '23:00', + closed: 0, + comment: '', + typeOfDiary: TypeOfDiaryModel(hasChosenDiet: hasDiet, hasChosenPlan: hasPlan, diaryState: DiaryStateEnum.started), + meals: const [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])], + chosenPlanId: hasPlan ? 1 : null, + dayOfChosenDiet: hasDiet ? 2 : null, + diet: hasDiet ? const DiaryDietModel(id: 10, duration: 7, name: 'Diet') : null, + ); + const nutrition = NutritionDataModel( + dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + ); + return DiaryState.loaded( + diary: diary, + nutrition: nutrition, + selectedDay: DateTime(2025, 1, 1), + selectedMeal: 1, + chosenPlanCurrentDay: hasPlan ? 3 : null, + chosenPlanName: hasPlan ? 'Plan' : null, + chosenPlanDuration: hasPlan ? 30 : null, + ) + as DiaryStateLoaded; + } + + testWidgets('ChosenPlanWidget renders when plan selected', (tester) async { + final cubit = FakeDiaryCubit(loadedState(hasDiet: false, hasPlan: true)); + await tester.pumpWidget(wrap(const ChosenPlanWidget(), cubit)); + await tester.pump(); + expect(find.text('Plan'), findsOneWidget); + expect(find.textContaining('из 30'), findsOneWidget); + }); + + testWidgets('ChosenDietWidget renders and is tappable in loadedTomorrowWithDiet', (tester) async { + const futureDiary = DiaryFutureDietDayEntity( + date: '2025-01-02', + typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null), + meals: [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])], + diet: DiaryDietModel(id: 10, duration: 7, name: 'Diet'), + dayOfChosenDiet: 2, + ); + final cubit = FakeDiaryCubit(DiaryState.loadedTomorrowWithDiet(diary: futureDiary, selectedDay: DateTime(2025, 1, 2), selectedMeal: 1)); + await tester.pumpWidget(wrap(const ChosenDietWidget(), cubit)); + await tester.pump(); + expect(find.text('Diet'), findsOneWidget); + }); + + testWidgets('AddProductButtonWidget renders for loaded state', (tester) async { + final cubit = FakeDiaryCubit(loadedState(hasDiet: false, hasPlan: false)); + await tester.pumpWidget(wrap(const AddProductButtonWidget(), cubit)); + await tester.pump(); + expect(find.text('Добавить'), findsOneWidget); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/diary_ratings_and_time_widgets_smoke_test.dart b/test/features/diary/presentation/widgets/diary_ratings_and_time_widgets_smoke_test.dart new file mode 100644 index 00000000..027e6026 --- /dev/null +++ b/test/features/diary/presentation/widgets/diary_ratings_and_time_widgets_smoke_test.dart @@ -0,0 +1,224 @@ + +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart'; +import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/meal_comment_widget.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/mood_rating_widget.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/satiety_rating_widget.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/time_of_meal_picker_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit(super.initialState); + + int? fillTimeDiaryId; + String? fillTimeValue; + int? fillMoodValue; + int? fillSatietyValue; + + @override + Future fillTime({required int diaryId, required String time}) async { + fillTimeDiaryId = diaryId; + fillTimeValue = time; + } + + @override + Future fillMood({required int diaryId, required int mood}) async { + fillMoodValue = mood; + } + + @override + Future fillSatiety({required int diaryId, required int satiety}) async { + fillSatietyValue = satiety; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class FakeConsultantCommentsCubit extends Cubit implements ConsultantCommentsCubit { + FakeConsultantCommentsCubit(super.initialState); + + int? lastMealNumber; + String? lastMealComment; + + @override + void setMealComment({required int mealNumber, required String comment}) { + lastMealNumber = mealNumber; + lastMealComment = comment; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeAssetBundle extends CachingAssetBundle { + _FakeAssetBundle(this.assets); + final Map assets; + + @override + Future loadString(String key, {bool cache = true}) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return value; + } + + @override + Future load(String key) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return ByteData.sublistView(Uint8List.fromList(value.codeUnits)); + } +} + +void main() { + group('Diary rating/time widgets smoke', () { + const fakeSvg = ''' + + + +'''; + + final bundle = _FakeAssetBundle({ + 'assets/icons/satiety_emoji_left.svg': fakeSvg, + 'assets/icons/satiety_emoji_right.svg': fakeSvg, + 'assets/icons/mood_1.svg': fakeSvg, + 'assets/icons/mood_2.svg': fakeSvg, + 'assets/icons/error_icon.svg': fakeSvg, + }); + + Widget wrap({required DiaryCubit diaryCubit, required ConsultantCommentsCubit commentsCubit, required Widget child}) { + return DefaultAssetBundle( + bundle: bundle, + child: MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: MediaQuery( + data: const MediaQueryData(size: Size(500, 900)), + child: MultiBlocProvider( + providers: [ + BlocProvider.value(value: diaryCubit), + BlocProvider.value(value: commentsCubit), + ], + child: Scaffold(body: child), + ), + ), + ), + ); + } + + DiaryStateLoaded loadedState({ + required int satiety, + required int mood, + bool consultantView = false, + bool diaryFinished = false, + bool diaryCommented = false, + }) { + final diary = DiaryDayEntity( + id: 1, + date: '2025-01-01', + wakeup: '07:00', + retire: '23:00', + closed: 0, + comment: '', + typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: true, diaryState: DiaryStateEnum.started), + meals: [MealModel(id: 1, time: '08:00:00', typeNumber: 1, satiety: satiety, mood: mood, portions: const [], photos: const [])], + chosenPlanId: 1, + dayOfChosenDiet: null, + diet: null, + ); + const nutrition = NutritionDataModel( + dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + ); + return DiaryState.loaded( + diary: diary, + nutrition: nutrition, + selectedDay: DateTime(2025, 1, 1), + selectedMeal: 1, + isConsultantView: consultantView, + isDiaryFinished: diaryFinished, + isDiaryCommented: diaryCommented, + consultantReview: ConsultantReviewModel( + planName: 'Plan', + planDuration: 10, + currentPlanDay: 1, + consultantComments: ConsultantComments(dayComment: null, mealsComments: const [], portionsComments: const []), + ), + ) + as DiaryStateLoaded; + } + + testWidgets('TimeOfMealPickerWidget renders time and opens picker', (tester) async { + final diaryCubit = FakeDiaryCubit(loadedState(satiety: 0, mood: 0)); + final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + + await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const TimeOfMealPickerWidget())); + await tester.pump(); + + expect(find.text('Время приема пищи'), findsOneWidget); + expect(find.text('08:00'), findsOneWidget); + + await tester.tap(find.text('08:00')); + await tester.pumpAndSettle(); + + expect(find.text('Сохранить'), findsOneWidget); + }); + + testWidgets('SatietyRatingWidget renders and is disabled for consultant', (tester) async { + final diaryCubit = FakeDiaryCubit(loadedState(satiety: 2, mood: 0, consultantView: true)); + final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + + await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const SatietyRatingWidget())); + await tester.pump(); + + expect(find.text('Сытость пользователя после приема пищи'), findsOneWidget); + }); + + testWidgets('MoodRatingWidget renders', (tester) async { + final diaryCubit = FakeDiaryCubit(loadedState(satiety: 0, mood: 3)); + final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + + await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const MoodRatingWidget())); + await tester.pump(); + + expect(find.text('Ваше настроение после приёма пищи'), findsOneWidget); + }); + + testWidgets('MealCommentWidget shows validation error when comment too short', (tester) async { + final diaryCubit = FakeDiaryCubit(loadedState(satiety: 0, mood: 0)); + final commentsCubit = FakeConsultantCommentsCubit( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel( + comment: '', + mealsComments: {'1': MealCommentModel(comment: 'a', typeNumber: 1)}, + ), + ), + ); + + await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const MealCommentWidget(selectedMeal: 1))); + await tester.pump(); + + expect(find.text('Минимум 3 символа'), findsOneWidget); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/diary_time_and_rating_interactions_test.dart b/test/features/diary/presentation/widgets/diary_time_and_rating_interactions_test.dart new file mode 100644 index 00000000..0060ecb5 --- /dev/null +++ b/test/features/diary/presentation/widgets/diary_time_and_rating_interactions_test.dart @@ -0,0 +1,207 @@ + +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart'; +import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/mood_rating_widget.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/satiety_rating_widget.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/time_of_meal_picker_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit(super.initialState); + + int? lastDiaryId; + String? lastTime; + int? lastSatiety; + int? lastMood; + + @override + Future fillTime({required int diaryId, required String time}) async { + lastDiaryId = diaryId; + lastTime = time; + } + + @override + Future fillSatiety({required int diaryId, required int satiety}) async { + lastDiaryId = diaryId; + lastSatiety = satiety; + } + + @override + Future fillMood({required int diaryId, required int mood}) async { + lastDiaryId = diaryId; + lastMood = mood; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class FakeConsultantCommentsCubit extends Cubit implements ConsultantCommentsCubit { + FakeConsultantCommentsCubit(super.initialState); + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeAssetBundle extends CachingAssetBundle { + _FakeAssetBundle(this.assets); + final Map assets; + + @override + Future loadString(String key, {bool cache = true}) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return value; + } + + @override + Future load(String key) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return ByteData.sublistView(Uint8List.fromList(value.codeUnits)); + } +} + +void main() { + group('Diary time & rating interactions', () { + const fakeSvg = ''' + + + +'''; + + final bundle = _FakeAssetBundle({ + 'assets/icons/satiety_emoji_left.svg': fakeSvg, + 'assets/icons/satiety_emoji_right.svg': fakeSvg, + 'assets/icons/mood_1.svg': fakeSvg, + 'assets/icons/mood_2.svg': fakeSvg, + }); + + Widget wrap({required DiaryCubit diaryCubit, required ConsultantCommentsCubit commentsCubit, required Widget child}) { + return DefaultAssetBundle( + bundle: bundle, + child: MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: MediaQuery( + data: const MediaQueryData(size: Size(500, 900)), + child: MultiBlocProvider( + providers: [ + BlocProvider.value(value: diaryCubit), + BlocProvider.value(value: commentsCubit), + ], + child: Scaffold(body: child), + ), + ), + ), + ); + } + + DiaryStateLoaded loadedState({required int satiety, required int mood}) { + final diary = DiaryDayEntity( + id: 1, + date: '2025-01-01', + wakeup: '07:00', + retire: '23:00', + closed: 0, + comment: '', + typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: true, diaryState: DiaryStateEnum.started), + meals: [MealModel(id: 1, time: '08:00:00', typeNumber: 1, satiety: satiety, mood: mood, portions: const [], photos: const [])], + chosenPlanId: 1, + dayOfChosenDiet: null, + diet: null, + ); + const nutrition = NutritionDataModel( + dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + ); + return DiaryState.loaded( + diary: diary, + nutrition: nutrition, + selectedDay: DateTime(2025, 1, 1), + selectedMeal: 1, + isConsultantView: false, + isDiaryFinished: false, + isDiaryCommented: false, + consultantReview: ConsultantReviewModel( + planName: 'Plan', + planDuration: 10, + currentPlanDay: 1, + consultantComments: ConsultantComments(dayComment: null, mealsComments: const [], portionsComments: const []), + ), + ) + as DiaryStateLoaded; + } + + testWidgets('TimeOfMealPickerWidget calls fillTime on save', (tester) async { + final diaryCubit = FakeDiaryCubit(loadedState(satiety: 0, mood: 0)); + final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + + await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const TimeOfMealPickerWidget())); + await tester.pump(); + + // Open picker. + await tester.tap(find.text('08:00')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Сохранить')); + await tester.pump(); + + expect(diaryCubit.lastDiaryId, 1); + expect(diaryCubit.lastTime, isNotNull); + }); + + testWidgets('SatietyRatingWidget calls fillSatiety after rating change', (tester) async { + final diaryCubit = FakeDiaryCubit(loadedState(satiety: 0, mood: 0)); + final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + + await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const SatietyRatingWidget())); + await tester.pump(); + + final bar = find.byType(RatingBar); + expect(bar, findsOneWidget); + final box = tester.renderObject(bar); + + await tester.tapAt(box.localToGlobal(box.size.center(Offset.zero))); + await tester.pump(const Duration(milliseconds: 300)); // allow EasyDebounce to fire + + expect(diaryCubit.lastSatiety, isNotNull); + }); + + testWidgets('MoodRatingWidget calls fillMood after rating change', (tester) async { + final diaryCubit = FakeDiaryCubit(loadedState(satiety: 0, mood: 0)); + final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + + await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const MoodRatingWidget())); + await tester.pump(); + + final bar = find.byType(RatingBar); + expect(bar, findsOneWidget); + final box = tester.renderObject(bar); + await tester.tapAt(box.localToGlobal(box.size.center(Offset.zero))); + await tester.pump(const Duration(milliseconds: 300)); + + expect(diaryCubit.lastMood, isNotNull); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/glossary_moderation_request_modal_widget_test.dart b/test/features/diary/presentation/widgets/glossary_moderation_request_modal_widget_test.dart new file mode 100644 index 00000000..f77710e0 --- /dev/null +++ b/test/features/diary/presentation/widgets/glossary_moderation_request_modal_widget_test.dart @@ -0,0 +1,82 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart'; +import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'glossary_moderation_request_modal_widget_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('GlossaryModerationRequestModalWidget', () { + Widget wrap(Widget child) => MaterialApp( + theme: ThemeHelper().themeData(), + home: Scaffold(body: child), + ); + + testWidgets('renders alert state and switches to addNewProduct', (tester) async { + final cubit = MockDiaryCubit(); + + await tester.pumpWidget(wrap(GlossaryModerationRequestModalWidget(productName: 'Apple', diaryCubit: cubit))); + await tester.pump(); + + expect(find.text('Добавление нового продукта'), findsOneWidget); + expect(find.text('Записать'), findsOneWidget); + + await tester.tap(find.text('Записать')); + await tester.pump(); + + + expect(find.text('Сохранить'), findsOneWidget); + }); + + testWidgets('alert -> refuse triggers sendProductToModeration and success', (tester) async { + final cubit = MockDiaryCubit(); + when( + cubit.sendProductToModeration( + name: anyNamed('name'), + energy: anyNamed('energy'), + proteins: anyNamed('proteins'), + fats: anyNamed('fats'), + carbohydrates: anyNamed('carbohydrates'), + ), + ).thenAnswer( + (_) async => const GlossaryModerationModel( + glossary: GlossaryModel( + id: 1, + name: 'Apple', + grInPortion: 0, + energyOn100Gr: 52, + proteinsOn100Gr: 0, + fatsOn100Gr: 0, + carbohydratesOn100Gr: 14, + energyOnPortion: 0, + proteinsOnPortion: 0, + fatsOnPortion: 0, + carbohydratesOnPortion: 0, + ), + ), + ); + + await tester.pumpWidget(wrap(GlossaryModerationRequestModalWidget(productName: 'Apple', diaryCubit: cubit))); + await tester.pump(); + + await tester.tap(find.text('Отказаться')); + await tester.pump(); + + expect(find.textContaining('Продукт успешно отправлен'), findsOneWidget); + verify( + cubit.sendProductToModeration( + name: anyNamed('name'), + energy: anyNamed('energy'), + proteins: anyNamed('proteins'), + fats: anyNamed('fats'), + carbohydrates: anyNamed('carbohydrates'), + ), + ).called(1); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/meal_button_widget_test.dart b/test/features/diary/presentation/widgets/meal_button_widget_test.dart new file mode 100644 index 00000000..87fa5d9e --- /dev/null +++ b/test/features/diary/presentation/widgets/meal_button_widget_test.dart @@ -0,0 +1,216 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart'; +import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/meal_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit(super.initialState); + + int? lastSelectedMeal; + + @override + void setSelectedMeal(int meal) { + lastSelectedMeal = meal; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('MealButtonWidget', () { + Widget wrap(DiaryCubit cubit, Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: MediaQuery( + data: const MediaQueryData(size: Size(500, 900)), + child: BlocProvider.value( + value: cubit, + child: Scaffold(body: child), + ), + ), + ); + } + + DiaryStateLoaded loadedState({required int selectedMeal, required List> mealsPortions, bool withConsultantComment = false}) { + const caloriesInfo = ProductCaloriesInfoModel( + id: 1, + productName: 'P', + calories: 100, + proteins: 10, + fats: 10, + carbohydrates: 10, + energyOnPortion: 100, + proteinsOnPortion: 10, + fatsOnPortion: 10, + carbohydratesOnPortion: 10, + ); + + final meals = List.generate( + 5, + (i) => MealModel( + id: i + 1, + time: '08:00', + typeNumber: i + 1, + satiety: 0, + mood: 0, + portions: mealsPortions.length > i + ? mealsPortions[i] + : const [ + PortionModel( + id: 1, + productNutritionalValueId: 1, + productCaloriesInfo: caloriesInfo, + productName: 'P', + quantity: 100, + portion: 'gr', + fromDiet: false, + portionState: PortionState.eaten, + ), + ], + photos: const [], + ), + ); + + final diary = DiaryDayEntity( + id: 1, + date: '2025-01-01', + wakeup: '07:00', + retire: '23:00', + closed: 0, + comment: '', + typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: true, diaryState: DiaryStateEnum.started), + meals: meals, + chosenPlanId: 1, + dayOfChosenDiet: null, + diet: null, + ); + + const nutrition = NutritionDataModel( + dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + ); + + return DiaryState.loaded( + diary: diary, + nutrition: nutrition, + selectedDay: DateTime(2025, 1, 1), + selectedMeal: selectedMeal, + isConsultantView: false, + isDiaryFinished: false, + isDiaryCommented: false, + consultantReview: ConsultantReviewModel( + planName: 'Plan', + planDuration: 10, + currentPlanDay: 1, + consultantComments: ConsultantComments( + dayComment: null, + mealsComments: withConsultantComment ? [MealComment(comment: 'c', typeNo: 2)] : const [], + portionsComments: const [], + ), + ), + ) + as DiaryStateLoaded; + } + + testWidgets('shows calories for loaded state and calls setSelectedMeal on tap', (tester) async { + const caloriesInfo = ProductCaloriesInfoModel( + id: 1, + productName: 'P', + calories: 100, + proteins: 10, + fats: 10, + carbohydrates: 10, + energyOnPortion: 100, + proteinsOnPortion: 10, + fatsOnPortion: 10, + carbohydratesOnPortion: 10, + ); + const portion = PortionModel( + id: 1, + productNutritionalValueId: 1, + productCaloriesInfo: caloriesInfo, + productName: 'P', + quantity: 100, + portion: 'gr', + fromDiet: false, + portionState: PortionState.eaten, + ); + + final cubit = FakeDiaryCubit( + loadedState( + selectedMeal: 1, + mealsPortions: const [ + [portion], + [], + [], + [], + [], + ], + ), + ); + await tester.pumpWidget(wrap(cubit, const MealButtonWidget(mealNumber: 1))); + await tester.pump(); + + expect(find.text('100 ккал'), findsOneWidget); + await tester.tap(find.text('1')); + await tester.pump(); + expect(cubit.lastSelectedMeal, 1); + }); + + testWidgets('shows dash calories when meal empty', (tester) async { + final cubit = FakeDiaryCubit(loadedState(selectedMeal: 1, mealsPortions: const [[], [], [], [], []])); + await tester.pumpWidget(wrap(cubit, const MealButtonWidget(mealNumber: 3))); + await tester.pump(); + expect(find.text('— ккал'), findsOneWidget); + }); + + testWidgets('works for tomorrow-with-diet state', (tester) async { + const caloriesInfo = ProductCaloriesInfoModel( + id: 1, + productName: 'P', + calories: 100, + proteins: 10, + fats: 10, + carbohydrates: 10, + energyOnPortion: 100, + proteinsOnPortion: 10, + fatsOnPortion: 10, + carbohydratesOnPortion: 10, + ); + const portion = PortionModel( + id: 1, + productNutritionalValueId: 1, + productCaloriesInfo: caloriesInfo, + productName: 'P', + quantity: 100, + portion: 'gr', + fromDiet: true, + portionState: PortionState.eatenFromDiet, + ); + const diary = DiaryFutureDietDayEntity( + date: '2025-01-02', + typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null), + meals: [ + MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [portion], photos: []), + ], + diet: null, + dayOfChosenDiet: 1, + ); + final cubit = FakeDiaryCubit(DiaryState.loadedTomorrowWithDiet(diary: diary, selectedDay: DateTime(2025, 1, 2), selectedMeal: 1)); + + await tester.pumpWidget(wrap(cubit, const MealButtonWidget(mealNumber: 1))); + await tester.pump(); + expect(find.text('100 ккал'), findsOneWidget); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/meal_comment_widget_test.dart b/test/features/diary/presentation/widgets/meal_comment_widget_test.dart new file mode 100644 index 00000000..eed20765 --- /dev/null +++ b/test/features/diary/presentation/widgets/meal_comment_widget_test.dart @@ -0,0 +1,107 @@ + +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/meal_comment_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeConsultantCommentsCubit extends Cubit implements ConsultantCommentsCubit { + FakeConsultantCommentsCubit(super.initialState); + + int? lastMealNumber; + String? lastMealComment; + + @override + void setMealComment({required int mealNumber, required String comment}) { + lastMealNumber = mealNumber; + lastMealComment = comment; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeAssetBundle extends CachingAssetBundle { + _FakeAssetBundle(this.assets); + final Map assets; + + @override + Future loadString(String key, {bool cache = true}) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return value; + } + + @override + Future load(String key) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return ByteData.sublistView(Uint8List.fromList(value.codeUnits)); + } +} + +void main() { + group('MealCommentWidget', () { + const fakeSvg = ''' + + + +'''; + + final bundle = _FakeAssetBundle({'assets/icons/error_icon.svg': fakeSvg}); + + Widget wrap(ConsultantCommentsCubit cubit, Widget child) { + return DefaultAssetBundle( + bundle: bundle, + child: MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: BlocProvider.value( + value: cubit, + child: Scaffold(body: child), + ), + ), + ); + } + + testWidgets('shows validation error when comment too short', (tester) async { + final cubit = FakeConsultantCommentsCubit( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel( + comment: '', + mealsComments: {'1': MealCommentModel(comment: 'a', typeNumber: 1)}, + ), + ), + ); + await tester.pumpWidget(wrap(cubit, const MealCommentWidget(selectedMeal: 1))); + await tester.pump(); + expect(find.text('Минимум 3 символа'), findsOneWidget); + }); + + testWidgets('renders and shows comment title', (tester) async { + final cubit = FakeConsultantCommentsCubit( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel( + comment: '', + mealsComments: {'1': MealCommentModel(comment: 'ok', typeNumber: 1)}, + ), + ), + ); + await tester.pumpWidget(wrap(cubit, const MealCommentWidget(selectedMeal: 1))); + await tester.pump(); + expect(find.textContaining(S.current.commentForMeal), findsOneWidget); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/meal_widget_smoke_test.dart b/test/features/diary/presentation/widgets/meal_widget_smoke_test.dart new file mode 100644 index 00000000..4ec58013 --- /dev/null +++ b/test/features/diary/presentation/widgets/meal_widget_smoke_test.dart @@ -0,0 +1,173 @@ + +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart'; +import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/meal_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit(super.initialState); + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class FakeConsultantCommentsCubit extends Cubit implements ConsultantCommentsCubit { + FakeConsultantCommentsCubit(super.initialState); + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeAssetBundle extends CachingAssetBundle { + _FakeAssetBundle(this.assets); + final Map assets; + + @override + Future loadString(String key, {bool cache = true}) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return value; + } + + @override + Future load(String key) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return ByteData.sublistView(Uint8List.fromList(value.codeUnits)); + } +} + +void main() { + group('MealWidget smoke', () { + const fakeSvg = ''' + + + +'''; + + final bundle = _FakeAssetBundle({ + 'assets/icons/satiety_emoji_left.svg': fakeSvg, + 'assets/icons/satiety_emoji_right.svg': fakeSvg, + 'assets/icons/mood_1.svg': fakeSvg, + 'assets/icons/mood_2.svg': fakeSvg, + 'assets/icons/error_icon.svg': fakeSvg, + 'assets/no_one_image_was_added.svg': fakeSvg, + }); + + Widget wrap({required DiaryCubit diaryCubit, required ConsultantCommentsCubit commentsCubit, required Widget child}) { + return DefaultAssetBundle( + bundle: bundle, + child: MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: MediaQuery( + data: const MediaQueryData(size: Size(500, 900)), + child: MultiBlocProvider( + providers: [ + BlocProvider.value(value: diaryCubit), + BlocProvider.value(value: commentsCubit), + ], + child: Scaffold(body: SingleChildScrollView(child: child)), + ), + ), + ), + ); + } + + DiaryStateLoaded loadedState({required bool consultantView, required bool showMealCommentFromReview}) { + const meals = [ + MealModel(id: 1, time: '08:00:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: []), + MealModel(id: 2, time: '09:00:00', typeNumber: 2, satiety: 0, mood: 0, portions: [], photos: []), + MealModel(id: 3, time: '10:00:00', typeNumber: 3, satiety: 0, mood: 0, portions: [], photos: []), + MealModel(id: 4, time: '11:00:00', typeNumber: 4, satiety: 0, mood: 0, portions: [], photos: []), + MealModel(id: 5, time: '12:00:00', typeNumber: 5, satiety: 0, mood: 0, portions: [], photos: []), + ]; + + final diary = DiaryDayEntity( + id: 1, + date: '2025-01-01', + wakeup: '07:00', + retire: '23:00', + closed: 0, + comment: '', + typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: false, diaryState: DiaryStateEnum.started), + meals: meals, + chosenPlanId: null, + dayOfChosenDiet: null, + diet: null, + ); + const nutrition = NutritionDataModel( + dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + ); + + final review = ConsultantReviewModel( + planName: 'Plan', + planDuration: 10, + currentPlanDay: 1, + consultantComments: ConsultantComments( + dayComment: null, + mealsComments: showMealCommentFromReview ? [MealComment(comment: 'Comment', typeNo: 1)] : const [], + portionsComments: const [], + ), + ); + + return DiaryState.loaded( + diary: diary, + nutrition: nutrition, + selectedDay: DateTime(2025, 1, 1), + selectedMeal: 1, + isConsultantView: consultantView, + isDiaryFinished: false, + isDiaryCommented: false, + consultantReview: review, + ) + as DiaryStateLoaded; + } + + testWidgets('renders base meal widget content', (tester) async { + final diaryCubit = FakeDiaryCubit(loadedState(consultantView: false, showMealCommentFromReview: false)); + final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + + await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const MealWidget(showMood: true, showAddButton: true))); + await tester.pump(); + + expect(find.text('Прием пищи'), findsOneWidget); + expect(find.byType(MealWidget), findsOneWidget); + }); + + testWidgets('renders consultant meal comment widgets', (tester) async { + final diaryCubit = FakeDiaryCubit(loadedState(consultantView: true, showMealCommentFromReview: true)); + final commentsCubit = FakeConsultantCommentsCubit( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel(comment: '', mealsComments: const {}), + ), + ); + + await tester.pumpWidget(wrap(diaryCubit: diaryCubit, commentsCubit: commentsCubit, child: const MealWidget(showMood: false, showAddButton: false))); + await tester.pump(); + + + expect(find.textContaining(S.current.mealComment), findsOneWidget); + expect(find.text('Comment'), findsOneWidget); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/photo_gallery_widget_smoke_test.dart b/test/features/diary/presentation/widgets/photo_gallery_widget_smoke_test.dart new file mode 100644 index 00000000..52ee9ca9 --- /dev/null +++ b/test/features/diary/presentation/widgets/photo_gallery_widget_smoke_test.dart @@ -0,0 +1,233 @@ +import 'dart:async'; +import 'dart:io'; + +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/photo_gallery_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit(super.initialState); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeHttpClient implements HttpClient { + bool _autoUncompress = true; + + @override + bool get autoUncompress => _autoUncompress; + + @override + set autoUncompress(bool value) { + _autoUncompress = value; + } + + @override + Future getUrl(Uri url) async => _FakeHttpClientRequest(); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeHttpClientRequest implements HttpClientRequest { + @override + Future close() async => _FakeHttpClientResponse(); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeHttpClientResponse extends Stream> implements HttpClientResponse { + // 1x1 transparent PNG. + static final List _png = [ + 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, + ]; + + @override + int get statusCode => 200; + + @override + int get contentLength => _png.length; + + @override + HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed; + + @override + StreamSubscription> listen(void Function(List)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) { + return Stream>.fromIterable([_png]).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('PhotoGalleryWidget smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(DiaryCubit cubit) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: BlocProvider.value( + value: cubit, + child: const Scaffold(body: PhotoGalleryWidget()), + ), + ); + } + + DiaryStateLoaded loadedState({required List photos, bool isConsultantView = false}) { + final diary = DiaryDayEntity( + id: 1, + date: '2025-01-01', + wakeup: '06:30:00', + retire: '22:30:00', + closed: 0, + comment: '', + typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: false, diaryState: null), + meals: [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: const [], photos: photos)], + chosenPlanId: null, + dayOfChosenDiet: null, + diet: null, + ); + + const nutrition = NutritionDataModel( + dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + dailyNutritionRecommendation: NutritionModel(energy: 1, proteins: 1, fats: 1, carbohydrates: 1), + ); + + return DiaryState.loaded( + diary: diary, + nutrition: nutrition, + selectedDay: DateTime(2025, 1, 1), + selectedMeal: 1, + isConsultantView: isConsultantView, + isDiaryFinished: false, + isDiaryCommented: false, + ) + as DiaryStateLoaded; + } + + testWidgets('renders empty placeholder when no photos', (tester) async { + final state = loadedState(photos: const []); + final cubit = FakeDiaryCubit(state); + + await tester.pumpWidget(wrap(cubit)); + await tester.pump(); + + expect(find.byType(PhotoGalleryWidget), findsOneWidget); + + expect(find.byType(SvgPicture), findsOneWidget); + + await cubit.close(); + }); + + testWidgets('renders gallery when photos exist', (tester) async { + final photos = [const DiaryPhotoModel(id: 1, originalName: 'a.png', path: 'chat_files/a.png', size: 1)]; + final state = loadedState(photos: photos); + final cubit = FakeDiaryCubit(state); + + await HttpOverrides.runZoned(() async { + await tester.pumpWidget(wrap(cubit)); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + expect(find.byType(PhotoGalleryWidget), findsOneWidget); + }, createHttpClient: (_) => _FakeHttpClient()); + + await cubit.close(); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/portion_comment_widget_test.dart b/test/features/diary/presentation/widgets/portion_comment_widget_test.dart new file mode 100644 index 00000000..9e21550b --- /dev/null +++ b/test/features/diary/presentation/widgets/portion_comment_widget_test.dart @@ -0,0 +1,112 @@ + +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/portion_comment_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeConsultantCommentsCubit extends Cubit implements ConsultantCommentsCubit { + FakeConsultantCommentsCubit(super.initialState); + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeAssetBundle extends CachingAssetBundle { + _FakeAssetBundle(this.assets); + final Map assets; + + @override + Future loadString(String key, {bool cache = true}) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return value; + } + + @override + Future load(String key) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return ByteData.sublistView(Uint8List.fromList(value.codeUnits)); + } +} + +void main() { + group('PortionCommentWidget', () { + const fakeSvg = ''' + + + +'''; + + final bundle = _FakeAssetBundle({'assets/icons/error_icon.svg': fakeSvg}); + + Widget wrap(ConsultantCommentsCubit cubit, Widget child) { + return DefaultAssetBundle( + bundle: bundle, + child: MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: BlocProvider.value( + value: cubit, + child: Scaffold(body: child), + ), + ), + ); + } + + testWidgets('shows CommentContainer when disabled and comment provided', (tester) async { + final cubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + await tester.pumpWidget(wrap(cubit, const PortionCommentWidget(enabled: false, portionId: 1, comment: 'ok'))); + await tester.pump(); + expect(find.text('ok'), findsOneWidget); + }); + + testWidgets('shows validation error when enabled and comment too short', (tester) async { + final cubit = FakeConsultantCommentsCubit( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel( + comment: '', + portions: [PortionCommentModel(id: 1, comment: 'a')], + ), + ), + ); + await tester.pumpWidget(wrap(cubit, const PortionCommentWidget(enabled: true, portionId: 1))); + await tester.pump(); + expect(find.text('Минимум 3 символа'), findsOneWidget); + }); + + testWidgets('updates controller text on portionId change', (tester) async { + final cubit = FakeConsultantCommentsCubit( + ConsultantCommentsState( + commentsModel: ConsultantCommentModel( + comment: '', + portions: [ + PortionCommentModel(id: 1, comment: 'first'), + PortionCommentModel(id: 2, comment: 'second'), + ], + ), + ), + ); + + await tester.pumpWidget(wrap(cubit, const PortionCommentWidget(enabled: true, portionId: 1))); + await tester.pump(); + expect(find.text('first'), findsOneWidget); + + await tester.pumpWidget(wrap(cubit, const PortionCommentWidget(enabled: true, portionId: 2))); + await tester.pump(); + expect(find.text('second'), findsOneWidget); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/product_widget_smoke_test.dart b/test/features/diary/presentation/widgets/product_widget_smoke_test.dart new file mode 100644 index 00000000..d80c99a1 --- /dev/null +++ b/test/features/diary/presentation/widgets/product_widget_smoke_test.dart @@ -0,0 +1,244 @@ + +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart'; +import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart'; +import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/comment_container.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/portion_comment_widget.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit(super.initialState); + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class FakeConsultantCommentsCubit extends Cubit implements ConsultantCommentsCubit { + FakeConsultantCommentsCubit(super.initialState); + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeAssetBundle extends CachingAssetBundle { + _FakeAssetBundle(this.assets); + final Map assets; + + @override + Future loadString(String key, {bool cache = true}) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return value; + } + + @override + Future load(String key) async { + final value = assets[key]; + if (value == null) throw FlutterError('Asset not found: $key'); + return ByteData.sublistView(Uint8List.fromList(value.codeUnits)); + } +} + +void main() { + group('ProductWidget smoke', () { + const fakeSvg = ''' + + + +'''; + + final bundle = _FakeAssetBundle({'assets/icons/revert_product.svg': fakeSvg, 'assets/icons/edit.svg': fakeSvg}); + + Widget wrap({required DiaryCubit diaryCubit, required ConsultantCommentsCubit consultantCommentsCubit, required Widget child}) { + return DefaultAssetBundle( + bundle: bundle, + child: MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: MultiBlocProvider( + providers: [ + BlocProvider.value(value: diaryCubit), + BlocProvider.value(value: consultantCommentsCubit), + ], + child: Scaffold(body: child), + ), + ), + ); + } + + DiaryStateLoaded loadedState({required PortionModel portion, required bool consultantView}) { + final diary = DiaryDayEntity( + id: 1, + date: '2025-01-01', + wakeup: '07:00', + retire: '23:00', + closed: 0, + comment: '', + typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: true, diaryState: DiaryStateEnum.started), + meals: [ + MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [portion], photos: const []), + ], + chosenPlanId: 1, + dayOfChosenDiet: null, + diet: null, + ); + + const nutrition = NutritionDataModel( + dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + dailyNutritionRecommendation: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + ); + + return DiaryState.loaded( + diary: diary, + nutrition: nutrition, + selectedDay: DateTime(2025, 1, 1), + selectedMeal: 1, + isConsultantView: consultantView, + isDiaryFinished: false, + isDiaryCommented: false, + consultantReview: ConsultantReviewModel( + planName: 'Plan', + planDuration: 10, + currentPlanDay: 1, + consultantComments: ConsultantComments(dayComment: null, mealsComments: const [], portionsComments: const []), + ), + ) + as DiaryStateLoaded; + } + + testWidgets('renders tomorrow-with-diet tile', (tester) async { + const portion = PortionModel( + id: 1, + productNutritionalValueId: null, + productCaloriesInfo: null, + productName: 'Apple', + quantity: 1, + portion: 'piece', + fromDiet: true, + portionState: PortionState.filledDiet, + ); + final diaryCubit = FakeDiaryCubit( + DiaryState.loadedTomorrowWithDiet( + diary: const DiaryFutureDietDayEntity( + date: '2025-01-02', + typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null), + meals: [ + MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [portion], photos: []), + ], + diet: null, + dayOfChosenDiet: 1, + ), + selectedDay: DateTime(2025, 1, 2), + selectedMeal: 1, + ), + ); + final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + + await tester.pumpWidget( + wrap( + diaryCubit: diaryCubit, + consultantCommentsCubit: commentsCubit, + child: const ProductWidget(portion: portion), + ), + ); + await tester.pump(); + + expect(find.text('Apple'), findsOneWidget); + expect(find.byType(ListTile), findsOneWidget); + }); + + testWidgets('renders eatenDeleted state with revert icon', (tester) async { + const portion = PortionModel( + id: 1, + productNutritionalValueId: null, + productCaloriesInfo: null, + productName: 'Deleted', + quantity: 100, + portion: 'gr', + fromDiet: false, + portionState: PortionState.eatenDeleted, + ); + final diaryCubit = FakeDiaryCubit(loadedState(portion: portion, consultantView: false)); + final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + + await tester.pumpWidget( + wrap( + diaryCubit: diaryCubit, + consultantCommentsCubit: commentsCubit, + child: const ProductWidget(portion: portion), + ), + ); + await tester.pump(); + + expect(find.text('Deleted'), findsOneWidget); + + expect(find.byType(ProductWidget), findsOneWidget); + }); + + testWidgets('consultant view toggles comment widget and shows existing comment container', (tester) async { + const portion = PortionModel( + id: 1, + productNutritionalValueId: null, + productCaloriesInfo: null, + productName: 'Portion', + quantity: 100, + portion: 'gr', + fromDiet: false, + portionState: PortionState.filledDiet, + ); + final state = loadedState(portion: portion, consultantView: true).copyWith( + consultantReview: ConsultantReviewModel( + planName: 'Plan', + planDuration: 10, + currentPlanDay: 1, + consultantComments: ConsultantComments( + dayComment: null, + mealsComments: const [], + portionsComments: [PortionComment(portionId: 1, comment: 'Looks good')], + ), + ), + ); + + final diaryCubit = FakeDiaryCubit(state); + final commentsCubit = FakeConsultantCommentsCubit(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''))); + + await tester.pumpWidget( + wrap( + diaryCubit: diaryCubit, + consultantCommentsCubit: commentsCubit, + child: const ProductWidget(portion: portion), + ), + ); + await tester.pump(); + + + expect(find.byType(CommentContainer), findsOneWidget); + expect(find.text('Looks good'), findsOneWidget); + + // Tap edit icon to enable PortionCommentWidget. + await tester.tap(find.byType(GestureDetector).first); + await tester.pump(); + + expect(find.byType(PortionCommentWidget), findsOneWidget); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/products_list_widget_smoke_test.dart b/test/features/diary/presentation/widgets/products_list_widget_smoke_test.dart new file mode 100644 index 00000000..ff25b5a8 --- /dev/null +++ b/test/features/diary/presentation/widgets/products_list_widget_smoke_test.dart @@ -0,0 +1,131 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/meal_summary_widget.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/products_list_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit(super.initialState); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('ProductsListWidget smoke', () { + Widget wrap(DiaryCubit cubit, Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: BlocProvider.value( + value: cubit, + child: Scaffold(body: child), + ), + ); + } + + testWidgets('renders tomorrow-with-diet list with summary', (tester) async { + const caloriesInfo = ProductCaloriesInfoModel( + id: 1, + productName: 'Apple', + calories: 52, + proteins: 0.3, + fats: 0.2, + carbohydrates: 14, + energyOnPortion: 52, + proteinsOnPortion: 0.3, + fatsOnPortion: 0.2, + carbohydratesOnPortion: 14, + ); + const portion = PortionModel( + id: 1, + productNutritionalValueId: 1, + productCaloriesInfo: caloriesInfo, + productName: 'Apple', + quantity: 100, + portion: 'gr', + fromDiet: true, + portionState: PortionState.eatenFromDiet, + ); + + const futureDiary = DiaryFutureDietDayEntity( + date: '2025-01-02', + typeOfDiary: TypeOfDiaryModel(hasChosenDiet: true, hasChosenPlan: false, diaryState: null), + meals: [ + MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [portion], photos: []), + ], + diet: null, + dayOfChosenDiet: 1, + ); + + final state = DiaryState.loadedTomorrowWithDiet(diary: futureDiary, selectedDay: DateTime(2025, 1, 2), selectedMeal: 1); + final cubit = FakeDiaryCubit(state); + + await tester.pumpWidget(wrap(cubit, const ProductsListWidget())); + await tester.pump(); + + expect(find.byType(ProductsListWidget), findsOneWidget); + expect(find.text('Apple'), findsOneWidget); + expect(find.byType(MealSummaryWidget), findsOneWidget); + + await cubit.close(); + }); + + test('MealSummaryWidget.calculateMealTotals ignores deleted and non-eaten diet items', () { + const caloriesInfo = ProductCaloriesInfoModel( + id: 1, + productName: 'P', + calories: 100, + proteins: 10, + fats: 10, + carbohydrates: 10, + energyOnPortion: 100, + proteinsOnPortion: 10, + fatsOnPortion: 10, + carbohydratesOnPortion: 10, + ); + const portions = [ + PortionModel( + id: 1, + productNutritionalValueId: 1, + productCaloriesInfo: caloriesInfo, + productName: 'A', + quantity: 100, + portion: 'gr', + fromDiet: true, + portionState: PortionState.eatenFromDiet, + ), + PortionModel( + id: 2, + productNutritionalValueId: 1, + productCaloriesInfo: caloriesInfo, + productName: 'B', + quantity: 100, + portion: 'gr', + fromDiet: true, + portionState: PortionState.filledDiet, // not eaten -> ignored + ), + PortionModel( + id: 3, + productNutritionalValueId: 1, + productCaloriesInfo: caloriesInfo, + productName: 'C', + quantity: 100, + portion: 'gr', + fromDiet: false, + portionState: PortionState.eatenDeleted, // deleted -> ignored + ), + ]; + + final totals = MealSummaryWidget.calculateMealTotals(portions); + expect(totals['calories']!.round(), 100); + expect(totals['proteins']!.round(), 10); + expect(totals['fats']!.round(), 10); + expect(totals['carbohydrates']!.round(), 10); + }); + }); +} diff --git a/test/features/diary/presentation/widgets/wakeup_and_retire_widget_test.dart b/test/features/diary/presentation/widgets/wakeup_and_retire_widget_test.dart new file mode 100644 index 00000000..c93f7ccd --- /dev/null +++ b/test/features/diary/presentation/widgets/wakeup_and_retire_widget_test.dart @@ -0,0 +1,72 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart'; +import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/wakeup_and_retire_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeDiaryCubit extends Cubit implements DiaryCubit { + FakeDiaryCubit(super.initialState); + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('WakeupAndRetireWidget', () { + Widget wrap(Widget child, DiaryCubit cubit) { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: BlocProvider.value( + value: cubit, + child: Scaffold(body: child), + ), + ); + } + + testWidgets('hidden when isEnabled=false', (tester) async { + final cubit = FakeDiaryCubit(const DiaryState.loading()); + await tester.pumpWidget(wrap(const WakeupAndRetireWidget(isEnabled: false), cubit)); + await tester.pump(); + expect(find.text('Подъём'), findsNothing); + await cubit.close(); + }); + + testWidgets('renders wakeup/retire when state loaded', (tester) async { + final diary = DiaryDayEntity( + id: 1, + date: '2025-01-01', + wakeup: '06:30:00', + retire: '22:30:00', + closed: 0, + comment: '', + typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: false, diaryState: null), + meals: const [MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: [])], + chosenPlanId: null, + dayOfChosenDiet: null, + diet: null, + ); + + const nutrition = NutritionDataModel( + dailyNutritionIntake: NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0), + dailyNutritionRecommendation: NutritionModel(energy: 1, proteins: 1, fats: 1, carbohydrates: 1), + ); + + final state = DiaryState.loaded(diary: diary, nutrition: nutrition, selectedDay: DateTime(2025, 1, 1), selectedMeal: 1); + final cubit = FakeDiaryCubit(state); + + await tester.pumpWidget(wrap(const WakeupAndRetireWidget(isEnabled: true), cubit)); + await tester.pump(); + + expect(find.text('Подъём'), findsOneWidget); + expect(find.text('Уход ко сну'), findsOneWidget); + expect(find.textContaining('06:30'), findsOneWidget); + expect(find.textContaining('22:30'), findsOneWidget); + + await cubit.close(); + }); + }); +} diff --git a/test/features/diets/presentation/pages/about_diet_screen_smoke_test.dart b/test/features/diets/presentation/pages/about_diet_screen_smoke_test.dart new file mode 100644 index 00000000..a09bfe1d --- /dev/null +++ b/test/features/diets/presentation/pages/about_diet_screen_smoke_test.dart @@ -0,0 +1,192 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart'; +import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_state.dart'; +import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +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/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +import 'about_diet_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec(), MockSpec()]) +void main() { + group('AboutDietScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + void registerMocks(AboutDietCubit dietCubit, CommentsCubit commentsCubit) { + sl.registerLazySingleton(() => dietCubit); + sl.registerLazySingleton(() => commentsCubit); + } + + const testDiet = Diet( + id: 1, + slug: 'test-diet', + name: 'Test Diet', + image: 'https://example.com/image.jpg', + duration: 7, + state: DietState.active, + type: DietType(veg: true, light: false, carb: false, meat: false), + details: 'Test details', + dailySchedule: {}, + author: Author( + authorSlug: 'author', + authorName: 'Author Name', + authorProfileIcon: 'https://example.com/icon.jpg', + authorIsMe: false, + isEntitySubscribedByMe: null, + authorPlaceInRating: null, + ), + totalLikes: 10, + totalViews: 100, + totalComments: 5, + wasLikedByMe: false, + isFavorite: false, + dailyNutrition: [], + ); + + Widget wrap(AboutDietCubit cubit, FeedCubit feedCubit, CommentsCubit commentsCubit, Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: MultiBlocProvider( + providers: [ + BlocProvider.value(value: cubit), + BlocProvider.value(value: feedCubit), + BlocProvider.value(value: commentsCubit), + ], + child: child, + ), + ); + } + + testWidgets('renders loading state', (tester) async { + final cubit = MockAboutDietCubit(); + final feedCubit = MockFeedCubit(); + final commentsCubit = MockCommentsCubit(); + when(cubit.state).thenReturn(const AboutDietState.loading()); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(cubit, commentsCubit); + + await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false))); + await tester.pump(); + + expect(find.byType(AboutDietScreen), findsOneWidget); + }); + + testWidgets('renders loaded state with diet not started', (tester) async { + final cubit = MockAboutDietCubit(); + final feedCubit = MockFeedCubit(); + final commentsCubit = MockCommentsCubit(); + const state = AboutDietState.loaded(diet: testDiet, isStarted: false, hasStartedDiet: false, selectedIndexes: []); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.viewDiet(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(cubit, commentsCubit); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false))); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 600)); + }); + + expect(find.byType(AboutDietScreen), findsOneWidget); + expect(find.text('Test Diet'), findsOneWidget); + expect(find.text(S.current.startDiet), findsOneWidget); + }); + + testWidgets('renders loaded state with diet started', (tester) async { + final cubit = MockAboutDietCubit(); + final feedCubit = MockFeedCubit(); + final commentsCubit = MockCommentsCubit(); + const state = AboutDietState.loaded(diet: testDiet, isStarted: true, hasStartedDiet: true, selectedIndexes: []); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.viewDiet(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(cubit, commentsCubit); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false))); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 600)); + }); + + expect(find.byType(AboutDietScreen), findsOneWidget); + expect(find.text('Отменить диету'), findsOneWidget); + }); + + testWidgets('renders error state', (tester) async { + final cubit = MockAboutDietCubit(); + final feedCubit = MockFeedCubit(); + final commentsCubit = MockCommentsCubit(); + const state = AboutDietState.error(message: 'Error loading diet'); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + + registerMocks(cubit, commentsCubit); + + await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false))); + await tester.pump(); + + expect(find.byType(AboutDietScreen), findsOneWidget); + }); + }); +} diff --git a/test/features/feed/presentation/pages/feed_screen_smoke_test.dart b/test/features/feed/presentation/pages/feed_screen_smoke_test.dart new file mode 100644 index 00000000..3f7923b2 --- /dev/null +++ b/test/features/feed/presentation/pages/feed_screen_smoke_test.dart @@ -0,0 +1,139 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.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/features/feed/presentation/bloc/consultants_cubit/consultants_state.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_state.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; +import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; +import 'package:eatvio_app/core/views/repository/views_repository.dart'; +import 'package:eatvio_app/core/views/service/views_service.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'feed_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + group('FeedScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(FeedCubit feedCubit, MarketingCubit marketingCubit, Widget child) { + final viewsService = MockViewsService(); + final viewsRepository = ViewsRepository(viewsService: viewsService); + final viewsCubit = ViewsCubit(viewsRepository: viewsRepository); + + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: viewsCubit), + BlocProvider.value(value: marketingCubit), + BlocProvider.value(value: feedCubit), + ], + child: MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ), + ); + } + + testWidgets('renders FeedScreen with all tabs', (tester) async { + final recipesCubit = MockRecipesCubit(); + final profileCubit = MockFeedProfileCubit(); + final dietsCubit = MockDietsCubit(); + final plansCubit = MockPlansCubit(); + final consultantsCubit = MockConsultantsCubit(); + final feedCubit = MockFeedCubit(); + final viewsService = MockViewsService(); + final viewsRepository = ViewsRepository(viewsService: viewsService); + final viewsCubit = ViewsCubit(viewsRepository: viewsRepository); + final marketingCubit = MockMarketingCubit(); + + when(recipesCubit.state).thenReturn(RecipesState.initial()); + when(recipesCubit.stream).thenAnswer((_) => const Stream.empty()); + when(recipesCubit.close()).thenAnswer((_) async => Future.value()); + when(profileCubit.state).thenReturn(ProfileState(profile: null)); + when(profileCubit.stream).thenAnswer((_) => const Stream.empty()); + when(profileCubit.close()).thenAnswer((_) async => Future.value()); + when(dietsCubit.state).thenReturn(DietsState.initial()); + when(dietsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(dietsCubit.close()).thenAnswer((_) async => Future.value()); + when(plansCubit.state).thenReturn(PlansState.initial()); + when(plansCubit.stream).thenAnswer((_) => const Stream.empty()); + when(plansCubit.close()).thenAnswer((_) async => Future.value()); + when(consultantsCubit.state).thenReturn(const ConsultantsState()); + when(consultantsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(consultantsCubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.state).thenReturn(const FeedState.loading()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.loadFeedBlogPostList()).thenAnswer((_) async => Future.value()); + when(marketingCubit.checkRegistrationDate()).thenAnswer((_) async => false); + when(marketingCubit.stream).thenAnswer((_) => const Stream.empty()); + when(marketingCubit.close()).thenAnswer((_) async => Future.value()); + + sl.registerLazySingleton(() => recipesCubit); + sl.registerLazySingleton(() => profileCubit); + sl.registerLazySingleton(() => dietsCubit); + sl.registerLazySingleton(() => plansCubit); + sl.registerLazySingleton(() => consultantsCubit); + sl.registerLazySingleton(() => feedCubit); + if (!sl.isRegistered()) { + sl.registerLazySingleton(() => viewsRepository); + } else { + sl.unregister(); + sl.registerLazySingleton(() => viewsRepository); + } + if (!sl.isRegistered()) { + sl.registerLazySingleton(() => viewsCubit); + } else { + sl.unregister(); + sl.registerLazySingleton(() => viewsCubit); + } + + await tester.pumpWidget(wrap(feedCubit, marketingCubit, const FeedScreen())); + await tester.pump(); + + expect(find.byType(FeedScreen), findsOneWidget); + }); + }); +} diff --git a/test/features/feed/presentation/widgets/price_choice_filter_widget_test.dart b/test/features/feed/presentation/widgets/price_choice_filter_widget_test.dart new file mode 100644 index 00000000..b42574ca --- /dev/null +++ b/test/features/feed/presentation/widgets/price_choice_filter_widget_test.dart @@ -0,0 +1,57 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/price_choice_filter_widget.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:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'price_choice_filter_widget_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('PriceChoiceFilterWidget', () { + Widget wrap(PlansCubit cubit, Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: BlocProvider.value( + value: cubit, + child: Scaffold(body: child), + ), + ); + } + + testWidgets('renders with PlansCubit', (tester) async { + final cubit = MockPlansCubit(); + const state = PlansState( + isLoading: false, + plans: [], + errorMessage: null, + filterState: PlansFilterState(priceFilter: (null, null), durationIndex: 0, typeIndex: 0), + minPlanPrice: 100, + maxPlanPrice: 1000, + ); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(cubit.changePriceFilter(any, any, disableDebounce: anyNamed('disableDebounce'))).thenReturn(null); + + await tester.pumpWidget(wrap(cubit, PriceChoiceFilterWidget(plansCubit: cubit))); + await tester.pump(); + + expect(find.byType(PriceChoiceFilterWidget), findsOneWidget); + }); + }); +} diff --git a/test/features/finances_and_consultations_archive/presentation/consultant_finances_screen_smoke_test.dart b/test/features/finances_and_consultations_archive/presentation/consultant_finances_screen_smoke_test.dart new file mode 100644 index 00000000..d62431ab --- /dev/null +++ b/test/features/finances_and_consultations_archive/presentation/consultant_finances_screen_smoke_test.dart @@ -0,0 +1,121 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/models/consultant_finance_model.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.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 'consultant_finances_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('ConsultantFinancesScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + final mockFinances = [ + ConsultantFinanceModel( + startDate: DateTime(2025, 1, 1), + bankId: '1234567890123456', + title: 'Test Plan', + clientName: 'Test Client', + duration: 7, + paymentPrice: '1000.00', + paymentStatus: PaymentStatus.completed, + ), + ]; + + Widget wrap(Widget child) { + return MaterialApp.router( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + routerConfig: GoRouter( + initialLocation: '/', + routes: [ + GoRoute(path: '/', builder: (context, state) => child), + GoRoute( + path: '/card-settings-screen', + name: 'cardSettingsScreen', + builder: (context, state) => const Scaffold(body: Text('Card Settings')), + ), + ], + ), + ); + } + + testWidgets('renders loading state', (tester) async { + final repository = MockConsultationsRepository(); + final cubit = ConsultantFinancesCubit(consultationsRepository: repository); + + when(repository.getConsultantFinances(any, any)).thenAnswer((_) async => mockFinances); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const ConsultantFinancesScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(ConsultantFinancesScreen), findsOneWidget); + }); + + testWidgets('renders loaded state with finances', (tester) async { + final repository = MockConsultationsRepository(); + final cubit = ConsultantFinancesCubit(consultationsRepository: repository); + + when(repository.getConsultantFinances(any, any)).thenAnswer((_) async => mockFinances); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const ConsultantFinancesScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.text('Финансы'), findsOneWidget); + expect(find.text('Управление картами'), findsOneWidget); + }); + + testWidgets('renders empty state', (tester) async { + tester.view.physicalSize = const Size(800, 1200); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + final repository = MockConsultationsRepository(); + final cubit = ConsultantFinancesCubit(consultationsRepository: repository); + + when(repository.getConsultantFinances(any, any)).thenAnswer((_) async => []); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const ConsultantFinancesScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.text('У вас нет таких консультаций'), findsOneWidget); + }); + }); +} diff --git a/test/features/finances_and_consultations_archive/presentation/consultations_archive_screen_smoke_test.dart b/test/features/finances_and_consultations_archive/presentation/consultations_archive_screen_smoke_test.dart new file mode 100644 index 00000000..a9e159f2 --- /dev/null +++ b/test/features/finances_and_consultations_archive/presentation/consultations_archive_screen_smoke_test.dart @@ -0,0 +1,141 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; +import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart'; +import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'consultations_archive_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('ConsultationsArchiveScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + final mockStatistic = StatisticModel(total: 10, canceled: 2, inProgress: 3, finished: 5); + + final mockConsultations = [ + ConsultationModel( + id: 1, + chosenPlanState: ChosenPlanStateEnum.stateDone, + planTitle: 'Test Plan', + userName: 'Test User', + userSlug: 'test-user', + consultantPayment: 1000.0, + duration: 7, + currentDiaryDay: 3, + userProfilePhotoUrl: 'https://example.com/avatar.jpg', + consultationOptionPrice: 500, + startDate: DateTime(2025, 1, 1), + endDate: DateTime(2025, 1, 8), + consultantId: 1, + diaries: [], + personalDietId: null, + ), + ]; + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ); + } + + testWidgets('renders loading state', (tester) async { + final repository = MockConsultationsRepository(); + final cubit = ConsultationsArchiveCubit(consultationsRepository: repository); + + when( + repository.getFilteredConsultations( + date: anyNamed('date'), + state: anyNamed('state'), + dateComparison: anyNamed('dateComparison'), + stateComparison: anyNamed('stateComparison'), + ), + ).thenAnswer((_) async => mockConsultations); + when(repository.getStatistic()).thenAnswer((_) async => mockStatistic); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const ConsultationsArchiveScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(ConsultationsArchiveScreen), findsOneWidget); + }); + + testWidgets('renders loaded state with consultations', (tester) async { + final repository = MockConsultationsRepository(); + final cubit = ConsultationsArchiveCubit(consultationsRepository: repository); + + when( + repository.getFilteredConsultations( + date: anyNamed('date'), + state: anyNamed('state'), + dateComparison: anyNamed('dateComparison'), + stateComparison: anyNamed('stateComparison'), + ), + ).thenAnswer((_) async => mockConsultations); + when(repository.getStatistic()).thenAnswer((_) async => mockStatistic); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const ConsultationsArchiveScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.text('Архив консультаций'), findsOneWidget); + }); + + testWidgets('renders error state', (tester) async { + final repository = MockConsultationsRepository(); + final cubit = ConsultationsArchiveCubit(consultationsRepository: repository); + + when( + repository.getFilteredConsultations( + date: anyNamed('date'), + state: anyNamed('state'), + dateComparison: anyNamed('dateComparison'), + stateComparison: anyNamed('stateComparison'), + ), + ).thenThrow(Exception('Error loading consultations')); + when(repository.getStatistic()).thenAnswer((_) async => mockStatistic); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const ConsultationsArchiveScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(ConsultationsArchiveScreen), findsOneWidget); + }); + }); +} diff --git a/test/features/finances_and_consultations_archive/presentation/user_purchases_screen_smoke_test.dart b/test/features/finances_and_consultations_archive/presentation/user_purchases_screen_smoke_test.dart new file mode 100644 index 00000000..da71783a --- /dev/null +++ b/test/features/finances_and_consultations_archive/presentation/user_purchases_screen_smoke_test.dart @@ -0,0 +1,110 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_model.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; +import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'user_purchases_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('UserPurchasesScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + final mockPurchases = [ + UserPurchasesModel( + startDate: DateTime(2025, 1, 1), + bankId: '1234567890123456', + title: 'Test Plan', + consultantName: 'Test Consultant', + duration: 7, + paymentPrice: 1000, + chosenPlanState: ChosenPlanStateEnum.stateChargeWasCollected, + ), + ]; + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ); + } + + testWidgets('renders loading state', (tester) async { + final repository = MockConsultationsRepository(); + final cubit = UserPurchasesCubit(consultationsRepository: repository); + + when(repository.getUserPurchases(any, any)).thenAnswer((_) async => mockPurchases); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const UserPurchasesScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(UserPurchasesScreen), findsOneWidget); + }); + + testWidgets('renders loaded state with purchases', (tester) async { + final repository = MockConsultationsRepository(); + final cubit = UserPurchasesCubit(consultationsRepository: repository); + + when(repository.getUserPurchases(any, any)).thenAnswer((_) async => mockPurchases); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const UserPurchasesScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.text('Финансы'), findsOneWidget); + }); + + testWidgets('renders empty state', (tester) async { + tester.view.physicalSize = const Size(800, 1200); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + final repository = MockConsultationsRepository(); + final cubit = UserPurchasesCubit(consultationsRepository: repository); + + when(repository.getUserPurchases(any, any)).thenAnswer((_) async => []); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const UserPurchasesScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.text('У вас нет таких консультаций'), findsOneWidget); + }); + }); +} diff --git a/test/features/menu/presentation/pages/menu_screen_smoke_test.dart b/test/features/menu/presentation/pages/menu_screen_smoke_test.dart new file mode 100644 index 00000000..cc1cc135 --- /dev/null +++ b/test/features/menu/presentation/pages/menu_screen_smoke_test.dart @@ -0,0 +1,173 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.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/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.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 'menu_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('MenuScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + const mockUserProfile = ProfileModel( + id: 1, + name: 'Test User', + email: 'test@example.com', + phoneConfirmationState: 'confirmed', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + slug: 'test-user', + profilePhotoUrl: 'https://example.com/avatar.jpg', + roles: ['user'], + badges: [], + subscriptions: [], + subscribers: [], + ); + + const mockConsultantProfile = ProfileModel( + id: 1, + name: 'Test Consultant', + email: 'consultant@example.com', + phoneConfirmationState: 'confirmed', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + slug: 'test-consultant', + profilePhotoUrl: 'https://example.com/avatar.jpg', + roles: ['consultant'], + badges: [], + subscriptions: [], + subscribers: [], + ); + + Widget wrap(Widget child) { + return MaterialApp.router( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + routerConfig: GoRouter( + initialLocation: '/', + routes: [ + GoRoute(path: '/', builder: (context, state) => child), + GoRoute( + path: '/settings-screen', + builder: (context, state) => const Scaffold(body: Text('Settings')), + ), + GoRoute( + path: '/glossary-screen', + builder: (context, state) => const Scaffold(body: Text('Glossary')), + ), + GoRoute( + path: '/support-screen', + builder: (context, state) => const Scaffold(body: Text('Support')), + ), + GoRoute( + path: '/my-recipes-screen', + name: 'myRecipesScreen', + builder: (context, state) => const Scaffold(body: Text('My Recipes')), + ), + GoRoute( + path: '/my-diets-screen', + name: 'myDietsScreen', + builder: (context, state) => const Scaffold(body: Text('My Diets')), + ), + GoRoute( + path: '/my-plans-screen', + name: 'myPlansScreen', + builder: (context, state) => const Scaffold(body: Text('My Plans')), + ), + GoRoute( + path: '/consultant-finances-screen', + name: 'consultantFinancesScreen', + builder: (context, state) => const Scaffold(body: Text('Consultant Finances')), + ), + GoRoute( + path: '/user-purchases-screen', + name: 'userPurchasesScreen', + builder: (context, state) => const Scaffold(body: Text('User Purchases')), + ), + GoRoute( + path: '/consultant-rating-screen', + name: 'consultantRatingScreen', + builder: (context, state) => const Scaffold(body: Text('Consultant Rating')), + ), + GoRoute( + path: '/notifications-settings-screen', + name: 'notificationsSettingsScreen', + builder: (context, state) => const Scaffold(body: Text('Notifications Settings')), + ), + GoRoute( + path: '/blocked-users-screen', + name: 'blockedUsersScreen', + builder: (context, state) => const Scaffold(body: Text('Blocked Users')), + ), + ], + ), + ); + } + + testWidgets('renders menu screen for user', (tester) async { + final repository = MockProfileRepository(); + final cubit = MenuCubit(repository); + + when(repository.getPassKey(routeName: anyNamed('routeName'), routeParams: anyNamed('routeParams'))).thenAnswer((_) async => 'test-passkey'); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const MenuScreen(profileModel: mockUserProfile))); + await tester.pumpAndSettle(); + + expect(find.text('Меню'), findsOneWidget); + expect(find.text('Настройки профиля'), findsOneWidget); + }); + + testWidgets('renders menu screen for consultant', (tester) async { + final repository = MockProfileRepository(); + final cubit = MenuCubit(repository); + + when(repository.getPassKey(routeName: anyNamed('routeName'), routeParams: anyNamed('routeParams'))).thenAnswer((_) async => 'test-passkey'); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const MenuScreen(profileModel: mockConsultantProfile))); + await tester.pumpAndSettle(); + + expect(find.text('Меню'), findsOneWidget); + expect(find.text('Мои рецепты'), findsOneWidget); + expect(find.text('Мои диеты'), findsOneWidget); + expect(find.text('Мои планы'), findsOneWidget); + }); + }); +} diff --git a/test/features/menu/presentation/pages/notifications_settings_screen_smoke_test.dart b/test/features/menu/presentation/pages/notifications_settings_screen_smoke_test.dart new file mode 100644 index 00000000..08d3b522 --- /dev/null +++ b/test/features/menu/presentation/pages/notifications_settings_screen_smoke_test.dart @@ -0,0 +1,115 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart'; +import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'notifications_settings_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('NotificationsSettingsScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + const mockNotificationSettings = NotificationSettingsModel( + newUpdatesNotification: true, + weightMeasurementsNotification: true, + isEmailNotificationsEnabled: true, + isPushNotificationsEnabled: true, + diaryReminderNotifications: DiaryReminderNotifications( + firstReminder: ReminderSettings(enabled: true, localTimeHours: 9, localTimeMins: 0), + secondReminder: ReminderSettings(enabled: true, localTimeHours: 13, localTimeMins: 0), + thirdReminder: ReminderSettings(enabled: false, localTimeHours: 18, localTimeMins: 0), + ), + ); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ); + } + + testWidgets('renders loading state', (tester) async { + tester.view.physicalSize = const Size(800, 1200); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + final repository = MockNotificationSettingsRepository(); + final cubit = NotificationSettingsCubit(notificationSettingsRepository: repository); + + when(repository.getNotificationSettings()).thenAnswer((_) async => mockNotificationSettings); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const NotificationsSettingsScreen())); + await tester.pump(); + + + expect(find.byType(NotificationsSettingsScreen), findsOneWidget); + }); + + testWidgets('renders loaded state with settings', (tester) async { + tester.view.physicalSize = const Size(800, 1200); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + final repository = MockNotificationSettingsRepository(); + final cubit = NotificationSettingsCubit(notificationSettingsRepository: repository); + + when(repository.getNotificationSettings()).thenAnswer((_) async => mockNotificationSettings); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const NotificationsSettingsScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + + expect(find.byType(NotificationsSettingsScreen), findsOneWidget); + expect(find.text('Новые обновления'), findsOneWidget); + }); + + testWidgets('renders error state', (tester) async { + final repository = MockNotificationSettingsRepository(); + final cubit = NotificationSettingsCubit(notificationSettingsRepository: repository); + + when(repository.getNotificationSettings()).thenThrow(Exception('Error loading settings')); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const NotificationsSettingsScreen())); + await tester.pumpAndSettle(); + + expect(find.textContaining('Ошибка'), findsOneWidget); + expect(find.text('Повторить'), findsOneWidget); + }); + }); +} diff --git a/test/features/my_diets/presentation/my_diets_screen_smoke_test.dart b/test/features/my_diets/presentation/my_diets_screen_smoke_test.dart new file mode 100644 index 00000000..1a852e9e --- /dev/null +++ b/test/features/my_diets/presentation/my_diets_screen_smoke_test.dart @@ -0,0 +1,110 @@ +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/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; +import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'my_diets_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('MyDietsScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + const mockDietsPagination = DietsPagination( + data: [], + links: Links(first: 'http://localhost/api?page=1', last: 'http://localhost/api?page=1', prev: null, next: null), + meta: Meta(currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 0), + ); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ); + } + + testWidgets('renders loading state', (tester) async { + final myDietsRepository = MockMyDietsRepository(); + final createDietRepository = MockCreateDietRepository(); + final cubit = MyDietsCubit(myDietsRepository: myDietsRepository, createDietRepository: createDietRepository); + + when(myDietsRepository.getDiets(page: 1, state: 'active')).thenAnswer((_) async => mockDietsPagination); + when(myDietsRepository.getDiets(page: 1, state: 'draft')).thenAnswer((_) async => mockDietsPagination); + when(myDietsRepository.getDiets(page: 1, state: 'personal')).thenAnswer((_) async => mockDietsPagination); + + sl.registerLazySingleton(() => myDietsRepository); + sl.registerLazySingleton(() => createDietRepository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const MyDietsScreen())); + await tester.pump(); + + expect(find.byType(MyDietsScreen), findsOneWidget); + }); + + testWidgets('renders loaded state', (tester) async { + final myDietsRepository = MockMyDietsRepository(); + final createDietRepository = MockCreateDietRepository(); + final cubit = MyDietsCubit(myDietsRepository: myDietsRepository, createDietRepository: createDietRepository); + + when(myDietsRepository.getDiets(page: 1, state: 'active')).thenAnswer((_) async => mockDietsPagination); + when(myDietsRepository.getDiets(page: 1, state: 'draft')).thenAnswer((_) async => mockDietsPagination); + when(myDietsRepository.getDiets(page: 1, state: 'personal')).thenAnswer((_) async => mockDietsPagination); + + sl.registerLazySingleton(() => myDietsRepository); + sl.registerLazySingleton(() => createDietRepository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const MyDietsScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(MyDietsScreen), findsOneWidget); + }); + + testWidgets('renders error state', (tester) async { + final myDietsRepository = MockMyDietsRepository(); + final createDietRepository = MockCreateDietRepository(); + final cubit = MyDietsCubit(myDietsRepository: myDietsRepository, createDietRepository: createDietRepository); + + when(myDietsRepository.getDiets(page: 1, state: 'active')).thenThrow(Exception('Error loading diets')); + + sl.registerLazySingleton(() => myDietsRepository); + sl.registerLazySingleton(() => createDietRepository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const MyDietsScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(MyDietsScreen), findsOneWidget); + }); + }); +} diff --git a/test/features/my_finance/presentation/pages/card_settings_smoke_test.dart b/test/features/my_finance/presentation/pages/card_settings_smoke_test.dart new file mode 100644 index 00000000..a6e09d0b --- /dev/null +++ b/test/features/my_finance/presentation/pages/card_settings_smoke_test.dart @@ -0,0 +1,92 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/core/enums/base_state.dart'; +import 'package:eatvio_app/features/my_finance/models/card_model.dart'; +import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; +import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'card_settings_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('CardSettingsScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + void registerMock(UserCardsCubit cubit) { + sl.registerLazySingleton(() => cubit); + } + + Widget wrap(UserCardsCubit cubit, Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: BlocProvider.value( + value: cubit, + child: child, + ), + ); + } + + testWidgets('renders with loading state', (tester) async { + final cubit = MockUserCardsCubit(); + final state = UserCardsState(state: BaseState.initial, cards: const [], error: null); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(cubit.getCards()).thenAnswer((_) async => Future.value()); + + registerMock(cubit); + + await tester.pumpWidget(wrap(cubit, const CardSettingsScreen())); + await tester.pump(); + + expect(find.byType(CardSettingsScreen), findsOneWidget); + }); + + testWidgets('renders with cards', (tester) async { + final cubit = MockUserCardsCubit(); + final state = UserCardsState( + state: BaseState.success, + cards: [CardModel(id: 1, cardNumber: '1234567890123456', isDefault: true)], + error: null, + ); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(cubit.getCards()).thenAnswer((_) async => Future.value()); + + registerMock(cubit); + + await tester.pumpWidget(wrap(cubit, const CardSettingsScreen())); + await tester.pump(); + + expect(find.byType(CardSettingsScreen), findsOneWidget); + }); + }); +} diff --git a/test/features/my_finance/presentation/pages/user_finance_screen_smoke_test.dart b/test/features/my_finance/presentation/pages/user_finance_screen_smoke_test.dart new file mode 100644 index 00000000..3cba81f7 --- /dev/null +++ b/test/features/my_finance/presentation/pages/user_finance_screen_smoke_test.dart @@ -0,0 +1,111 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart'; +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'user_finance_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('UserFinanceScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + const mockProfile = ProfileModel( + id: 1, + name: 'Test User', + email: 'test@example.com', + phoneConfirmationState: 'confirmed', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + slug: 'test-user', + profilePhotoUrl: 'https://example.com/avatar.jpg', + roles: ['user'], + badges: [], + subscriptions: [], + subscribers: [], + ); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ); + } + + testWidgets('renders user finance screen', (tester) async { + final cubit = MockProfileAvatarCubit(); + const state = ProfileAvatarStateLoaded(profile: mockProfile); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + + await tester.pumpWidget(BlocProvider.value(value: cubit, child: wrap(const UserFinanceScreen()))); + await tester.pumpAndSettle(); + + expect(find.text('Финансы'), findsOneWidget); + }); + + testWidgets('renders consultant finance screen with card management', (tester) async { + const consultantProfile = ProfileModel( + id: 1, + name: 'Test Consultant', + email: 'consultant@example.com', + phoneConfirmationState: 'confirmed', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + slug: 'test-consultant', + profilePhotoUrl: 'https://example.com/avatar.jpg', + roles: ['consultant'], + badges: [], + subscriptions: [], + subscribers: [], + ); + + final cubit = MockProfileAvatarCubit(); + const state = ProfileAvatarStateLoaded(profile: consultantProfile); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + + await tester.pumpWidget(BlocProvider.value(value: cubit, child: wrap(const UserFinanceScreen()))); + await tester.pumpAndSettle(); + + expect(find.text('Финансы'), findsOneWidget); + expect(find.text('Управление картами'), findsOneWidget); + }); + }); +} diff --git a/test/features/my_plans/presentation/my_plans_screen_smoke_test.dart b/test/features/my_plans/presentation/my_plans_screen_smoke_test.dart new file mode 100644 index 00000000..a8d95fcf --- /dev/null +++ b/test/features/my_plans/presentation/my_plans_screen_smoke_test.dart @@ -0,0 +1,99 @@ +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/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; +import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'my_plans_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('MyPlansScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + const mockPlansPagination = PlanPaginationModel( + data: [], + links: Links(first: 'http://localhost/api?page=1', last: 'http://localhost/api?page=1', prev: null, next: null), + meta: Meta(currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 0), + ); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ); + } + + testWidgets('renders loading state', (tester) async { + final repository = MockMyPlansRepository(); + final cubit = MyPlansCubit(myPlansRepository: repository); + + when(repository.getPlans(page: 1)).thenAnswer((_) async => mockPlansPagination); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const MyPlansScreen())); + await tester.pump(); + + expect(find.byType(MyPlansScreen), findsOneWidget); + }); + + testWidgets('renders loaded state', (tester) async { + final repository = MockMyPlansRepository(); + final cubit = MyPlansCubit(myPlansRepository: repository); + + when(repository.getPlans(page: 1)).thenAnswer((_) async => mockPlansPagination); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const MyPlansScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(MyPlansScreen), findsOneWidget); + }); + + testWidgets('renders error state', (tester) async { + final repository = MockMyPlansRepository(); + final cubit = MyPlansCubit(myPlansRepository: repository); + + when(repository.getPlans(page: 1)).thenThrow(Exception('Error loading plans')); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const MyPlansScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(MyPlansScreen), findsOneWidget); + }); + }); +} diff --git a/test/features/notifications/presentation/notifications_screen_smoke_test.dart b/test/features/notifications/presentation/notifications_screen_smoke_test.dart new file mode 100644 index 00000000..0c6e63e4 --- /dev/null +++ b/test/features/notifications/presentation/notifications_screen_smoke_test.dart @@ -0,0 +1,70 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/notifications/models/notification_model.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'notifications_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('NotificationsScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(NotificationsCubit cubit) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: BlocProvider.value(value: cubit, child: const NotificationsScreen()), + ); + } + + testWidgets('renders loaded state with notifications list', (tester) async { + final cubit = MockNotificationsCubit(); + final state = NotificationsState.loaded( + notifications: [DiaryReminderNotification(type: NotificationType.diaryReminderNotification, isRead: false, createdAt: DateTime(2025, 1, 1))], + notificationsCount: 1, + isLoading: false, + hasUnreadNotifications: true, + currentPage: 1, + hasNextPage: false, + isLoadingMore: false, + ); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(cubit.markNotificationsAsRead()).thenAnswer((_) async => Future.value()); + + await tester.pumpWidget(wrap(cubit)); + await tester.pump(); + + expect(find.text('Уведомления'), findsOneWidget); + expect(find.text(S.current.markAsRead), findsOneWidget); + }); + }); +} diff --git a/test/features/order_info/presentation/info_by_order_screen_smoke_test.dart b/test/features/order_info/presentation/info_by_order_screen_smoke_test.dart new file mode 100644 index 00000000..1a8b5be4 --- /dev/null +++ b/test/features/order_info/presentation/info_by_order_screen_smoke_test.dart @@ -0,0 +1,79 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart'; +import 'package:eatvio_app/features/order_info/cubit/order_info_state.dart'; +import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.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 'info_by_order_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('InfoByOrderScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + void registerMock(OrderInfoCubit cubit) { + sl.registerLazySingleton(() => cubit); + } + + Widget wrap(OrderInfoCubit cubit, Widget child) { + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => BlocProvider.value( + value: cubit, + child: child, + ), + ), + ], + ); + + return MaterialApp.router( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + routerConfig: router, + ); + } + + testWidgets('renders loading state', (tester) async { + final cubit = MockOrderInfoCubit(); + final state = OrderInfoState(isLoading: true, chosenPlan: null); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + when(cubit.getChosenPlan(id: anyNamed('id'), customerSlug: anyNamed('customerSlug'))).thenAnswer((_) async => Future.value()); + + registerMock(cubit); + + await tester.pumpWidget(wrap(cubit, const InfoByOrderScreen(planId: 1))); + await tester.pump(); + + expect(find.byType(InfoByOrderScreen), findsOneWidget); + }); + }); +} diff --git a/test/features/plans/presentation/pages/about_plan_screen_smoke_test.dart b/test/features/plans/presentation/pages/about_plan_screen_smoke_test.dart new file mode 100644 index 00000000..e57b7601 --- /dev/null +++ b/test/features/plans/presentation/pages/about_plan_screen_smoke_test.dart @@ -0,0 +1,186 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +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/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'; +import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; +import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_state.dart'; +import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_portal/flutter_portal.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:network_image_mock/network_image_mock.dart'; + +import 'about_plan_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec(), MockSpec()]) +void main() { + group('AboutPlanScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + void registerMocks(AboutPlanCubit planCubit, CommentsCubit commentsCubit) { + sl.registerLazySingleton(() => planCubit); + sl.registerLazySingleton(() => commentsCubit); + } + + const testPlan = PlanModel( + id: 1, + slug: 'test-plan', + name: 'Test Plan', + image: 'https://example.com/image.jpg', + duration: 7, + state: 'active', + type: PlanType(fit: true, slim: false, behavior: false, wellness: false), + details: 'Test details', + packetsCount: 1, + packet1Options: null, + packet2Options: null, + packet3Options: null, + author: Author( + authorSlug: 'author', + authorName: 'Author Name', + authorProfileIcon: 'https://example.com/icon.jpg', + authorIsMe: false, + isEntitySubscribedByMe: null, + authorPlaceInRating: null, + ), + totalLikes: 10, + totalViews: 100, + totalComments: 5, + isFavorite: false, + minPriceOfOptions: 100.0, + maxPriceOfOptions: 200.0, + wasLikedByMe: false, + ); + + Widget wrap(AboutPlanCubit planCubit, FeedCubit feedCubit, CommentsCubit commentsCubit, Widget child) { + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => MultiBlocProvider( + providers: [ + BlocProvider.value(value: planCubit), + BlocProvider.value(value: feedCubit), + BlocProvider.value(value: commentsCubit), + ], + child: child, + ), + ), + GoRoute(path: '/feed-screen', builder: (_, _) => const SizedBox.shrink()), + ], + ); + + return Portal( + child: MaterialApp.router( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + routerConfig: router, + ), + ); + } + + testWidgets('renders loading state', (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.empty()); + when(planCubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(planCubit, commentsCubit); + + await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null))); + await tester.pump(); + + expect(find.byType(AboutPlanScreen), findsOneWidget); + }); + + testWidgets('renders loaded state with plan', (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.empty()); + when(planCubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(planCubit, commentsCubit); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null))); + await tester.pump(); + await tester.pump(); + }); + + expect(find.byType(AboutPlanScreen), findsOneWidget); + expect(find.text('Test Plan'), findsOneWidget); + }); + + testWidgets('renders error state', (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.empty()); + when(planCubit.close()).thenAnswer((_) async => Future.value()); + when(planCubit.getPlan(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + + registerMocks(planCubit, commentsCubit); + + await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null))); + await tester.pump(); + + expect(find.byType(AboutPlanScreen), findsOneWidget); + }); + }); +} diff --git a/test/features/profile/presentation/pages/profile_screen_smoke_test.dart b/test/features/profile/presentation/pages/profile_screen_smoke_test.dart new file mode 100644 index 00000000..4ecef522 --- /dev/null +++ b/test/features/profile/presentation/pages/profile_screen_smoke_test.dart @@ -0,0 +1,205 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; +import 'package:eatvio_app/features/profile/models/circumference_model.dart'; +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:eatvio_app/features/profile/models/weight_model.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans; +import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:eatvio_app/features/plans/repositories/plans_repository.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +import 'profile_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec(), MockSpec(), MockSpec()]) +void main() { + group('ProfileScreen smoke', () { + late MockProfileRepository profileRepository; + late MockChatRepository chatRepository; + late MockConsultantRepository consultantRepository; + late MockPlansRepository plansRepository; + + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + + profileRepository = MockProfileRepository(); + chatRepository = MockChatRepository(); + consultantRepository = MockConsultantRepository(); + plansRepository = MockPlansRepository(); + + sl.registerLazySingleton(() => profileRepository); + sl.registerLazySingleton(() => chatRepository); + sl.registerLazySingleton(() => consultantRepository); + sl.registerLazySingleton(() => plansRepository); + + sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl())); + sl.registerFactory(() => profile_plans.PlansCubit(plansRepository: sl())); + }); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: Scaffold(body: child), + ); + } + + testWidgets('renders my profile loaded state', (tester) async { + const profile = ProfileModel( + id: 1, + name: 'Test User', + email: 'test@example.com', + phoneConfirmationState: 'confirmed', + slug: 'test-user', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + profilePhotoUrl: '', + roles: [], + badges: [], + subscriptions: [], + subscribers: [], + ); + + const profileDetails = ProfileDetailsModel( + firstName: 'Test', + lastName: 'User', + birthdate: '1990-01-01', + gender: 'male', + country: 'Russia', + city: 'Moscow', + lang: 'ru', + weight: 70.0, + height: 175, + chestCircumference: 100, + waistCircumference: 80, + hipCircumference: 90, + bloodPressureUp: 120, + bloodPressureDown: 80, + hasHypo: false, + hasHyper: false, + aboutAlergy: '', + aboutTakingPils: '', + aboutSportHobbies: '', + favoriteFoods: '', + unfavoriteFoods: '', + additionalInfo: '', + isMoodFillable: true, + isBedtimeFillable: true, + activityCoefficient: 1.5, + ); + + when(profileRepository.getMyProfile()).thenAnswer((_) async => profile); + when(profileRepository.getMyProfileDetails()).thenAnswer((_) async => profileDetails); + when(profileRepository.getBlogPostsByAuthorSlug(slug: anyNamed('slug'))).thenAnswer((_) async => []); + when(profileRepository.getWeight(period: anyNamed('period'))).thenAnswer((_) async => []); + when(profileRepository.getCircumferences(period: anyNamed('period'))).thenAnswer((_) async => []); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget(wrap(const ProfileScreen())); + await tester.pumpAndSettle(); + }); + + expect(find.byType(ProfileScreen), findsOneWidget); + }); + + testWidgets('renders error state', (tester) async { + when(profileRepository.getMyProfile()).thenThrow(Exception('Error loading profile')); + when(profileRepository.getMyProfileDetails()).thenThrow(Exception('Error')); + + await tester.pumpWidget(wrap(const ProfileScreen())); + await tester.pumpAndSettle(); + + expect(find.byType(ProfileScreen), findsOneWidget); + }); + + testWidgets('renders blocked user state', (tester) async { + const profile = ProfileModel( + id: 1, + name: 'Test User', + email: 'test@example.com', + phoneConfirmationState: 'confirmed', + slug: 'test-user', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: true, + isEntitySubscribedByMe: false, + profilePhotoUrl: '', + roles: [], + badges: [], + subscriptions: [], + subscribers: [], + ); + + const profileDetails = ProfileDetailsModel( + firstName: 'Test', + lastName: 'User', + birthdate: '1990-01-01', + gender: 'male', + country: 'Russia', + city: 'Moscow', + lang: 'ru', + weight: 70.0, + height: 175, + chestCircumference: 100, + waistCircumference: 80, + hipCircumference: 90, + bloodPressureUp: 120, + bloodPressureDown: 80, + hasHypo: false, + hasHyper: false, + aboutAlergy: '', + aboutTakingPils: '', + aboutSportHobbies: '', + favoriteFoods: '', + unfavoriteFoods: '', + additionalInfo: '', + isMoodFillable: true, + isBedtimeFillable: true, + activityCoefficient: 1.5, + ); + + when(profileRepository.getProfileBySlug(any)).thenAnswer((_) async => profile); + when(profileRepository.getMyProfile()).thenAnswer((_) async => profile); + when(profileRepository.getMyProfileDetails()).thenAnswer((_) async => profileDetails); + when(profileRepository.getProfileDetailsBySlug(any)).thenAnswer((_) async => profileDetails); + when(profileRepository.getBlogPostsByAuthorSlug(slug: anyNamed('slug'))).thenAnswer((_) async => []); + when(profileRepository.getWeight(period: anyNamed('period'))).thenAnswer((_) async => []); + when(profileRepository.getCircumferences(period: anyNamed('period'))).thenAnswer((_) async => []); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget(wrap(const ProfileScreen(slug: 'test-user'))); + await tester.pumpAndSettle(); + }); + + expect(find.byType(ProfileScreen), findsOneWidget); + expect(find.text('Пользователь ограничил доступ к своей странице'), findsOneWidget); + }); + }); +} diff --git a/test/features/profile/presentation/pages/subs_screen_smoke_test.dart b/test/features/profile/presentation/pages/subs_screen_smoke_test.dart new file mode 100644 index 00000000..70797652 --- /dev/null +++ b/test/features/profile/presentation/pages/subs_screen_smoke_test.dart @@ -0,0 +1,96 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/profile/models/sub_model.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +import 'subs_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('SubsScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) { + return MaterialApp(theme: ThemeHelper().themeData(), home: child); + } + + testWidgets('renders subscriptions list', (tester) async { + final repo = MockProfileRepository(); + + when(repo.getSubscriptionsBySlug(any, any)).thenAnswer( + (_) async => [ + SubModel( + slug: 'user-1', + name: 'User 1', + city: 'Moscow', + birthdate: DateTime(1990, 1, 1), + isEntitySubscribedByMe: false, + profilePhotoUrl: '/photo.jpg', + roles: const [], + ), + ], + ); + + when(repo.getSubscribersBySlug(any, any)).thenAnswer((_) async => []); + + sl.registerLazySingleton(() => repo); + sl.registerFactory(() => SubCubit(profileRepository: sl())); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget(wrap(const SubsScreen(slug: 'test', subType: SubType.subscriptions))); + await tester.pumpAndSettle(); + }); + + expect(find.text('Подписки'), findsOneWidget); + expect(find.text('User 1'), findsOneWidget); + }); + + testWidgets('renders subscribers list', (tester) async { + final repo = MockProfileRepository(); + + when(repo.getSubscribersBySlug(any, any)).thenAnswer( + (_) async => [ + SubModel( + slug: 'user-2', + name: 'User 2', + city: 'London', + birthdate: DateTime(1995, 5, 15), + isEntitySubscribedByMe: true, + profilePhotoUrl: '/photo2.jpg', + roles: const [], + ), + ], + ); + + when(repo.getSubscriptionsBySlug(any, any)).thenAnswer((_) async => []); + + sl.registerLazySingleton(() => repo); + sl.registerFactory(() => SubCubit(profileRepository: sl())); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget(wrap(const SubsScreen(slug: 'test', subType: SubType.subscribers))); + await tester.pumpAndSettle(); + }); + + expect(find.text('Подписчики'), findsOneWidget); + expect(find.text('User 2'), findsOneWidget); + }); + }); +} diff --git a/test/features/profile/presentation/widgets/profile_blog_post_widget_smoke_test.dart b/test/features/profile/presentation/widgets/profile_blog_post_widget_smoke_test.dart new file mode 100644 index 00000000..0444d7c4 --- /dev/null +++ b/test/features/profile/presentation/widgets/profile_blog_post_widget_smoke_test.dart @@ -0,0 +1,250 @@ +import 'dart:async'; +import 'dart:io'; + +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/theme/theme_helper.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/profile/models/profile_model.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart'; +import 'package:eatvio_app/features/profile/presentation/widgets/profile_blog_post_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:eatvio_app/generated/l10n.dart'; + +import 'profile_blog_post_widget_smoke_test.mocks.dart'; + +class _FakeHttpClient implements HttpClient { + bool _autoUncompress = true; + + @override + bool get autoUncompress => _autoUncompress; + + @override + set autoUncompress(bool value) { + _autoUncompress = value; + } + + @override + Future getUrl(Uri url) async => _FakeHttpClientRequest(); + + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeHttpClientRequest implements HttpClientRequest { + @override + Future close() async => _FakeHttpClientResponse(); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeHttpClientResponse extends Stream> implements HttpClientResponse { + // 1x1 transparent PNG. + static final List _png = [ + 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, + ]; + + @override + int get statusCode => 200; + + @override + int get contentLength => _png.length; + + @override + HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed; + + @override + StreamSubscription> listen(void Function(List)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) { + return Stream>.fromIterable([_png]).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('ProfileBlogPostWidget smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + testWidgets('renders blog post card', (tester) async { + final post = FeedBlogPostModel( + id: 1, + state: 'published', + image: 'test.png', + description: 'Hello', + author: const Author( + authorSlug: 'author', + authorName: 'Author', + authorProfileIcon: 'https://example.com/icon.png', + authorIsMe: false, + isEntitySubscribedByMe: false, + ), + videoUrl: '', + createdAt: DateTime(2025, 1, 1), + updatedAt: DateTime(2025, 1, 1), + title: 'Title', + totalLikes: 0, + totalViews: 0, + totalComments: 0, + wasLikedByMe: false, + ); + + const profile = ProfileModel( + id: 1, + name: 'User', + email: 'u@e.com', + phoneConfirmationState: 'confirmed', + slug: 'user', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + profilePhotoUrl: '', + roles: [], + badges: [], + subscriptions: [], + subscribers: [], + ); + + const details = ProfileDetailsModel( + firstName: 'Test', + lastName: 'User', + birthdate: '1990-01-01', + gender: 'male', + country: 'Russia', + city: 'Moscow', + lang: 'ru', + weight: 70.0, + height: 175, + chestCircumference: 100, + waistCircumference: 80, + hipCircumference: 90, + bloodPressureUp: 120, + bloodPressureDown: 80, + hasHypo: false, + hasHyper: false, + aboutAlergy: '', + aboutTakingPils: '', + aboutSportHobbies: '', + favoriteFoods: '', + unfavoriteFoods: '', + additionalInfo: '', + isMoodFillable: true, + isBedtimeFillable: true, + activityCoefficient: 1.5, + ); + + final loadedState = ProfileState.loaded(profile: profile, profileDetails: details, blogPosts: [post], isMyProfile: true); + final cubit = MockProfileCubit(); + when(cubit.state).thenReturn(loadedState); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async => Future.value()); + + await HttpOverrides.runZoned(() async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: Scaffold(body: ProfileBlogPostWidget(index: 0, profileCubit: cubit)), + ), + ); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + }, createHttpClient: (context) => _FakeHttpClient()); + + expect(find.text('Регистрация'), findsNothing); + expect(find.text('Title'), findsOneWidget); + expect(find.text('Author'), findsOneWidget); + }); + }); +} diff --git a/test/features/profile/presentation/widgets/profile_charts_smoke_test.dart b/test/features/profile/presentation/widgets/profile_charts_smoke_test.dart new file mode 100644 index 00000000..df88a7b5 --- /dev/null +++ b/test/features/profile/presentation/widgets/profile_charts_smoke_test.dart @@ -0,0 +1,64 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/profile/models/circumference_model.dart'; +import 'package:eatvio_app/features/profile/models/weight_model.dart'; +import 'package:eatvio_app/features/profile/presentation/widgets/circumference_chart_widget.dart'; +import 'package:eatvio_app/features/profile/presentation/widgets/weight_chart_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; + +void main() { + group('Profile chart widgets smoke', () { + setUpAll(() async { + Intl.defaultLocale = 'ru'; + await initializeDateFormatting('ru'); + }); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + home: Scaffold(body: SingleChildScrollView(child: child)), + ); + } + + testWidgets('WeightChartWidget renders empty state', (tester) async { + await tester.pumpWidget(wrap(const WeightChartWidget(weightPoints: []))); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + expect(find.text('Добавьте Ваш вес'), findsOneWidget); + }); + + testWidgets('WeightChartWidget renders with one point', (tester) async { + final points = [WeightModel(id: 1, userId: 1, weight: 70, createdAt: DateTime(2025, 1, 1), updatedAt: DateTime(2025, 1, 1), bmi: 22.0)]; + await tester.pumpWidget(wrap(WeightChartWidget(weightPoints: points))); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + expect(find.textContaining('кг'), findsWidgets); + }); + + testWidgets('CircumferenceChartWidget renders empty state', (tester) async { + await tester.pumpWidget(wrap(const CircumferenceChartWidget(circumferencePoints: []))); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + expect(find.text('Добавьте Ваши замеры'), findsOneWidget); + }); + + testWidgets('CircumferenceChartWidget renders with one point', (tester) async { + final points = [ + CircumferenceModel( + id: 1, + userId: 1, + chestCircumference: 100, + waistCircumference: 80, + hipCircumference: 90, + createdAt: DateTime(2025, 1, 1), + updatedAt: DateTime(2025, 1, 1), + ), + ]; + await tester.pumpWidget(wrap(CircumferenceChartWidget(circumferencePoints: points))); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + expect(find.text('100'), findsOneWidget); + expect(find.text('80'), findsOneWidget); + expect(find.text('90'), findsOneWidget); + }); + }); +} diff --git a/test/features/recipe/presentation/pages/about_recipe_screen_smoke_test.dart b/test/features/recipe/presentation/pages/about_recipe_screen_smoke_test.dart new file mode 100644 index 00000000..3008d3b3 --- /dev/null +++ b/test/features/recipe/presentation/pages/about_recipe_screen_smoke_test.dart @@ -0,0 +1,197 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +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/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_state.dart'; +import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.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:network_image_mock/network_image_mock.dart'; + +import 'about_recipe_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec(), MockSpec()]) +void main() { + group('AboutRecipeScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + FlutterError.onError = (FlutterErrorDetails details) { + if (details.exception is FlutterError && details.exception.toString().contains('overflowed')) { + return; + } + FlutterError.presentError(details); + }; + }); + + void registerMocks(AboutRecipeCubit recipeCubit, CommentsCubit commentsCubit) { + sl.registerLazySingleton(() => recipeCubit); + sl.registerLazySingleton(() => commentsCubit); + } + + final testRecipe = Recipe( + id: 1, + slug: 'test-recipe', + title: 'Test Recipe', + image: 'https://example.com/image.jpg', + description: 'Test description', + state: 'active', + cookingTime: 30, + activeCookingTime: 20, + ingredients: const [], + steps: const [], + videoUrl: null, + createdAt: DateTime(2025, 1, 1), + updatedAt: DateTime(2025, 1, 1), + type: 'main', + commentsCount: 5, + wasLikedByMe: false, + isFavorite: false, + calories: 200.0, + servings: 4, + likesCount: 10, + viewsCount: 100, + tags: const [], + author: const Author( + authorSlug: 'author', + authorName: 'Author Name', + authorProfileIcon: 'https://example.com/icon.jpg', + authorIsMe: false, + isEntitySubscribedByMe: null, + authorPlaceInRating: null, + ), + ); + + Widget wrap(AboutRecipeCubit recipeCubit, FeedCubit feedCubit, CommentsCubit commentsCubit, Widget child) { + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => MediaQuery( + data: const MediaQueryData(size: Size(2000, 1200)), + child: MultiBlocProvider( + providers: [ + BlocProvider.value(value: recipeCubit), + BlocProvider.value(value: feedCubit), + BlocProvider.value(value: commentsCubit), + ], + child: child, + ), + ), + ), + GoRoute(path: '/feed-screen', builder: (_, _) => const SizedBox.shrink()), + ], + ); + + return MaterialApp.router( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + routerConfig: router, + ); + } + + testWidgets('renders loading state', (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.empty()); + when(recipeCubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.viewRecipe(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(recipeCubit, commentsCubit); + + await tester.pumpWidget(wrap(recipeCubit, feedCubit, commentsCubit, const AboutRecipeScreen(recipeSlug: 'test', index: null, navigateToComments: false))); + await tester.pump(); + + expect(find.byType(AboutRecipeScreen), findsOneWidget); + }); + + testWidgets('renders loaded state with recipe', (tester) async { + final recipeCubit = MockAboutRecipeCubit(); + final feedCubit = MockFeedCubit(); + final commentsCubit = MockCommentsCubit(); + final state = AboutRecipeState.loaded(recipe: testRecipe); + + when(recipeCubit.state).thenReturn(state); + when(recipeCubit.stream).thenAnswer((_) => const Stream.empty()); + when(recipeCubit.close()).thenAnswer((_) async => Future.value()); + when(recipeCubit.getRecipe(any, index: anyNamed('index'))).thenAnswer((_) async => Future.value()); + when(feedCubit.viewRecipe(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + when(commentsCubit.setReplyId(any)).thenReturn(null); + + registerMocks(recipeCubit, commentsCubit); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + wrap(recipeCubit, feedCubit, commentsCubit, const AboutRecipeScreen(recipeSlug: 'test', index: null, navigateToComments: false)), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 600)); + }); + + expect(find.byType(AboutRecipeScreen), findsOneWidget); + }); + + testWidgets('renders error state', (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.empty()); + when(recipeCubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.viewRecipe(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + + registerMocks(recipeCubit, commentsCubit); + + await tester.pumpWidget(wrap(recipeCubit, feedCubit, commentsCubit, const AboutRecipeScreen(recipeSlug: 'test', index: null, navigateToComments: false))); + await tester.pump(); + + expect(find.byType(AboutRecipeScreen), findsOneWidget); + }); + }); +} diff --git a/test/features/recipe/presentation/widgets/my_recipe_widget_test.dart b/test/features/recipe/presentation/widgets/my_recipe_widget_test.dart new file mode 100644 index 00000000..52fca10f --- /dev/null +++ b/test/features/recipe/presentation/widgets/my_recipe_widget_test.dart @@ -0,0 +1,112 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart'; +import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart'; +import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipe_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.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:network_image_mock/network_image_mock.dart'; + +import 'my_recipe_widget_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('MyRecipeWidget smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + const mockRecipe = MyRecipeModel( + id: 1, + image: 'https://example.com/recipe.jpg', + state: 'published', + cookingTime: 60, + activeCookingTime: 30, + description: '# Test Recipe\nThis is a test recipe.', + steps: null, + videoUrl: null, + title: 'Test Recipe', + slug: 'test-recipe', + calories: 300.0, + likesCount: 10, + viewsCount: 100, + ); + + Widget wrap(Widget child) { + return MaterialApp.router( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + routerConfig: GoRouter( + initialLocation: '/', + routes: [ + GoRoute(path: '/', builder: (context, state) => child), + GoRoute( + path: '/about-recipes-screen', + name: 'aboutRecipesScreen', + builder: (context, state) => const Scaffold(body: Text('About Recipe')), + ), + ], + ), + ); + } + + testWidgets('renders my recipe widget', (tester) async { + tester.view.physicalSize = const Size(800, 2000); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + mockNetworkImagesFor(() async { + final recipesCubit = MockRecipesCubit(); + final myRecipesCubit = MockMyRecipesCubit(); + + when(recipesCubit.state).thenReturn(RecipesState.initial()); + when(recipesCubit.stream).thenAnswer((_) => const Stream.empty()); + when(recipesCubit.close()).thenAnswer((_) async => Future.value()); + when(myRecipesCubit.state).thenReturn(const MyRecipesState()); + when(myRecipesCubit.stream).thenAnswer((_) => const Stream.empty()); + when(myRecipesCubit.close()).thenAnswer((_) async => Future.value()); + + await tester.pumpWidget( + MultiBlocProvider( + providers: [ + BlocProvider.value(value: recipesCubit), + BlocProvider.value(value: myRecipesCubit), + ], + child: wrap( + const Scaffold( + body: SingleChildScrollView(child: MyRecipeWidget(recipe: mockRecipe)), + ), + ), + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(MyRecipeWidget), findsOneWidget); + }); + }); + }); +} diff --git a/test/features/registration/presentation/pages/registration_screen_smoke_test.dart b/test/features/registration/presentation/pages/registration_screen_smoke_test.dart new file mode 100644 index 00000000..d3b60f15 --- /dev/null +++ b/test/features/registration/presentation/pages/registration_screen_smoke_test.dart @@ -0,0 +1,75 @@ +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/prefs.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart'; +import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +import 'registration_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('RegistrationScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ); + } + + testWidgets('renders registration screen with initial step', (tester) async { + final repository = MockRegistrationRepository(); + final prefs = MockPrefs(); + final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => prefs); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const RegistrationScreen())); + await tester.pump(); + + expect(find.byType(RegistrationScreen), findsOneWidget); + }); + + testWidgets('renders phone screen as first step', (tester) async { + final repository = MockRegistrationRepository(); + final prefs = MockPrefs(); + final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs); + + sl.registerLazySingleton(() => repository); + sl.registerLazySingleton(() => prefs); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const RegistrationScreen())); + await tester.pump(); + + + expect(find.byType(PageView), findsOneWidget); + }); + }); +} diff --git a/test/features/registration_v2/presentation/widgets/first_step_content_test.dart b/test/features/registration_v2/presentation/widgets/first_step_content_test.dart new file mode 100644 index 00000000..cfb23b2d --- /dev/null +++ b/test/features/registration_v2/presentation/widgets/first_step_content_test.dart @@ -0,0 +1,72 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart'; +import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart'; +import 'package:eatvio_app/features/registration_v2/presentation/widgets/first_step_content.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'first_step_content_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('FirstStepContent smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: Scaffold(body: child), + ); + } + + testWidgets('renders first step content', (tester) async { + final cubit = MockRegistrationCubitV2(); + final state = RegistrationStateV2( + step: 0, + firstName: 'John', + lastName: 'Doe', + email: 'test@example.com', + phone: '+7 (999) 123-45-67', + birthday: DateTime(1990, 1, 1), + isFirstNameValid: true, + isLastNameValid: true, + isEmailValid: true, + isPhoneValid: true, + isBirthdayValid: true, + ); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => Stream.value(state)); + when(cubit.close()).thenAnswer((_) async => Future.value()); + + await tester.pumpWidget(BlocProvider.value(value: cubit, child: wrap(const FirstStepContent()))); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(FirstStepContent), findsOneWidget); + }); + }); +} diff --git a/test/features/registration_v2/presentation/widgets/second_step_content_test.dart b/test/features/registration_v2/presentation/widgets/second_step_content_test.dart new file mode 100644 index 00000000..9ea40583 --- /dev/null +++ b/test/features/registration_v2/presentation/widgets/second_step_content_test.dart @@ -0,0 +1,81 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart'; +import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart'; +import 'package:eatvio_app/features/registration_v2/presentation/widgets/second_step_content.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'second_step_content_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('SecondStepContent smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: Scaffold(body: child), + ); + } + + testWidgets('renders second step content', (tester) async { + final cubit = MockRegistrationCubitV2(); + final state = RegistrationStateV2( + step: 1, + firstName: 'John', + lastName: 'Doe', + email: 'test@example.com', + phone: '+7 (999) 123-45-67', + gender: 'male', + password: '', + weight: 70, + height: 180, + activityCoefficient: 1.2, + birthday: DateTime(1990, 1, 1), + isFirstNameValid: true, + isLastNameValid: true, + isEmailValid: true, + isPhoneValid: true, + isGenderValid: true, + isWeightValid: true, + isHeightValid: true, + isActivityCoefficientValid: true, + isBirthdayValid: true, + ); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => Stream.value(state)); + when(cubit.close()).thenAnswer((_) async => Future.value()); + + await tester.pumpWidget(BlocProvider.value(value: cubit, child: wrap(const SecondStepContent()))); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(SecondStepContent), findsOneWidget); + }); + }); +} diff --git a/test/features/settings/presentation/pages/settings_additional_info_screens_smoke_test.dart b/test/features/settings/presentation/pages/settings_additional_info_screens_smoke_test.dart new file mode 100644 index 00000000..24b6c8dd --- /dev/null +++ b/test/features/settings/presentation/pages/settings_additional_info_screens_smoke_test.dart @@ -0,0 +1,199 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; +import 'package:eatvio_app/features/settings/models/profile_settings_model.dart'; +import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; +import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'settings_additional_info_screens_smoke_test.mocks.dart'; + +@GenerateMocks([SettingsCubit]) +void main() { + group('Settings additional info screens smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + SettingsEntity entity(MockSettingsCubit cubit) { + return SettingsEntity( + profileSettingsModel: const ProfileSettingsModel( + newUpdatesNotifications: true, + diaryReminderNotifications: true, + isMoodFillable: true, + isBedtimeFillable: true, + isPublicProfile: true, + ), + profileDetailsModel: const ProfileDetailsModel( + firstName: 'John', + lastName: 'Doe', + birthdate: '1990-01-01', + gender: 'male', + country: 'Russia', + city: 'Moscow', + lang: 'ru', + weight: 75.0, + height: 180, + chestCircumference: 100, + waistCircumference: 80, + hipCircumference: 90, + bloodPressureUp: 120, + bloodPressureDown: 80, + hasHypo: false, + hasHyper: false, + aboutAlergy: 'None', + aboutTakingPils: 'None', + aboutSportHobbies: 'Running', + favoriteFoods: 'Pizza', + unfavoriteFoods: 'Broccoli', + additionalInfo: 'No additional info', + isMoodFillable: true, + isBedtimeFillable: true, + activityCoefficient: 1.5, + ), + settingsCubit: cubit, + ); + } + + void stubCubit(MockSettingsCubit cubit) { + const profile = ProfileModel( + id: 1, + name: 'Test User', + email: 'test@example.com', + phoneConfirmationState: 'confirmed', + slug: 'test-user', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + profilePhotoUrl: '', + roles: [], + badges: [], + subscriptions: [], + subscribers: [], + ); + + const state = SettingsState.loaded( + profileSettings: ProfileSettingsModel( + newUpdatesNotifications: true, + diaryReminderNotifications: true, + isMoodFillable: true, + isBedtimeFillable: true, + isPublicProfile: true, + ), + profileDetails: ProfileDetailsModel( + firstName: 'John', + lastName: 'Doe', + birthdate: '1990-01-01', + gender: 'male', + country: 'Russia', + city: 'Moscow', + lang: 'ru', + weight: 75.0, + height: 180, + chestCircumference: 100, + waistCircumference: 80, + hipCircumference: 90, + bloodPressureUp: 120, + bloodPressureDown: 80, + hasHypo: false, + hasHyper: false, + aboutAlergy: 'None', + aboutTakingPils: 'None', + aboutSportHobbies: 'Running', + favoriteFoods: 'Pizza', + unfavoriteFoods: 'Broccoli', + additionalInfo: 'No additional info', + isMoodFillable: true, + isBedtimeFillable: true, + activityCoefficient: 1.5, + ), + profile: profile, + hasActiveConsultation: false, + ); + + when(cubit.state).thenReturn(state); + when(cubit.stream).thenAnswer((_) => const Stream.empty()); + when(cubit.close()).thenAnswer((_) async {}); + } + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: Scaffold(body: child), + ); + } + + testWidgets('GeneralInfoScreen renders', (tester) async { + final mockCubit = MockSettingsCubit(); + stubCubit(mockCubit); + await tester.pumpWidget(wrap(GeneralInfoScreen(settingsEntity: entity(mockCubit)))); + await tester.pump(); + expect(find.byType(GeneralInfoScreen), findsOneWidget); + expect(find.text('Общая информация'), findsOneWidget); + }); + + testWidgets('PhysicalFormScreen renders', (tester) async { + final mockCubit = MockSettingsCubit(); + stubCubit(mockCubit); + await tester.pumpWidget(wrap(PhysicalFormScreen(settingsEntity: entity(mockCubit)))); + await tester.pump(); + expect(find.byType(PhysicalFormScreen), findsOneWidget); + expect(find.text('Физическая форма'), findsOneWidget); + }); + + testWidgets('PressureScreen renders', (tester) async { + final mockCubit = MockSettingsCubit(); + stubCubit(mockCubit); + await tester.pumpWidget(wrap(PressureScreen(settingsEntity: entity(mockCubit)))); + await tester.pump(); + expect(find.byType(PressureScreen), findsOneWidget); + expect(find.text('Давление'), findsOneWidget); + }); + + testWidgets('OtherInfoScreen renders', (tester) async { + final mockCubit = MockSettingsCubit(); + stubCubit(mockCubit); + await tester.pumpWidget(wrap(OtherInfoScreen(settingsEntity: entity(mockCubit)))); + await tester.pump(); + expect(find.byType(OtherInfoScreen), findsOneWidget); + expect(find.text('Прочая информаиця'), findsOneWidget); + }); + + testWidgets('PhysicalActivityScreen renders', (tester) async { + final mockCubit = MockSettingsCubit(); + stubCubit(mockCubit); + await tester.pumpWidget(wrap(PhysicalActivityScreen(settingsEntity: entity(mockCubit)))); + await tester.pump(); + expect(find.byType(PhysicalActivityScreen), findsOneWidget); + expect(find.text('Физическая нагрузка'), findsOneWidget); + }); + }); +} diff --git a/test/features/settings/presentation/pages/settings_screen_smoke_test.dart b/test/features/settings/presentation/pages/settings_screen_smoke_test.dart new file mode 100644 index 00000000..92cb508a --- /dev/null +++ b/test/features/settings/presentation/pages/settings_screen_smoke_test.dart @@ -0,0 +1,149 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; +import 'package:eatvio_app/features/consultations/repositories/consultations_repository.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/settings/models/profile_settings_model.dart'; +import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart'; +import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'settings_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + group('SettingsScreen smoke', () { + late MockProfileRepository profileRepository; + late MockSettingsRepository settingsRepository; + late MockConsultantRepository consultantRepository; + late MockConsultationsRepository consultationsRepository; + + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + + profileRepository = MockProfileRepository(); + settingsRepository = MockSettingsRepository(); + consultantRepository = MockConsultantRepository(); + consultationsRepository = MockConsultationsRepository(); + + sl.registerLazySingleton(() => profileRepository); + sl.registerLazySingleton(() => settingsRepository); + sl.registerLazySingleton(() => consultantRepository); + sl.registerLazySingleton(() => consultationsRepository); + + sl.registerFactory( + () => SettingsCubit( + profileRepository: sl(), + settingsRepository: sl(), + consultantRepository: sl(), + consultationsRepository: sl(), + ), + ); + }); + + Widget wrap(Widget child) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ); + } + + testWidgets('renders loaded settings', (tester) async { + const profile = ProfileModel( + id: 1, + name: 'Test User', + email: 'test@example.com', + phoneConfirmationState: 'confirmed', + slug: 'test-user', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + profilePhotoUrl: '', + roles: [], + badges: [], + subscriptions: [], + subscribers: [], + ); + + const profileDetails = ProfileDetailsModel( + firstName: 'Test', + lastName: 'User', + birthdate: '1990-01-01', + gender: 'male', + country: 'Russia', + city: 'Moscow', + lang: 'ru', + weight: 70.0, + height: 175, + chestCircumference: 100, + waistCircumference: 80, + hipCircumference: 90, + bloodPressureUp: 120, + bloodPressureDown: 80, + hasHypo: false, + hasHyper: false, + aboutAlergy: '', + aboutTakingPils: '', + aboutSportHobbies: '', + favoriteFoods: '', + unfavoriteFoods: '', + additionalInfo: '', + isMoodFillable: true, + isBedtimeFillable: true, + activityCoefficient: 1.5, + ); + + const profileSettings = ProfileSettingsModel( + newUpdatesNotifications: true, + diaryReminderNotifications: true, + isMoodFillable: true, + isBedtimeFillable: true, + isPublicProfile: true, + ); + + when(profileRepository.getMyProfile()).thenAnswer((_) async => profile); + when(profileRepository.getMyProfileDetails()).thenAnswer((_) async => profileDetails); + when(settingsRepository.getProfileSettings()).thenAnswer((_) async => profileSettings); + + when(consultantRepository.getMyRequestStatus(any)).thenThrow(Exception('skip consultant status in tests')); + when(settingsRepository.getOAuthAccounts()).thenThrow(Exception('skip oauth in tests')); + when(consultationsRepository.getUserPurchases(any, any)).thenThrow(Exception('skip purchases in tests')); + + await tester.pumpWidget(wrap(const SettingsScreen())); + await tester.pumpAndSettle(); + + expect(find.text('Настройки'), findsWidgets); + expect(find.byType(SettingsScreen), findsOneWidget); + }); + }); +} + diff --git a/test/features/user_details/presentation/user_details_screen_smoke_test.dart b/test/features/user_details/presentation/user_details_screen_smoke_test.dart new file mode 100644 index 00000000..6b8a2996 --- /dev/null +++ b/test/features/user_details/presentation/user_details_screen_smoke_test.dart @@ -0,0 +1,147 @@ +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/theme/theme_helper.dart'; +import 'package:eatvio_app/features/profile/models/circumference_model.dart'; +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:eatvio_app/features/profile/models/weight_model.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart'; +import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'user_details_screen_smoke_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('UserDetailsScreen smoke', () { + setUpAll(() async { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + }); + + setUp(() async { + await sl.reset(); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + const mockProfileDetails = ProfileDetailsModel( + firstName: 'John', + lastName: 'Doe', + birthdate: '1990-01-01', + gender: 'male', + country: 'Russia', + city: 'Moscow', + lang: 'ru', + weight: 75.0, + height: 180, + chestCircumference: 100, + waistCircumference: 80, + hipCircumference: 90, + bloodPressureUp: 120, + bloodPressureDown: 80, + hasHypo: false, + hasHyper: false, + aboutAlergy: 'No allergies', + aboutTakingPils: 'No pills', + aboutSportHobbies: 'Running', + favoriteFoods: 'Pizza', + unfavoriteFoods: 'Broccoli', + additionalInfo: 'Test info', + isMoodFillable: false, + isBedtimeFillable: false, + activityCoefficient: 1.2, + ); + + final mockWeightPoints = [ + WeightModel(id: 1, userId: 1, weight: 70, createdAt: DateTime(2025, 1, 1), updatedAt: DateTime(2025, 1, 1), bmi: 21.6), + WeightModel(id: 2, userId: 1, weight: 72, createdAt: DateTime(2025, 2, 1), updatedAt: DateTime(2025, 2, 1), bmi: 22.2), + ]; + + final mockCircumferencePoints = [ + CircumferenceModel( + id: 1, + userId: 1, + chestCircumference: 100, + waistCircumference: 80, + hipCircumference: 90, + createdAt: DateTime(2025, 1, 1), + updatedAt: DateTime(2025, 1, 1), + ), + ]; + + Widget wrap(Widget child) { + return Portal( + child: MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: child, + ), + ); + } + + testWidgets('renders loading state', (tester) async { + final profileRepository = MockProfileRepository(); + final cubit = UserDetailsCubit(profileRepository: profileRepository); + + when(profileRepository.getProfileDetailsBySlug('test-slug')).thenAnswer((_) async => mockProfileDetails); + when(profileRepository.getWeight(period: '1', slug: 'test-slug')).thenAnswer((_) async => mockWeightPoints); + when(profileRepository.getCircumferences(period: '1', slug: 'test-slug')).thenAnswer((_) async => mockCircumferencePoints); + + sl.registerLazySingleton(() => profileRepository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const UserDetailsScreen(slug: 'test-slug'))); + await tester.pump(); + + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(UserDetailsScreen), findsOneWidget); + }); + + testWidgets('renders loaded state with profile details', (tester) async { + final profileRepository = MockProfileRepository(); + final cubit = UserDetailsCubit(profileRepository: profileRepository); + + when(profileRepository.getProfileDetailsBySlug('test-slug')).thenAnswer((_) async => mockProfileDetails); + when(profileRepository.getWeight(period: '1', slug: 'test-slug')).thenAnswer((_) async => mockWeightPoints); + when(profileRepository.getCircumferences(period: '1', slug: 'test-slug')).thenAnswer((_) async => mockCircumferencePoints); + + sl.registerLazySingleton(() => profileRepository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const UserDetailsScreen(slug: 'test-slug'))); + await tester.pumpAndSettle(); + + expect(find.textContaining('John Doe'), findsOneWidget); + }); + + testWidgets('renders error state', (tester) async { + final profileRepository = MockProfileRepository(); + final cubit = UserDetailsCubit(profileRepository: profileRepository); + + when(profileRepository.getProfileDetailsBySlug('test-slug')).thenThrow(Exception('Error loading profile')); + + sl.registerLazySingleton(() => profileRepository); + sl.registerLazySingleton(() => cubit); + + await tester.pumpWidget(wrap(const UserDetailsScreen(slug: 'test-slug'))); + await tester.pumpAndSettle(); + + expect(find.byType(UserDetailsScreen), findsOneWidget); + }); + }); +}