merge upstream

This commit is contained in:
mso7nueh
2025-12-23 13:01:33 +00:00
67 changed files with 8391 additions and 42 deletions

View File

@@ -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<AuthCubit>()])
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>(() => 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<AuthState>.empty());
when(cubit.close()).thenAnswer((_) async {});
sl.registerFactory<AuthCubit>(() => 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);
});
});
}

View File

@@ -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<ResetPasswordCubit>()])
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>(() => 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<ResetPasswordState>.empty());
when(cubit.close()).thenAnswer((_) async {});
sl.registerFactory<ResetPasswordCubit>(() => 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<ResetPasswordState>.empty());
when(cubit.close()).thenAnswer((_) async {});
sl.registerFactory<ResetPasswordCubit>(() => 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);
});
});
}

View File

@@ -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<OAuthCubit>()])
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>(() => 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<OAuthState>.empty());
when(cubit.close()).thenAnswer((_) async {});
CubitRegistrations.sl.registerFactory<OAuthCubit>(() => 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);
});
});
}

View File

@@ -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<BlogPostCubit>(), MockSpec<FeedCubit>(), MockSpec<CommentsCubit>()])
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>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
void registerMocks(BlogPostCubit blogPostCubit, CommentsCubit commentsCubit) {
sl.registerLazySingleton<BlogPostCubit>(() => blogPostCubit);
sl.registerLazySingleton<CommentsCubit>(() => 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<BlogPostCubit>.value(value: blogPostCubit),
BlocProvider<FeedCubit>.value(value: feedCubit),
BlocProvider<CommentsCubit>.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<BlogPostState>.empty());
when(blogPostCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewPost(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(blogPostCubit, commentsCubit);
await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, const BlogPostScreen(id: 1)));
await tester.pump();
expect(find.byType(BlogPostScreen), findsOneWidget);
});
});
}

View File

@@ -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<BlogPostCreateCubit>.value(
value: blogPostCreateCubit,
),
BlocProvider<FeedCubit>.value(
value: mockFeedCubit,
),
BlocProvider<BlogPostCreateCubit>.value(value: blogPostCreateCubit),
BlocProvider<FeedCubit>.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<CustomTextFormField>(find.byKey(AppKeys.blogPostCreateScreenContentTextField));
expect(descriptionField.controller?.text, 'Test Description');
// Only check video field if it's visible (when image is null)
final videoFieldFinder = find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField);
if (videoFieldFinder.evaluate().isNotEmpty) {
final videoField = tester.widget<CustomTextFormField>(videoFieldFinder);
@@ -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);
});
});
}

View File

@@ -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<NotificationsCubit>(), MockSpec<ChatsCubit>()])
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>(() => 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<void>.value());
when(chatsCubit.state).thenReturn(ChatsState());
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(
MultiBlocProvider(
providers: [
BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
BlocProvider<ChatsCubit>.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<void>.value());
when(chatsCubit.state).thenReturn(ChatsState());
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(
MultiBlocProvider(
providers: [
BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
BlocProvider<ChatsCubit>.value(value: chatsCubit),
],
child: wrap(
const Scaffold(
body: BottomNavigationBarWidget(profileModel: mockConsultantProfile, child: Text('Test Content')),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(BottomNavigationBarWidget), findsOneWidget);
});
});
});
}

View File

@@ -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<CalendarRepository>()])
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>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
final mockCalendarList = [
const CalendarModel(date: '2025-01-01', isCurrentDayIncludedInTheChosenDietProgression: true, diary: <String, dynamic>{'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>(() => calendarRepository);
sl.registerLazySingleton<CalendarCubit>(() => 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>(() => calendarRepository);
sl.registerLazySingleton<CalendarCubit>(() => 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>(() => calendarRepository);
sl.registerLazySingleton<CalendarCubit>(() => cubit);
await tester.pumpWidget(wrap(const CalendarScreen()));
await tester.pumpAndSettle();
expect(find.textContaining('Error loading calendar'), findsOneWidget);
});
});
}

View File

@@ -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<ChatRoomCubit>(), MockSpec<ChatsCubit>()])
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>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
const fakeSvg = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
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<ChatsCubit>.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<ChatRoomState>.empty());
when(chatRoomCubit.close()).thenAnswer((_) async => Future<void>.value());
when(chatRoomCubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'))).thenAnswer((_) async => Future<void>.value());
when(chatRoomCubit.setTimer()).thenReturn(null);
sl.registerFactory<ChatRoomCubit>(() => chatRoomCubit);
when(chatsCubit.state).thenReturn(ChatsState());
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.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<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) {
throw FlutterError('Asset not found: $key');
}
return value;
}
@override
Future<ByteData> 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);
}
}

View File

@@ -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<ChatRoomCubit>()])
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>(() => 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<ChatRoomState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'))).thenAnswer((_) async => Future<void>.value());
when(cubit.setTimer()).thenReturn(null);
sl.registerFactory<ChatRoomCubit>(() => cubit);
await tester.pumpWidget(wrap(const ChatScreen(uuid: 'uuid', sendInitialMessage: false)));
await tester.pump();
expect(find.byType(ChatScreen), findsOneWidget);
expect(find.byType(Loader), findsOneWidget);
});
});
}

View File

@@ -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<ChatsCubit>()])
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<ChatsCubit>.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<ChatsState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.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<ChatsState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(wrap(const ChatsScreen(), cubit));
await tester.pump();
expect(find.byType(ChatsScreen), findsOneWidget);
});
});
}

View File

@@ -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);
});
});
}

View File

@@ -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<HttpClientRequest> getUrl(Uri url) async => _FakeHttpClientRequest();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientRequest implements HttpClientRequest {
@override
Future<HttpClientResponse> close() async => _FakeHttpClientResponse();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientResponse extends Stream<List<int>> implements HttpClientResponse {
static final List<int> _png = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
0x42,
0x60,
0x82,
];
@override
int get statusCode => 200;
@override
int get contentLength => _png.length;
@override
HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed;
@override
StreamSubscription<List<int>> listen(void Function(List<int>)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) {
return Stream<List<int>>.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>(() => 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());
});
});
}

View File

@@ -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<ChatRoomState> implements ChatRoomCubit {
FakeChatRoomCubit(super.initialState);
bool finishResult = true;
bool complainResult = true;
@override
Future<bool> choseStartPlanDate(DateTime date) async => true;
@override
Future<bool> finishChosenPlan() async => finishResult;
@override
Future<bool> complainAboutConsultation({required String description}) async => complainResult;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeAssetBundle extends CachingAssetBundle {
_FakeAssetBundle(this.assets);
final Map<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> 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 = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
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);
});
});
}

View File

@@ -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<HttpClientRequest> getUrl(Uri url) async => _FakeHttpClientRequest();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientRequest implements HttpClientRequest {
@override
Future<HttpClientResponse> close() async => _FakeHttpClientResponse();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientResponse extends Stream<List<int>> implements HttpClientResponse {
static final List<int> _png = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
0x42,
0x60,
0x82,
];
@override
int get statusCode => 200;
@override
int get contentLength => _png.length;
@override
HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed;
@override
StreamSubscription<List<int>> listen(void Function(List<int>)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) {
return Stream<List<int>>.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<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> 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 = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
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);
});
});
}

View File

@@ -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>(() => 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);
});
});
});
}

View File

@@ -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<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> 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 = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
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);
});
});
}

View File

@@ -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<HttpClientRequest> getUrl(Uri url) async => _FakeHttpClientRequest();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientRequest implements HttpClientRequest {
@override
Future<HttpClientResponse> close() async => _FakeHttpClientResponse();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientResponse extends Stream<List<int>> implements HttpClientResponse {
static final List<int> _png = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
0x42,
0x60,
0x82,
];
@override
int get statusCode => 200;
@override
int get contentLength => _png.length;
@override
HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed;
@override
StreamSubscription<List<int>> listen(void Function(List<int>)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) {
return Stream<List<int>>.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>(() => 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());
});
});
}

View File

@@ -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<ComplainingRepository>()])
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>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
repository = MockComplainingRepository();
sl.registerLazySingleton<ComplainingRepository>(() => repository);
sl.registerFactory<ComplainingCubit>(() => 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);
});
});
}

View File

@@ -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<RegisterConsultantCubit>()])
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>(() => 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<void>.value());
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
await tester.pumpWidget(BlocProvider<RegisterConsultantCubit>.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<void>.value());
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
await tester.pumpWidget(BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: wrap(const CertificatesAndDiplomasScreen())));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(CertificatesAndDiplomasScreen), findsOneWidget);
});
});
}

View File

@@ -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<RegisterConsultantCubit>()])
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>(() => 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<void>.value());
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
await tester.pumpWidget(BlocProvider<RegisterConsultantCubit>.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<void>.value());
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
await tester.pumpWidget(BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: wrap(const SocialNetworksScreen())));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(SocialNetworksScreen), findsOneWidget);
});
});
}

View File

@@ -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>(() => 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);
});
});
});
}

View File

@@ -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<CreateDietRepository>(), MockSpec<ProfileRepository>(), MockSpec<GlossaryRepository>()])
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) => <DailySchedule>[]));
cubit.setDailySchedule(schedule);
expect(cubit.state.dietData.dailySchedule, schedule);
expect(cubit.state.dietDurationError, null);
});
});
}

View File

@@ -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<CreatePlanRepository>()])
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);
});
});
}

View File

@@ -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<DiaryCubit>(), MockSpec<ConsultantCommentsCubit>()])
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>(() => 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<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit);
sl.registerFactory<ConsultantCommentsCubit>(() => 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<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit);
sl.registerFactory<ConsultantCommentsCubit>(() => 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<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit);
sl.registerFactory<ConsultantCommentsCubit>(() => 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<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit);
sl.registerFactory<ConsultantCommentsCubit>(() => 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<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.submitComments(any)).thenAnswer((_) async => true);
sl.registerFactory<DiaryCubit>(() => diaryCubit);
sl.registerFactory<ConsultantCommentsCubit>(() => 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<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.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<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit);
sl.registerFactory<ConsultantCommentsCubit>(() => 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);
});
});
}

View File

@@ -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<DiaryState> 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<DiaryCubit>.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();
});
});
}

View File

@@ -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<DiaryState> 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<DiaryCubit>.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();
});
});
}

View File

@@ -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<DiaryState> 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<DiaryCubit>.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();
});
});
}

View File

@@ -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<DiaryState> implements DiaryCubit {
FakeDiaryCubit() : super(const DiaryState.loading());
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> 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<DiaryCubit>.value(value: diaryCubit),
if (commentsCubit != null) BlocProvider<ConsultantCommentsCubit>.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);
});
});
}

View File

@@ -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<DiaryState> 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<DiaryCubit>.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);
});
});
}

View File

@@ -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<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
int? fillTimeDiaryId;
String? fillTimeValue;
int? fillMoodValue;
int? fillSatietyValue;
@override
Future<void> fillTime({required int diaryId, required String time}) async {
fillTimeDiaryId = diaryId;
fillTimeValue = time;
}
@override
Future<void> fillMood({required int diaryId, required int mood}) async {
fillMoodValue = mood;
}
@override
Future<void> fillSatiety({required int diaryId, required int satiety}) async {
fillSatietyValue = satiety;
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> 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<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> 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 = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
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<DiaryCubit>.value(value: diaryCubit),
BlocProvider<ConsultantCommentsCubit>.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);
});
});
}

View File

@@ -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<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
int? lastDiaryId;
String? lastTime;
int? lastSatiety;
int? lastMood;
@override
Future<void> fillTime({required int diaryId, required String time}) async {
lastDiaryId = diaryId;
lastTime = time;
}
@override
Future<void> fillSatiety({required int diaryId, required int satiety}) async {
lastDiaryId = diaryId;
lastSatiety = satiety;
}
@override
Future<void> fillMood({required int diaryId, required int mood}) async {
lastDiaryId = diaryId;
lastMood = mood;
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> implements ConsultantCommentsCubit {
FakeConsultantCommentsCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeAssetBundle extends CachingAssetBundle {
_FakeAssetBundle(this.assets);
final Map<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> 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 = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
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<DiaryCubit>.value(value: diaryCubit),
BlocProvider<ConsultantCommentsCubit>.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<RenderBox>(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<RenderBox>(bar);
await tester.tapAt(box.localToGlobal(box.size.center(Offset.zero)));
await tester.pump(const Duration(milliseconds: 300));
expect(diaryCubit.lastMood, isNotNull);
});
});
}

View File

@@ -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<DiaryCubit>()])
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);
});
});
}

View File

@@ -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<DiaryState> 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<DiaryCubit>.value(
value: cubit,
child: Scaffold(body: child),
),
),
);
}
DiaryStateLoaded loadedState({required int selectedMeal, required List<List<PortionModel>> 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<MealModel>.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);
});
});
}

View File

@@ -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<ConsultantCommentsState> 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<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> 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 = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
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<ConsultantCommentsCubit>.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);
});
});
}

View File

@@ -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<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> implements ConsultantCommentsCubit {
FakeConsultantCommentsCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeAssetBundle extends CachingAssetBundle {
_FakeAssetBundle(this.assets);
final Map<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> 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 = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
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<DiaryCubit>.value(value: diaryCubit),
BlocProvider<ConsultantCommentsCubit>.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);
});
});
}

View File

@@ -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<DiaryState> 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<HttpClientRequest> getUrl(Uri url) async => _FakeHttpClientRequest();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientRequest implements HttpClientRequest {
@override
Future<HttpClientResponse> close() async => _FakeHttpClientResponse();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientResponse extends Stream<List<int>> implements HttpClientResponse {
// 1x1 transparent PNG.
static final List<int> _png = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
0x42,
0x60,
0x82,
];
@override
int get statusCode => 200;
@override
int get contentLength => _png.length;
@override
HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed;
@override
StreamSubscription<List<int>> listen(void Function(List<int>)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) {
return Stream<List<int>>.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>(() => 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<DiaryCubit>.value(
value: cubit,
child: const Scaffold(body: PhotoGalleryWidget()),
),
);
}
DiaryStateLoaded loadedState({required List<DiaryPhotoModel> 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();
});
});
}

View File

@@ -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<ConsultantCommentsState> implements ConsultantCommentsCubit {
FakeConsultantCommentsCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeAssetBundle extends CachingAssetBundle {
_FakeAssetBundle(this.assets);
final Map<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> 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 = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
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<ConsultantCommentsCubit>.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);
});
});
}

View File

@@ -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<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class FakeConsultantCommentsCubit extends Cubit<ConsultantCommentsState> implements ConsultantCommentsCubit {
FakeConsultantCommentsCubit(super.initialState);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeAssetBundle extends CachingAssetBundle {
_FakeAssetBundle(this.assets);
final Map<String, String> assets;
@override
Future<String> loadString(String key, {bool cache = true}) async {
final value = assets[key];
if (value == null) throw FlutterError('Asset not found: $key');
return value;
}
@override
Future<ByteData> 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 = '''
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<rect width="10" height="10" fill="red"/>
</svg>
''';
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<DiaryCubit>.value(value: diaryCubit),
BlocProvider<ConsultantCommentsCubit>.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);
});
});
}

View File

@@ -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<DiaryState> 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<DiaryCubit>.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);
});
});
}

View File

@@ -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<DiaryState> 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<DiaryCubit>.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();
});
});
}

View File

@@ -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<AboutDietCubit>(), MockSpec<FeedCubit>(), MockSpec<CommentsCubit>()])
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>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
void registerMocks(AboutDietCubit dietCubit, CommentsCubit commentsCubit) {
sl.registerLazySingleton<AboutDietCubit>(() => dietCubit);
sl.registerLazySingleton<CommentsCubit>(() => 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<AboutDietCubit>.value(value: cubit),
BlocProvider<FeedCubit>.value(value: feedCubit),
BlocProvider<CommentsCubit>.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<AboutDietState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(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<AboutDietState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewDiet(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(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<AboutDietState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewDiet(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(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<AboutDietState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
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);
});
});
}

View File

@@ -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<RecipesCubit>(),
MockSpec<FeedProfileCubit>(),
MockSpec<DietsCubit>(),
MockSpec<PlansCubit>(),
MockSpec<ConsultantsCubit>(),
MockSpec<FeedCubit>(),
MockSpec<ViewsService>(),
MockSpec<MarketingCubit>(),
])
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>(() => 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<ViewsCubit>.value(value: viewsCubit),
BlocProvider<MarketingCubit>.value(value: marketingCubit),
BlocProvider<FeedCubit>.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<void>.value());
when(profileCubit.state).thenReturn(ProfileState(profile: null));
when(profileCubit.stream).thenAnswer((_) => const Stream.empty());
when(profileCubit.close()).thenAnswer((_) async => Future<void>.value());
when(dietsCubit.state).thenReturn(DietsState.initial());
when(dietsCubit.stream).thenAnswer((_) => const Stream.empty());
when(dietsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(plansCubit.state).thenReturn(PlansState.initial());
when(plansCubit.stream).thenAnswer((_) => const Stream.empty());
when(plansCubit.close()).thenAnswer((_) async => Future<void>.value());
when(consultantsCubit.state).thenReturn(const ConsultantsState());
when(consultantsCubit.stream).thenAnswer((_) => const Stream.empty());
when(consultantsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.state).thenReturn(const FeedState.loading());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.loadFeedBlogPostList()).thenAnswer((_) async => Future<void>.value());
when(marketingCubit.checkRegistrationDate()).thenAnswer((_) async => false);
when(marketingCubit.stream).thenAnswer((_) => const Stream.empty());
when(marketingCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerLazySingleton<RecipesCubit>(() => recipesCubit);
sl.registerLazySingleton<FeedProfileCubit>(() => profileCubit);
sl.registerLazySingleton<DietsCubit>(() => dietsCubit);
sl.registerLazySingleton<PlansCubit>(() => plansCubit);
sl.registerLazySingleton<ConsultantsCubit>(() => consultantsCubit);
sl.registerLazySingleton<FeedCubit>(() => feedCubit);
if (!sl.isRegistered<ViewsRepository>()) {
sl.registerLazySingleton<ViewsRepository>(() => viewsRepository);
} else {
sl.unregister<ViewsRepository>();
sl.registerLazySingleton<ViewsRepository>(() => viewsRepository);
}
if (!sl.isRegistered<ViewsCubit>()) {
sl.registerLazySingleton<ViewsCubit>(() => viewsCubit);
} else {
sl.unregister<ViewsCubit>();
sl.registerLazySingleton<ViewsCubit>(() => viewsCubit);
}
await tester.pumpWidget(wrap(feedCubit, marketingCubit, const FeedScreen()));
await tester.pump();
expect(find.byType(FeedScreen), findsOneWidget);
});
});
}

View File

@@ -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<PlansCubit>()])
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<PlansCubit>.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<PlansState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.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);
});
});
}

View File

@@ -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<ConsultationsRepository>()])
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>(() => 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<ConsultationsRepository>(() => repository);
sl.registerLazySingleton<ConsultantFinancesCubit>(() => 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<ConsultationsRepository>(() => repository);
sl.registerLazySingleton<ConsultantFinancesCubit>(() => 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<ConsultationsRepository>(() => repository);
sl.registerLazySingleton<ConsultantFinancesCubit>(() => cubit);
await tester.pumpWidget(wrap(const ConsultantFinancesScreen()));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('У вас нет таких консультаций'), findsOneWidget);
});
});
}

View File

@@ -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<ConsultationsRepository>()])
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>(() => 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<ConsultationsRepository>(() => repository);
sl.registerLazySingleton<ConsultationsArchiveCubit>(() => 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<ConsultationsRepository>(() => repository);
sl.registerLazySingleton<ConsultationsArchiveCubit>(() => 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<ConsultationsRepository>(() => repository);
sl.registerLazySingleton<ConsultationsArchiveCubit>(() => cubit);
await tester.pumpWidget(wrap(const ConsultationsArchiveScreen()));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(ConsultationsArchiveScreen), findsOneWidget);
});
});
}

View File

@@ -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<ConsultationsRepository>()])
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>(() => 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<ConsultationsRepository>(() => repository);
sl.registerLazySingleton<UserPurchasesCubit>(() => 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<ConsultationsRepository>(() => repository);
sl.registerLazySingleton<UserPurchasesCubit>(() => 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<ConsultationsRepository>(() => repository);
sl.registerLazySingleton<UserPurchasesCubit>(() => cubit);
await tester.pumpWidget(wrap(const UserPurchasesScreen()));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('У вас нет таких консультаций'), findsOneWidget);
});
});
}

View File

@@ -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<ProfileRepository>()])
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>(() => 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<ProfileRepository>(() => repository);
sl.registerLazySingleton<MenuCubit>(() => 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<ProfileRepository>(() => repository);
sl.registerLazySingleton<MenuCubit>(() => 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);
});
});
}

View File

@@ -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<NotificationSettingsRepository>()])
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>(() => 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<NotificationSettingsRepository>(() => repository);
sl.registerLazySingleton<NotificationSettingsCubit>(() => 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<NotificationSettingsRepository>(() => repository);
sl.registerLazySingleton<NotificationSettingsCubit>(() => 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<NotificationSettingsRepository>(() => repository);
sl.registerLazySingleton<NotificationSettingsCubit>(() => cubit);
await tester.pumpWidget(wrap(const NotificationsSettingsScreen()));
await tester.pumpAndSettle();
expect(find.textContaining('Ошибка'), findsOneWidget);
expect(find.text('Повторить'), findsOneWidget);
});
});
}

View File

@@ -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<MyDietsRepository>(), MockSpec<CreateDietRepository>()])
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>(() => 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>(() => myDietsRepository);
sl.registerLazySingleton<CreateDietRepository>(() => createDietRepository);
sl.registerLazySingleton<MyDietsCubit>(() => 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>(() => myDietsRepository);
sl.registerLazySingleton<CreateDietRepository>(() => createDietRepository);
sl.registerLazySingleton<MyDietsCubit>(() => 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>(() => myDietsRepository);
sl.registerLazySingleton<CreateDietRepository>(() => createDietRepository);
sl.registerLazySingleton<MyDietsCubit>(() => cubit);
await tester.pumpWidget(wrap(const MyDietsScreen()));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(MyDietsScreen), findsOneWidget);
});
});
}

View File

@@ -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<UserCardsCubit>()])
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>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
void registerMock(UserCardsCubit cubit) {
sl.registerLazySingleton<UserCardsCubit>(() => 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<UserCardsCubit>.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<UserCardsState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.getCards()).thenAnswer((_) async => Future<void>.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<UserCardsState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.getCards()).thenAnswer((_) async => Future<void>.value());
registerMock(cubit);
await tester.pumpWidget(wrap(cubit, const CardSettingsScreen()));
await tester.pump();
expect(find.byType(CardSettingsScreen), findsOneWidget);
});
});
}

View File

@@ -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<ProfileAvatarCubit>()])
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>(() => 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<void>.value());
await tester.pumpWidget(BlocProvider<ProfileAvatarCubit>.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<void>.value());
await tester.pumpWidget(BlocProvider<ProfileAvatarCubit>.value(value: cubit, child: wrap(const UserFinanceScreen())));
await tester.pumpAndSettle();
expect(find.text('Финансы'), findsOneWidget);
expect(find.text('Управление картами'), findsOneWidget);
});
});
}

View File

@@ -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<MyPlansRepository>()])
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>(() => 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<MyPlansRepository>(() => repository);
sl.registerLazySingleton<MyPlansCubit>(() => 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<MyPlansRepository>(() => repository);
sl.registerLazySingleton<MyPlansCubit>(() => 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<MyPlansRepository>(() => repository);
sl.registerLazySingleton<MyPlansCubit>(() => cubit);
await tester.pumpWidget(wrap(const MyPlansScreen()));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(MyPlansScreen), findsOneWidget);
});
});
}

View File

@@ -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<NotificationsCubit>()])
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>(() => 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<NotificationsCubit>.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<NotificationsState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.markNotificationsAsRead()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(wrap(cubit));
await tester.pump();
expect(find.text('Уведомления'), findsOneWidget);
expect(find.text(S.current.markAsRead), findsOneWidget);
});
});
}

View File

@@ -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<OrderInfoCubit>()])
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>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
void registerMock(OrderInfoCubit cubit) {
sl.registerLazySingleton<OrderInfoCubit>(() => cubit);
}
Widget wrap(OrderInfoCubit cubit, Widget child) {
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => BlocProvider<OrderInfoCubit>.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<OrderInfoState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.getChosenPlan(id: anyNamed('id'), customerSlug: anyNamed('customerSlug'))).thenAnswer((_) async => Future<void>.value());
registerMock(cubit);
await tester.pumpWidget(wrap(cubit, const InfoByOrderScreen(planId: 1)));
await tester.pump();
expect(find.byType(InfoByOrderScreen), findsOneWidget);
});
});
}

View File

@@ -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<AboutPlanCubit>(), MockSpec<FeedCubit>(), MockSpec<CommentsCubit>()])
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>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
void registerMocks(AboutPlanCubit planCubit, CommentsCubit commentsCubit) {
sl.registerLazySingleton<AboutPlanCubit>(() => planCubit);
sl.registerLazySingleton<CommentsCubit>(() => 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<AboutPlanCubit>.value(value: planCubit),
BlocProvider<FeedCubit>.value(value: feedCubit),
BlocProvider<CommentsCubit>.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<AboutPlanState>.empty());
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(planCubit, commentsCubit);
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null)));
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<AboutPlanState>.empty());
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(planCubit, commentsCubit);
await 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<AboutPlanState>.empty());
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
when(planCubit.getPlan(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
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);
});
});
}

View File

@@ -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<ProfileRepository>(), MockSpec<ChatRepository>(), MockSpec<ConsultantRepository>(), MockSpec<PlansRepository>()])
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>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
profileRepository = MockProfileRepository();
chatRepository = MockChatRepository();
consultantRepository = MockConsultantRepository();
plansRepository = MockPlansRepository();
sl.registerLazySingleton<ProfileRepository>(() => profileRepository);
sl.registerLazySingleton<ChatRepository>(() => chatRepository);
sl.registerLazySingleton<ConsultantRepository>(() => consultantRepository);
sl.registerLazySingleton<PlansRepository>(() => plansRepository);
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()));
sl.registerFactory<profile_plans.PlansCubit>(() => 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 => <WeightModel>[]);
when(profileRepository.getCircumferences(period: anyNamed('period'))).thenAnswer((_) async => <CircumferenceModel>[]);
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 => <WeightModel>[]);
when(profileRepository.getCircumferences(period: anyNamed('period'))).thenAnswer((_) async => <CircumferenceModel>[]);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(const ProfileScreen(slug: 'test-user')));
await tester.pumpAndSettle();
});
expect(find.byType(ProfileScreen), findsOneWidget);
expect(find.text('Пользователь ограничил доступ к своей странице'), findsOneWidget);
});
});
}

View File

@@ -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<ProfileRepository>()])
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>(() => 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<ProfileRepository>(() => repo);
sl.registerFactory<SubCubit>(() => 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<ProfileRepository>(() => repo);
sl.registerFactory<SubCubit>(() => 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);
});
});
}

View File

@@ -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<HttpClientRequest> getUrl(Uri url) async => _FakeHttpClientRequest();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientRequest implements HttpClientRequest {
@override
Future<HttpClientResponse> close() async => _FakeHttpClientResponse();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeHttpClientResponse extends Stream<List<int>> implements HttpClientResponse {
// 1x1 transparent PNG.
static final List<int> _png = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
0x42,
0x60,
0x82,
];
@override
int get statusCode => 200;
@override
int get contentLength => _png.length;
@override
HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed;
@override
StreamSubscription<List<int>> listen(void Function(List<int>)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) {
return Stream<List<int>>.fromIterable([_png]).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
@GenerateNiceMocks([MockSpec<ProfileCubit>()])
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>(() => 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<ProfileState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.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);
});
});
}

View File

@@ -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);
});
});
}

View File

@@ -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<AboutRecipeCubit>(), MockSpec<FeedCubit>(), MockSpec<CommentsCubit>()])
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>(() => 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<AboutRecipeCubit>(() => recipeCubit);
sl.registerLazySingleton<CommentsCubit>(() => 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<AboutRecipeCubit>.value(value: recipeCubit),
BlocProvider<FeedCubit>.value(value: feedCubit),
BlocProvider<CommentsCubit>.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<AboutRecipeState>.empty());
when(recipeCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewRecipe(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(recipeCubit, commentsCubit);
await tester.pumpWidget(wrap(recipeCubit, feedCubit, commentsCubit, const AboutRecipeScreen(recipeSlug: 'test', index: null, navigateToComments: false)));
await tester.pump();
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<AboutRecipeState>.empty());
when(recipeCubit.close()).thenAnswer((_) async => Future<void>.value());
when(recipeCubit.getRecipe(any, index: anyNamed('index'))).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewRecipe(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
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<AboutRecipeState>.empty());
when(recipeCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewRecipe(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
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);
});
});
}

View File

@@ -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<RecipesCubit>(), MockSpec<MyRecipesCubit>()])
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>(() => 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<void>.value());
when(myRecipesCubit.state).thenReturn(const MyRecipesState());
when(myRecipesCubit.stream).thenAnswer((_) => const Stream.empty());
when(myRecipesCubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(
MultiBlocProvider(
providers: [
BlocProvider<RecipesCubit>.value(value: recipesCubit),
BlocProvider<MyRecipesCubit>.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);
});
});
});
}

View File

@@ -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<RegistrationRepository>(), MockSpec<Prefs>()])
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>(() => 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<RegistrationRepository>(() => repository);
sl.registerLazySingleton<Prefs>(() => prefs);
sl.registerLazySingleton<RegistrationCubit>(() => 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<RegistrationRepository>(() => repository);
sl.registerLazySingleton<Prefs>(() => prefs);
sl.registerLazySingleton<RegistrationCubit>(() => cubit);
await tester.pumpWidget(wrap(const RegistrationScreen()));
await tester.pump();
expect(find.byType(PageView), findsOneWidget);
});
});
}

View File

@@ -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<RegistrationCubitV2>()])
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>(() => 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<void>.value());
await tester.pumpWidget(BlocProvider<RegistrationCubitV2>.value(value: cubit, child: wrap(const FirstStepContent())));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(FirstStepContent), findsOneWidget);
});
});
}

View File

@@ -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<RegistrationCubitV2>()])
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>(() => 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<void>.value());
await tester.pumpWidget(BlocProvider<RegistrationCubitV2>.value(value: cubit, child: wrap(const SecondStepContent())));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(SecondStepContent), findsOneWidget);
});
});
}

View File

@@ -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>(() => 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);
});
});
}

View File

@@ -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<ProfileRepository>(),
MockSpec<SettingsRepository>(),
MockSpec<ConsultantRepository>(),
MockSpec<ConsultationsRepository>(),
])
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>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
profileRepository = MockProfileRepository();
settingsRepository = MockSettingsRepository();
consultantRepository = MockConsultantRepository();
consultationsRepository = MockConsultationsRepository();
sl.registerLazySingleton<ProfileRepository>(() => profileRepository);
sl.registerLazySingleton<SettingsRepository>(() => settingsRepository);
sl.registerLazySingleton<ConsultantRepository>(() => consultantRepository);
sl.registerLazySingleton<ConsultationsRepository>(() => consultationsRepository);
sl.registerFactory<SettingsCubit>(
() => 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);
});
});
}

View File

@@ -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<ProfileRepository>()])
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>(() => 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>(() => profileRepository);
sl.registerLazySingleton<UserDetailsCubit>(() => 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>(() => profileRepository);
sl.registerLazySingleton<UserDetailsCubit>(() => 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>(() => profileRepository);
sl.registerLazySingleton<UserDetailsCubit>(() => cubit);
await tester.pumpWidget(wrap(const UserDetailsScreen(slug: 'test-slug')));
await tester.pumpAndSettle();
expect(find.byType(UserDetailsScreen), findsOneWidget);
});
});
}