forked from eatvio/eatvio_app
feat(feed): fixed comments state updating in feed, feat(consultantRegistration): fixed registration flow for users with unverified phone, feat(tests): fixed tests coverage
This commit is contained in:
@@ -74,7 +74,10 @@ class AppRouter {
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-diet-screen?slug=$contentIdentifier';
|
||||
}
|
||||
final initialTabIndex = hidePlans ? 3 : 4;
|
||||
const dietTabIndexWithHiddenPlansTab = 3;
|
||||
const dietTabIndexWithoutHiddenPlansTab = 4;
|
||||
|
||||
final initialTabIndex = hidePlans ? dietTabIndexWithHiddenPlansTab : dietTabIndexWithoutHiddenPlansTab;
|
||||
return '${RouteConstants.feedScreen}?initialTabIndex=$initialTabIndex';
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
final FeedRepository feedRepository;
|
||||
final CommentsRepository commentsRepository;
|
||||
|
||||
CommentsCubit({
|
||||
required this.feedRepository,
|
||||
required this.commentsRepository,
|
||||
}) : super(const CommentsState());
|
||||
CommentsCubit({required this.feedRepository, required this.commentsRepository}) : super(const CommentsState());
|
||||
|
||||
void setReplyId(int? replyId) {
|
||||
emit((state as CommentsStateLoaded).copyWith(replyId: replyId));
|
||||
@@ -41,46 +38,27 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendComment({
|
||||
required String id,
|
||||
required String commentText,
|
||||
required CommentSourceType commentSourceType,
|
||||
int? parentCommentId,
|
||||
}) async {
|
||||
Future<bool> sendComment({required String id, required String commentText, required CommentSourceType commentSourceType, int? parentCommentId}) async {
|
||||
try {
|
||||
switch (commentSourceType) {
|
||||
case CommentSourceType.blogPost:
|
||||
await feedRepository.sendBlogPostComment(
|
||||
id: id,
|
||||
commentText: commentText,
|
||||
parentCommentId: parentCommentId,
|
||||
);
|
||||
await feedRepository.sendBlogPostComment(id: id, commentText: commentText, parentCommentId: parentCommentId);
|
||||
break;
|
||||
case CommentSourceType.diet:
|
||||
await feedRepository.sendDietsComment(
|
||||
id: id,
|
||||
commentText: commentText,
|
||||
parentCommentId: parentCommentId,
|
||||
);
|
||||
await feedRepository.sendDietsComment(id: id, commentText: commentText, parentCommentId: parentCommentId);
|
||||
break;
|
||||
case CommentSourceType.recipe:
|
||||
await feedRepository.sendRecipesComment(
|
||||
id: id,
|
||||
commentText: commentText,
|
||||
parentCommentId: parentCommentId,
|
||||
);
|
||||
await feedRepository.sendRecipesComment(id: id, commentText: commentText, parentCommentId: parentCommentId);
|
||||
break;
|
||||
case CommentSourceType.plan:
|
||||
await feedRepository.sendPlansComment(
|
||||
slug: id,
|
||||
commentText: commentText,
|
||||
parentCommentId: parentCommentId,
|
||||
);
|
||||
await feedRepository.sendPlansComment(slug: id, commentText: commentText, parentCommentId: parentCommentId);
|
||||
break;
|
||||
}
|
||||
getComments(id, commentSourceType);
|
||||
return true;
|
||||
} catch (e) {
|
||||
emit(CommentsState.error(errorMessage: e.toString()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,8 +79,9 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
Future<bool> updateComment({required int id, required String body}) async {
|
||||
final commentModel = (state as CommentsStateLoaded).comment;
|
||||
final updatedCommentModel =
|
||||
commentModel.copyWith(data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList());
|
||||
final updatedCommentModel = commentModel.copyWith(
|
||||
data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList(),
|
||||
);
|
||||
try {
|
||||
await commentsRepository.updateComment(id: id, body: body);
|
||||
emit(CommentsState.loaded(comment: updatedCommentModel));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -12,18 +13,21 @@ class PhoneVerifyErrorInterceptor extends Interceptor {
|
||||
final data = err.response?.data;
|
||||
|
||||
if (data is Map<String, dynamic> && data['type'] == 'unverificated_phone') {
|
||||
final String phone = await sl<ProfileRepository>().getMyPhone();
|
||||
final String phone = await sl<ProfileRepository>().getMyPhone();
|
||||
final String passKey = await sl<ProfileRepository>().getPassKey(routeName: 'user_detailz.change-phone-verify', routeParams: {"phone": phone});
|
||||
await showErrorDialog(AppRouter.navigatorKey.currentContext!,
|
||||
title: 'Привяжите номер телефона', description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона');
|
||||
await showErrorDialog(
|
||||
AppRouter.navigatorKey.currentContext!,
|
||||
title: 'Привяжите номер телефона',
|
||||
description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона',
|
||||
);
|
||||
final currentContext = AppRouter.navigatorKey.currentContext;
|
||||
|
||||
if (currentContext != null && currentContext.mounted && GoRouter.of(currentContext).state.path == RouteConstants.stepperBecomeConsultantScreen) {
|
||||
GoRouter.of(currentContext).pop();
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.pushNamed('verifyScreen', extra: passKey);
|
||||
return handler.reject(
|
||||
DioException(
|
||||
requestOptions: err.requestOptions,
|
||||
response: err.response,
|
||||
error: 'Phone number not verified',
|
||||
type: DioExceptionType.badResponse,
|
||||
),
|
||||
DioException(requestOptions: err.requestOptions, response: err.response, error: 'Phone number not verified', type: DioExceptionType.badResponse),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ class MainNavigationRoutes {
|
||||
path: RouteConstants.feedScreen,
|
||||
name: 'feedScreen',
|
||||
pageBuilder: (context, state) {
|
||||
final initialTabIndex = int.tryParse(state.uri.queryParameters['initialTabIndex'] ?? '0');
|
||||
return NoTransitionPage(child: FeedScreen(initialTabIndex: initialTabIndex ?? 0));
|
||||
final initialTabIndex = int.tryParse(state.uri.queryParameters['initialTabIndex'] ?? '');
|
||||
return NoTransitionPage(child: FeedScreen(initialTabIndex: initialTabIndex));
|
||||
},
|
||||
redirect: (context, state) async {
|
||||
try {
|
||||
|
||||
@@ -78,9 +78,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
bottomSheet: Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||
@@ -147,14 +145,18 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
if (textEditingController.text.length > 2) {
|
||||
commentsCubit.sendComment(
|
||||
final isSent = await commentsCubit.sendComment(
|
||||
id: widget.id.toString(),
|
||||
commentText: textEditingController.text,
|
||||
parentCommentId: replyId,
|
||||
commentSourceType: CommentSourceType.blogPost,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (isSent) context.read<FeedCubit>().incrementCommentsState(widget.id);
|
||||
|
||||
context.read<CommentsCubit>().setReplyId(null);
|
||||
setState(() {
|
||||
textEditingController.clear();
|
||||
@@ -237,9 +239,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: MarkdownHtmlWidget(
|
||||
markdownText: blogPostState.blogPostModel!.description,
|
||||
),
|
||||
child: MarkdownHtmlWidget(markdownText: blogPostState.blogPostModel!.description),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
|
||||
@@ -20,45 +21,53 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
||||
);
|
||||
|
||||
Future<void> getMyRequestStatus(String slug) async {
|
||||
_update = true;
|
||||
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
|
||||
final certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
try {
|
||||
_update = true;
|
||||
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
|
||||
final certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
|
||||
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
|
||||
final course = courseMap.values.first;
|
||||
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
|
||||
final course = courseMap.values.first;
|
||||
|
||||
return CertificatesModel(title: course.title, date: course.date);
|
||||
}).toList();
|
||||
return CertificatesModel(title: course.title, date: course.date);
|
||||
}).toList();
|
||||
|
||||
String? sanitizeLink(String? link) {
|
||||
return (link != null && link.isNotEmpty) ? link : null;
|
||||
}
|
||||
String? sanitizeLink(String? link) {
|
||||
return (link != null && link.isNotEmpty) ? link : null;
|
||||
}
|
||||
|
||||
emit(
|
||||
RegisterConsultantState(
|
||||
consultant: ConsultantProfileModel(
|
||||
certificates: certificates,
|
||||
socialLinks: SocialLinksModel(
|
||||
vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink),
|
||||
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
|
||||
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
|
||||
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
|
||||
personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink),
|
||||
emit(
|
||||
RegisterConsultantState(
|
||||
consultant: ConsultantProfileModel(
|
||||
certificates: certificates,
|
||||
socialLinks: SocialLinksModel(
|
||||
vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink),
|
||||
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
|
||||
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
|
||||
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
|
||||
personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink),
|
||||
),
|
||||
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1,
|
||||
),
|
||||
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1,
|
||||
acceptOffer: false,
|
||||
loading: false,
|
||||
requestModel: myRequestStatus,
|
||||
certPhotos: certPhotos,
|
||||
isUpdating: _update,
|
||||
),
|
||||
acceptOffer: false,
|
||||
loading: false,
|
||||
requestModel: myRequestStatus,
|
||||
certPhotos: certPhotos,
|
||||
isUpdating: _update
|
||||
),
|
||||
);
|
||||
);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disableLoading() async {
|
||||
final certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
emit(state.copyWith(loading: false, certPhotos: certPhotos));
|
||||
try {
|
||||
final certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
emit(state.copyWith(loading: false, certPhotos: certPhotos));
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {
|
||||
|
||||
@@ -17,18 +17,13 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
|
||||
title: const Text('Стать консультантом'),
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: false,
|
||||
leading: IconButton(
|
||||
onPressed: () => GoRouter.of(context).pop(),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
leading: IconButton(onPressed: () => GoRouter.of(context).pop(), icon: const Icon(Icons.close)),
|
||||
),
|
||||
body: const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Чтобы стать консультантом необходимо пройти короткую регистрацию. Подготовьте свои дипломы и/или сертификаты в формате jpg, они Вам пригодятся для проверки и подтверждения. Также необходимо будет указать ссылки на Ваши соц. сети и указать тип лица (физ. лицо или юр. лицо). На данный момент доступна регистрация в качестве физ. лица.\n\nВнутри Итвио разница между обычным пользователем и консультантом заключается в дополнительных возможностях последнего создавать публичные диеты, рецепты и планы питания; участвовать в ежемесячных рейтингах.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
bottomSheet: _startRegistrationButton(),
|
||||
@@ -37,24 +32,16 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
|
||||
|
||||
InkWell _startRegistrationButton() {
|
||||
return InkWell(
|
||||
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen'),
|
||||
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen', extra: {'slug': null}),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
height: 52,
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Начать регистрацию',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
style: TextStyle(color: Colors.white, height: 1, fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -26,6 +26,17 @@ class FeedCubit extends Cubit<FeedState> {
|
||||
}
|
||||
}
|
||||
|
||||
void incrementCommentsState(int id) {
|
||||
final List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
|
||||
final int index = feedBlogPostList.indexWhere((e) => e.id == id);
|
||||
if (index == -1) return;
|
||||
final current = feedBlogPostList[index];
|
||||
feedBlogPostList[index] = current.copyWith(totalComments: current.totalComments + 1);
|
||||
if (!isClosed) {
|
||||
emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadFeedBlogPostList({String? query}) async {
|
||||
try {
|
||||
_query = query;
|
||||
|
||||
@@ -33,7 +33,7 @@ import '../widgets/recipes.dart';
|
||||
|
||||
class FeedScreen extends StatelessWidget {
|
||||
const FeedScreen({super.key, this.initialTabIndex = 0});
|
||||
final int initialTabIndex;
|
||||
final int? initialTabIndex;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
@@ -51,7 +51,7 @@ class FeedScreen extends StatelessWidget {
|
||||
|
||||
class FeedScreenContent extends StatefulWidget {
|
||||
const FeedScreenContent({super.key, required this.initialTabIndex});
|
||||
final int initialTabIndex;
|
||||
final int? initialTabIndex;
|
||||
@override
|
||||
State<FeedScreenContent> createState() => _FeedScreenContentState();
|
||||
}
|
||||
@@ -72,7 +72,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
final consultantsScrollController = ScrollController();
|
||||
@override
|
||||
void initState() {
|
||||
_selectedChipIndex = widget.initialTabIndex;
|
||||
_selectedChipIndex = widget.initialTabIndex ?? 0;
|
||||
chipsLabelList = hidePlans ? ['Все', 'Консультанты', 'Рецепты', 'Диеты'] : ['Все', 'Планы питания', 'Консультанты', 'Рецепты', 'Диеты'];
|
||||
consultantsScrollController.addListener(_onScrollConsultants);
|
||||
_checkRegistrationDate(context);
|
||||
@@ -92,8 +92,11 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
@override
|
||||
void didUpdateWidget(FeedScreenContent oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final initialTabIndex = widget.initialTabIndex;
|
||||
setState(() {
|
||||
_selectedChipIndex = widget.initialTabIndex;
|
||||
if (initialTabIndex != null) {
|
||||
_selectedChipIndex = initialTabIndex;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,12 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
|
||||
final PlanRepository planRepository;
|
||||
AboutPlanCubit({required this.planRepository}) : super(AboutPlanState.initial());
|
||||
|
||||
Future<void> getPlan(String slug) async {
|
||||
Future<void> getPlan({required String slug, int? index}) async {
|
||||
try {
|
||||
final plan = await planRepository.getPlan(slug);
|
||||
if (index != null) {
|
||||
planRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: plan.totalComments));
|
||||
}
|
||||
emit(state.copyWith(planModel: plan, error: null));
|
||||
} catch (e, stackTrace) {
|
||||
log(e.toString(), stackTrace: stackTrace);
|
||||
@@ -27,13 +30,7 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final plan = await planRepository.getPlan(slug);
|
||||
emit(
|
||||
state.copyWith(
|
||||
planModel: plan,
|
||||
error: null,
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(planModel: plan, error: null, isLoading: false));
|
||||
} catch (e, stackTrace) {
|
||||
log(e.toString(), stackTrace: stackTrace);
|
||||
emit(state.copyWith(error: e.toString(), isLoading: false));
|
||||
@@ -59,21 +56,17 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
|
||||
}
|
||||
}
|
||||
|
||||
EasyDebounce.debounce(
|
||||
'like_plan_$slug',
|
||||
const Duration(seconds: 1),
|
||||
() async {
|
||||
try {
|
||||
await planRepository.likePlan(slug);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
if (index != null) {
|
||||
planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentPlan.wasLikedByMe, totalLikes: currentPlan.totalLikes, index: index));
|
||||
}
|
||||
emit(state.copyWith(planModel: currentPlan, error: null));
|
||||
EasyDebounce.debounce('like_plan_$slug', const Duration(seconds: 1), () async {
|
||||
try {
|
||||
await planRepository.likePlan(slug);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
if (index != null) {
|
||||
planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentPlan.wasLikedByMe, totalLikes: currentPlan.totalLikes, index: index));
|
||||
}
|
||||
},
|
||||
);
|
||||
emit(state.copyWith(planModel: currentPlan, error: null));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addToFavorite({required String slug, required int? index}) async {
|
||||
@@ -84,22 +77,18 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
|
||||
if (index != null) {
|
||||
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: true, index: index));
|
||||
}
|
||||
EasyDebounce.debounce(
|
||||
'add_or_remove_plan_to_favorites',
|
||||
const Duration(seconds: 1),
|
||||
() async {
|
||||
try {
|
||||
await planRepository.addToFavorite(slug);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
|
||||
if (index != null) {
|
||||
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index));
|
||||
}
|
||||
emit(state.copyWith(planModel: currentPlan, error: null));
|
||||
EasyDebounce.debounce('add_or_remove_plan_to_favorites', const Duration(seconds: 1), () async {
|
||||
try {
|
||||
await planRepository.addToFavorite(slug);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
|
||||
if (index != null) {
|
||||
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index));
|
||||
}
|
||||
},
|
||||
);
|
||||
emit(state.copyWith(planModel: currentPlan, error: null));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> removeFromFavorite({required String slug, required int? index}) async {
|
||||
@@ -110,22 +99,18 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
|
||||
if (index != null) {
|
||||
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: false, index: index));
|
||||
}
|
||||
EasyDebounce.debounce(
|
||||
'add_or_remove_plan_to_favorites',
|
||||
const Duration(seconds: 1),
|
||||
() async {
|
||||
try {
|
||||
await planRepository.removeFromFavorite(slug);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
|
||||
if (index != null) {
|
||||
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index));
|
||||
}
|
||||
emit(state.copyWith(planModel: currentPlan, error: null));
|
||||
EasyDebounce.debounce('add_or_remove_plan_to_favorites', const Duration(seconds: 1), () async {
|
||||
try {
|
||||
await planRepository.removeFromFavorite(slug);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
|
||||
if (index != null) {
|
||||
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index));
|
||||
}
|
||||
},
|
||||
);
|
||||
emit(state.copyWith(planModel: currentPlan, error: null));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<(ChosenPlanModel?, String?)> chosePlan() async {
|
||||
@@ -149,8 +134,8 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
|
||||
}
|
||||
|
||||
String _getOptionNameFromIndex(int index) => switch (index) {
|
||||
1 => 'options2',
|
||||
2 => 'options3',
|
||||
_ => 'options1',
|
||||
};
|
||||
1 => 'options2',
|
||||
2 => 'options3',
|
||||
_ => 'options1',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
body: DataErrorWidget(onReload: () => context.read<AboutPlanCubit>().getPlan(widget.slug)),
|
||||
body: DataErrorWidget(onReload: () => context.read<AboutPlanCubit>().getPlan(slug: widget.slug)),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
@@ -295,7 +295,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (context.mounted) {
|
||||
context.read<AboutPlanCubit>().getPlan(widget.slug);
|
||||
context.read<AboutPlanCubit>().getPlan(slug: widget.slug, index: widget.index);
|
||||
}
|
||||
});
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
@@ -112,5 +112,43 @@ void main() {
|
||||
expect(() => AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'vk'), returnsNormally);
|
||||
expect(() => AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'google'), returnsNormally);
|
||||
});
|
||||
|
||||
test('navigateByDeepLinkUrl handles empty and invalid urls', () {
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl(''), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://unknown-host.example/foo'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('%%%'), returnsNormally);
|
||||
});
|
||||
|
||||
test('navigateByDeepLinkUrl handles supported recipe and blog links', () {
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/recipe/test-recipe'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.com/en/blog_post/123'), returnsNormally);
|
||||
});
|
||||
|
||||
test('navigateByDeepLinkUrl handles plan links', () {
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/plan/plan-slug'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/plan/42'), returnsNormally);
|
||||
});
|
||||
|
||||
test('navigateByDeepLinkUrl handles diet links', () {
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/diet/99'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/diet/diet-slug'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/diet'), returnsNormally);
|
||||
});
|
||||
|
||||
test('navigateByDeepLinkUrl handles chat links', () {
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/chat/uuid-1'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/chat/show/uuid-2'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/chat'), returnsNormally);
|
||||
});
|
||||
|
||||
test('navigateByDeepLinkUrl handles profile and misc links', () {
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/profile/some-user'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/profile'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/badge/1'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/diary'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/consultant_diary_comment/7'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/consultations/status/11'), returnsNormally);
|
||||
expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/consultations'), returnsNormally);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -205,5 +205,54 @@ void main() {
|
||||
expect(find.text('Test Post Title'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('loaded state tapping like invokes toggleLike', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final blogPostCubit = MockBlogPostCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
final blogPostModel = FeedBlogPostModel(
|
||||
id: 1,
|
||||
title: 'Test Post Title',
|
||||
description: 'Test post description',
|
||||
image: 'https://example.com/image.jpg',
|
||||
state: 'published',
|
||||
author: const Author(authorSlug: 'author-1', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
|
||||
videoUrl: '',
|
||||
createdAt: DateTime(2024, 1, 15),
|
||||
updatedAt: DateTime(2024, 1, 15),
|
||||
totalLikes: 10,
|
||||
totalViews: 100,
|
||||
totalComments: 5,
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
|
||||
|
||||
when(blogPostCubit.state).thenReturn(state);
|
||||
when(blogPostCubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
when(blogPostCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(blogPostCubit.getBlogPost(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(blogPostCubit.toggleLike()).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewPost(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.updateFeedBlogPostList(any, any)).thenReturn(null);
|
||||
when(commentsCubit.state).thenReturn(const CommentsState());
|
||||
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
|
||||
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
registerMocks(blogPostCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, const BlogPostScreen(id: 1)));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byIcon(Icons.favorite_border_rounded));
|
||||
await tester.pump();
|
||||
|
||||
verify(blogPostCubit.toggleLike()).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/models/pagination_model.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/models/ingredient.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart';
|
||||
@@ -8,31 +13,123 @@ import 'package:eatvio_app/features/create_recipe/models/step.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
class MockCreateRecipeService extends Mock implements CreateRecipeService {}
|
||||
|
||||
final mockHttpResponse = HttpResponse(
|
||||
null,
|
||||
Response(requestOptions: RequestOptions(path: ''), statusCode: 200),
|
||||
);
|
||||
final mockHttpResponse = HttpResponse(null, Response(requestOptions: RequestOptions(path: ''), statusCode: 200));
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
||||
if (sl.isRegistered<AppConfig>()) {
|
||||
sl.unregister<AppConfig>();
|
||||
}
|
||||
sl.registerSingleton<AppConfig>(AppConfig(buildType: AppBuildType.tests));
|
||||
registerFallbackValue(FormData());
|
||||
});
|
||||
|
||||
group('RecipeRepository', () {
|
||||
late RecipeRepository repository;
|
||||
late MockCreateRecipeService mockService;
|
||||
late Directory tempDir;
|
||||
late File recipeImage;
|
||||
late File stepImage;
|
||||
|
||||
setUp(() {
|
||||
mockService = MockCreateRecipeService();
|
||||
repository = RecipeRepository(recipeService: mockService);
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
tempDir = await Directory.systemTemp.createTemp('eatvio_recipe_repo_test_');
|
||||
recipeImage = File('${tempDir.path}/recipe.png');
|
||||
stepImage = File('${tempDir.path}/step.png');
|
||||
|
||||
// Valid 1x1 transparent PNG to satisfy ImageCompressUtility decoding.
|
||||
const png1x1 = <int>[
|
||||
0x89,
|
||||
0x50,
|
||||
0x4E,
|
||||
0x47,
|
||||
0x0D,
|
||||
0x0A,
|
||||
0x1A,
|
||||
0x0A,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0D,
|
||||
0x49,
|
||||
0x48,
|
||||
0x44,
|
||||
0x52,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x08,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1F,
|
||||
0x15,
|
||||
0xC4,
|
||||
0x89,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0A,
|
||||
0x49,
|
||||
0x44,
|
||||
0x41,
|
||||
0x54,
|
||||
0x78,
|
||||
0x9C,
|
||||
0x63,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x01,
|
||||
0x0D,
|
||||
0x0A,
|
||||
0x2D,
|
||||
0xB4,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x49,
|
||||
0x45,
|
||||
0x4E,
|
||||
0x44,
|
||||
0xAE,
|
||||
0x42,
|
||||
0x60,
|
||||
0x82,
|
||||
];
|
||||
|
||||
await recipeImage.writeAsBytes(png1x1, flush: true);
|
||||
await stepImage.writeAsBytes(png1x1, flush: true);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
if (tempDir.existsSync()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
test('inputProduct - возвращает Map с продуктами', () async {
|
||||
const name = 'test-product';
|
||||
const expected = {
|
||||
@@ -177,12 +274,12 @@ void main() {
|
||||
|
||||
test('updateRecipe - вызывает updateRecipe с правильными параметрами', () async {
|
||||
const slug = 'recipe-slug';
|
||||
const recipe = RecipeModel(
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Updated Recipe',
|
||||
description: 'Updated description',
|
||||
videoUrl: null,
|
||||
image: '/test/path/image.jpg',
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: null,
|
||||
tags: null,
|
||||
@@ -192,27 +289,29 @@ void main() {
|
||||
energyPerServing: 300,
|
||||
);
|
||||
|
||||
// Мокируем ImageCompressUtility.compressFile
|
||||
// Это сложно, так как это статический метод, но мы можем проверить, что вызывается updateRecipe
|
||||
when(() => mockService.updateRecipe(slug: slug, formData: any(named: 'formData')))
|
||||
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
when(
|
||||
() => mockService.updateRecipe(
|
||||
slug: slug,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
// Этот тест может упасть из-за ImageCompressUtility, но мы проверяем логику
|
||||
try {
|
||||
await repository.publishRecipe(recipe, true, slug);
|
||||
verify(() => mockService.updateRecipe(slug: slug, formData: any(named: 'formData'))).called(1);
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
|
||||
}
|
||||
await repository.publishRecipe(recipe, true, slug);
|
||||
verify(
|
||||
() => mockService.updateRecipe(
|
||||
slug: slug,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('publishRecipe - обрабатывает рецепт с тегами', () async {
|
||||
const recipe = RecipeModel(
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Test Recipe',
|
||||
description: 'Test description',
|
||||
videoUrl: null,
|
||||
image: '/test/path/image.jpg',
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: null,
|
||||
tags: ['tag1', 'tag2'],
|
||||
@@ -222,24 +321,19 @@ void main() {
|
||||
energyPerServing: 300,
|
||||
);
|
||||
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
try {
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
|
||||
}
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('publishRecipe - обрабатывает рецепт с videoUrl', () async {
|
||||
const recipe = RecipeModel(
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Test Recipe',
|
||||
description: 'Test description',
|
||||
videoUrl: 'https://example.com/video.mp4',
|
||||
image: '/test/path/image.jpg',
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: null,
|
||||
tags: null,
|
||||
@@ -249,24 +343,19 @@ void main() {
|
||||
energyPerServing: 300,
|
||||
);
|
||||
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
try {
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
|
||||
}
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('publishRecipe - обрабатывает рецепт без videoUrl (null)', () async {
|
||||
const recipe = RecipeModel(
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Test Recipe',
|
||||
description: 'Test description',
|
||||
videoUrl: null,
|
||||
image: '/test/path/image.jpg',
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: null,
|
||||
tags: null,
|
||||
@@ -276,24 +365,19 @@ void main() {
|
||||
energyPerServing: 300,
|
||||
);
|
||||
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
try {
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
|
||||
}
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('publishRecipe - обрабатывает рецепт с пустым videoUrl', () async {
|
||||
const recipe = RecipeModel(
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Test Recipe',
|
||||
description: 'Test description',
|
||||
videoUrl: '',
|
||||
image: '/test/path/image.jpg',
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: null,
|
||||
tags: null,
|
||||
@@ -303,24 +387,19 @@ void main() {
|
||||
energyPerServing: 300,
|
||||
);
|
||||
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
try {
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
|
||||
}
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('publishRecipe - пробрасывает исключение при ошибке', () async {
|
||||
const recipe = RecipeModel(
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Test Recipe',
|
||||
description: 'Test description',
|
||||
videoUrl: null,
|
||||
image: '/test/path/image.jpg',
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: null,
|
||||
tags: null,
|
||||
@@ -330,37 +409,21 @@ void main() {
|
||||
energyPerServing: 300,
|
||||
);
|
||||
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||
.thenThrow(Exception('Error'));
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenThrow(Exception('Error'));
|
||||
|
||||
try {
|
||||
await repository.publishRecipe(recipe);
|
||||
fail('Should have thrown exception');
|
||||
} catch (e) {
|
||||
expect(e, isA<Exception>());
|
||||
}
|
||||
expect(() => repository.publishRecipe(recipe), throwsA(isA<Exception>()));
|
||||
});
|
||||
|
||||
test('publishRecipe - обрабатывает рецепт с ингредиентами', () async {
|
||||
const recipe = RecipeModel(
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Test Recipe',
|
||||
description: 'Test description',
|
||||
videoUrl: null,
|
||||
image: '/test/path/image.jpg',
|
||||
image: recipeImage.path,
|
||||
ingredients: [
|
||||
Ingredient(
|
||||
name: 'Apple',
|
||||
unit: '100',
|
||||
portion: 'grams',
|
||||
productId: 1,
|
||||
),
|
||||
Ingredient(
|
||||
name: 'Banana',
|
||||
unit: '50',
|
||||
portion: 'grams',
|
||||
productId: 2,
|
||||
),
|
||||
const Ingredient(name: 'Apple', unit: '100', portion: 'grams', productId: 1),
|
||||
const Ingredient(name: 'Banana', unit: '50', portion: 'grams', productId: 2),
|
||||
],
|
||||
steps: null,
|
||||
tags: null,
|
||||
@@ -370,31 +433,21 @@ void main() {
|
||||
energyPerServing: 300,
|
||||
);
|
||||
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
try {
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
|
||||
}
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('publishRecipe - обрабатывает рецепт с null ингредиентами', () async {
|
||||
const recipe = RecipeModel(
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Test Recipe',
|
||||
description: 'Test description',
|
||||
videoUrl: null,
|
||||
image: '/test/path/image.jpg',
|
||||
image: recipeImage.path,
|
||||
ingredients: [
|
||||
Ingredient(
|
||||
name: 'Apple',
|
||||
unit: '100',
|
||||
portion: 'grams',
|
||||
productId: 1,
|
||||
),
|
||||
const Ingredient(name: 'Apple', unit: '100', portion: 'grams', productId: 1),
|
||||
null, // null ингредиент должен быть отфильтрован
|
||||
],
|
||||
steps: null,
|
||||
@@ -405,34 +458,23 @@ void main() {
|
||||
energyPerServing: 300,
|
||||
);
|
||||
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
try {
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
|
||||
}
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('publishRecipe - обрабатывает рецепт со шагами без изображений', () async {
|
||||
const recipe = RecipeModel(
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Test Recipe',
|
||||
description: 'Test description',
|
||||
videoUrl: null,
|
||||
image: '/test/path/image.jpg',
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: [
|
||||
Step(
|
||||
description: 'Step 1 description',
|
||||
image: null,
|
||||
),
|
||||
Step(
|
||||
description: 'Step 2 description',
|
||||
image: '',
|
||||
),
|
||||
const Step(description: 'Step 1 description', image: null),
|
||||
const Step(description: 'Step 2 description', image: ''),
|
||||
],
|
||||
tags: null,
|
||||
totalCookingTime: 30,
|
||||
@@ -441,35 +483,24 @@ void main() {
|
||||
energyPerServing: 300,
|
||||
);
|
||||
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
try {
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
|
||||
}
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('publishRecipe - обрабатывает рецепт со смешанными шагами (с null)', () async {
|
||||
const recipe = RecipeModel(
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Test Recipe',
|
||||
description: 'Test description',
|
||||
videoUrl: null,
|
||||
image: '/test/path/image.jpg',
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: [
|
||||
Step(
|
||||
description: 'Step 1 description',
|
||||
image: null,
|
||||
),
|
||||
const Step(description: 'Step 1 description', image: null),
|
||||
null, // null шаг должен быть отфильтрован
|
||||
Step(
|
||||
description: 'Step 3 description',
|
||||
image: '',
|
||||
),
|
||||
const Step(description: 'Step 3 description', image: ''),
|
||||
],
|
||||
tags: null,
|
||||
totalCookingTime: 30,
|
||||
@@ -478,24 +509,19 @@ void main() {
|
||||
energyPerServing: 300,
|
||||
);
|
||||
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
try {
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
|
||||
}
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('publishRecipe - удаляет пустые значения из formMap', () async {
|
||||
const recipe = RecipeModel(
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Test Recipe',
|
||||
description: '', // Пустое описание должно быть удалено
|
||||
videoUrl: '', // Пустой videoUrl должен быть удален
|
||||
image: '/test/path/image.jpg',
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: null,
|
||||
tags: null,
|
||||
@@ -505,25 +531,20 @@ void main() {
|
||||
energyPerServing: null,
|
||||
);
|
||||
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
try {
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки, связанные с ImageCompressUtility в тестах
|
||||
}
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('updateRecipe - пробрасывает исключение при ошибке', () async {
|
||||
const slug = 'recipe-slug';
|
||||
const recipe = RecipeModel(
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Test Recipe',
|
||||
description: 'Test description',
|
||||
videoUrl: null,
|
||||
image: '/test/path/image.jpg',
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: null,
|
||||
tags: null,
|
||||
@@ -533,16 +554,132 @@ void main() {
|
||||
energyPerServing: 300,
|
||||
);
|
||||
|
||||
when(() => mockService.updateRecipe(slug: slug, formData: any(named: 'formData')))
|
||||
.thenThrow(Exception('Error'));
|
||||
when(
|
||||
() => mockService.updateRecipe(
|
||||
slug: slug,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).thenThrow(Exception('Error'));
|
||||
|
||||
try {
|
||||
await repository.publishRecipe(recipe, true, slug);
|
||||
fail('Should have thrown exception');
|
||||
} catch (e) {
|
||||
expect(e, isA<Exception>());
|
||||
}
|
||||
expect(() => repository.publishRecipe(recipe, true, slug), throwsA(isA<Exception>()));
|
||||
});
|
||||
|
||||
test('publishRecipe - собирает steps с изображением (MultipartFile)', () async {
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Test Recipe',
|
||||
description: 'Test description',
|
||||
videoUrl: null,
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: [Step(description: 'Step with image', image: stepImage.path)],
|
||||
tags: null,
|
||||
totalCookingTime: 30,
|
||||
activeCookTime: 15,
|
||||
servings: 4,
|
||||
energyPerServing: 300,
|
||||
);
|
||||
|
||||
when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
await repository.publishRecipe(recipe);
|
||||
verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('saveAsDraft - создает draft (createDraftedRecipe)', () async {
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Draft title',
|
||||
description: 'Draft desc',
|
||||
videoUrl: null,
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: null,
|
||||
tags: const ['tag1'],
|
||||
totalCookingTime: 30,
|
||||
activeCookTime: 15,
|
||||
servings: 2,
|
||||
energyPerServing: 200,
|
||||
);
|
||||
|
||||
when(() => mockService.createDraftedRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
await repository.saveAsDraft(recipe);
|
||||
verify(() => mockService.createDraftedRecipe(formData: any(named: 'formData'))).called(1);
|
||||
verifyNever(
|
||||
() => mockService.updateRecipe(
|
||||
slug: any(named: 'slug'),
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('saveAsDraft - обновляет draft (updateRecipe)', () async {
|
||||
const slug = 'draft-slug';
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Draft title',
|
||||
description: 'Draft desc',
|
||||
videoUrl: null,
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: null,
|
||||
tags: null,
|
||||
totalCookingTime: 30,
|
||||
activeCookTime: 15,
|
||||
servings: 2,
|
||||
energyPerServing: 200,
|
||||
);
|
||||
|
||||
when(
|
||||
() => mockService.updateRecipe(
|
||||
slug: slug,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||
|
||||
await repository.saveAsDraft(recipe, true, slug);
|
||||
verify(
|
||||
() => mockService.updateRecipe(
|
||||
slug: slug,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('saveAsDraft - update=true без slug кидает исключение', () async {
|
||||
final recipe = RecipeModel(
|
||||
type: 'main',
|
||||
title: 'Draft title',
|
||||
description: 'Draft desc',
|
||||
videoUrl: null,
|
||||
image: recipeImage.path,
|
||||
ingredients: null,
|
||||
steps: null,
|
||||
tags: null,
|
||||
totalCookingTime: 30,
|
||||
activeCookTime: 15,
|
||||
servings: 2,
|
||||
energyPerServing: 200,
|
||||
);
|
||||
|
||||
// slug! должен упасть
|
||||
expect(() => repository.saveAsDraft(recipe, true, null), throwsA(anything));
|
||||
});
|
||||
|
||||
test('draftExistingRecipe - вызывает draftRecipe', () async {
|
||||
const id = 123;
|
||||
when(() => mockService.draftRecipe(id: id)).thenAnswer((_) async => Future.value());
|
||||
|
||||
await repository.draftExistingRecipe(id);
|
||||
verify(() => mockService.draftRecipe(id: id)).called(1);
|
||||
});
|
||||
|
||||
test('draftExistingRecipe - пробрасывает исключение при ошибке', () async {
|
||||
const id = 123;
|
||||
when(() => mockService.draftRecipe(id: id)).thenThrow(Exception('Error'));
|
||||
|
||||
expect(() => repository.draftExistingRecipe(id), throwsA(isA<Exception>()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -16,6 +17,7 @@ void main() {
|
||||
setUp(() async {
|
||||
await sl.reset();
|
||||
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
||||
sl.registerLazySingleton<Dio>(() => Dio(BaseOptions(baseUrl: 'http://localhost')));
|
||||
});
|
||||
|
||||
Widget wrap(Widget child) {
|
||||
@@ -39,5 +41,29 @@ void main() {
|
||||
|
||||
expect(find.byType(DevToolsScreenContent), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('updates base url from input by done button', (tester) async {
|
||||
await tester.pumpWidget(wrap(const DevToolsScreen()));
|
||||
await tester.pump();
|
||||
|
||||
await tester.enterText(find.byType(TextFormField).first, 'HTTP://EXAMPLE.COM');
|
||||
await tester.tap(find.byIcon(Icons.done));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(DevToolsScreenContent), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('toggle base url button is tappable', (tester) async {
|
||||
await tester.pumpWidget(wrap(const DevToolsScreen()));
|
||||
await tester.pump();
|
||||
|
||||
final toggleButton = find.text('Set production base url');
|
||||
expect(toggleButton, findsOneWidget);
|
||||
|
||||
await tester.tap(toggleButton);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(DevToolsScreenContent), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
107
test/features/dev_tools/test_cases_bulk_test.dart
Normal file
107
test/features/dev_tools/test_cases_bulk_test.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'package:eatvio_app/features/dev_tools/test_cases.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('DevTools TestCases bulk', () {
|
||||
test('all case names are readable', () {
|
||||
final cases = <TestCases>[
|
||||
CreateRecipeTask(),
|
||||
Test850task(),
|
||||
GetChatRoomPreviews(),
|
||||
GetChatRoom(),
|
||||
SendMessage(),
|
||||
GetChatMessages(),
|
||||
GetChatRoomIdByUserSlug(),
|
||||
GetBlogPostList(),
|
||||
GetRecipes(),
|
||||
GetDiets(),
|
||||
GetPlans(),
|
||||
GetBlogPostComments(),
|
||||
SendBlogPostComment(),
|
||||
GetDietsComments(),
|
||||
SendDietsComment(),
|
||||
GetPlansComments(),
|
||||
SendPlansComment(),
|
||||
GetRecipesComments(),
|
||||
AuthServiceTest(),
|
||||
DiaryServiceTest(),
|
||||
BlockedUserServiceTest(),
|
||||
PlanServiceTest(),
|
||||
AboutRecipeServiceTest(),
|
||||
BlogPostServiceTest(),
|
||||
CalendarServiceTest(),
|
||||
CardsServiceTest(),
|
||||
ConsultantServiceTest(),
|
||||
ConsultationServiceTest(),
|
||||
CreateDietServiceTest(),
|
||||
CreatePlanServiceTest(),
|
||||
GlossaryServiceTest(),
|
||||
MyDietsServiceTest(),
|
||||
MyPlansServiceTest(),
|
||||
MyRecipesServiceTest(),
|
||||
NotificationsServiceTest(),
|
||||
PlansServiceTest(),
|
||||
ProfileServiceTest(),
|
||||
RegistrationServiceTest(),
|
||||
ResetPasswordServiceTest(),
|
||||
SettingsServiceTest(),
|
||||
WsServiceTest(),
|
||||
];
|
||||
|
||||
for (final c in cases) {
|
||||
expect(c.name, isNotEmpty);
|
||||
}
|
||||
});
|
||||
|
||||
test('run() does not throw for all cases', () async {
|
||||
final cases = <TestCases>[
|
||||
CreateRecipeTask(),
|
||||
Test850task(),
|
||||
GetChatRoomPreviews(),
|
||||
GetChatRoom(),
|
||||
SendMessage(),
|
||||
GetChatMessages(),
|
||||
GetChatRoomIdByUserSlug(),
|
||||
GetBlogPostList(),
|
||||
GetRecipes(),
|
||||
GetDiets(),
|
||||
GetPlans(),
|
||||
GetBlogPostComments(),
|
||||
SendBlogPostComment(),
|
||||
GetDietsComments(),
|
||||
SendDietsComment(),
|
||||
GetPlansComments(),
|
||||
SendPlansComment(),
|
||||
GetRecipesComments(),
|
||||
AuthServiceTest(),
|
||||
DiaryServiceTest(),
|
||||
BlockedUserServiceTest(),
|
||||
PlanServiceTest(),
|
||||
AboutRecipeServiceTest(),
|
||||
BlogPostServiceTest(),
|
||||
CalendarServiceTest(),
|
||||
CardsServiceTest(),
|
||||
ConsultantServiceTest(),
|
||||
ConsultationServiceTest(),
|
||||
CreateDietServiceTest(),
|
||||
CreatePlanServiceTest(),
|
||||
GlossaryServiceTest(),
|
||||
MyDietsServiceTest(),
|
||||
MyPlansServiceTest(),
|
||||
MyRecipesServiceTest(),
|
||||
NotificationsServiceTest(),
|
||||
PlansServiceTest(),
|
||||
ProfileServiceTest(),
|
||||
RegistrationServiceTest(),
|
||||
ResetPasswordServiceTest(),
|
||||
SettingsServiceTest(),
|
||||
WsServiceTest(),
|
||||
];
|
||||
|
||||
for (final c in cases) {
|
||||
final result = await c.run(baseUrl: 'http://localhost');
|
||||
expect(result, isA<TestResult>());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'package:eatvio_app/features/notifications/models/notification_model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// Smoke coverage for [fromJson] factories not exercised elsewhere — improves line coverage on large model file.
|
||||
void main() {
|
||||
const c = '2024-06-01T12:00:00Z';
|
||||
final base = <String, dynamic>{'created_at': c};
|
||||
|
||||
final planAndUser = <String, dynamic>{'created_at': c, 'plan_name': 'p', 'user_name': 'u', 'renewal_date': '2024-06-02'};
|
||||
|
||||
final scoreLike = <String, dynamic>{
|
||||
'created_at': c,
|
||||
'score_key': 'k',
|
||||
'scorable_id': 1,
|
||||
'scorable_type': 'Type',
|
||||
'score_value': 2,
|
||||
};
|
||||
|
||||
final commentReply = <String, dynamic>{
|
||||
'created_at': c,
|
||||
'entity_name': 'e',
|
||||
'commented_entity_id': 1,
|
||||
'commented_entity_type': 'diet',
|
||||
'author': 'a',
|
||||
'author_id': 2,
|
||||
'parrent_comment_id': 3,
|
||||
};
|
||||
|
||||
final entityCommented = <String, dynamic>{
|
||||
'created_at': c,
|
||||
'entity_name': 'e',
|
||||
'entity_id': 1,
|
||||
'entity_type': 'diet',
|
||||
'entity_traslated_type': 't',
|
||||
'entity_url': 'u',
|
||||
'author_name': 'a',
|
||||
'author_id': 2,
|
||||
'author_slug': 's',
|
||||
};
|
||||
|
||||
group('CommentedEntityType.fromJson', () {
|
||||
test('maps known and legacy keys', () {
|
||||
expect(CommentedEntityType.fromJson('diet'), CommentedEntityType.diet);
|
||||
expect(CommentedEntityType.fromJson('Modules\\UserGeneratedPublicContent\\Models\\Diet'), CommentedEntityType.diet);
|
||||
expect(CommentedEntityType.fromJson('blog-post'), CommentedEntityType.blogPost);
|
||||
expect(CommentedEntityType.fromJson('Modules\\UserGeneratedPublicContent\\Models\\BlogPost'), CommentedEntityType.blogPost);
|
||||
expect(CommentedEntityType.fromJson('other'), CommentedEntityType.unknown);
|
||||
});
|
||||
});
|
||||
|
||||
group('notification fromJson bulk smoke', () {
|
||||
test('executes remaining model factories with minimal JSON', () {
|
||||
PlanSubscriptionExpiredNotification.fromJson(planAndUser, NotificationType.planSubscriptionExpired);
|
||||
PlanSubscriptionRenewedNotification.fromJson(planAndUser, NotificationType.planSubscriptionRenewed);
|
||||
ChosenPlanStartedToConsultantNotification.fromJson(base, NotificationType.chosenPlanStartedToConsultant);
|
||||
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(base, NotificationType.chosenPlanTomorrowReminderToConsultant);
|
||||
PlanSubscriptionPaidToAuthorNotification.fromJson(base, NotificationType.planSubscriptionPaidToAuthor);
|
||||
ChosenPlanFinishedToConsultantNotification.fromJson(base, NotificationType.chosenPlanFinishedToConsultant);
|
||||
ChosenPlanLastDayFinishedForUser.fromJson(base, NotificationType.chosenPlanLastDayFinishedForUser);
|
||||
ChosenPlanConsultantLastDayStarted.fromJson(base, NotificationType.chosenPlanConsultantLastDayStarted);
|
||||
ComplaintToModerationForUser.fromJson(base, NotificationType.complaintToModerationForUser);
|
||||
CommentReplied.fromJson(commentReply, NotificationType.commentReplied);
|
||||
ScoreAdded.fromJson(scoreLike, NotificationType.scoreAdded);
|
||||
ScoreSubstractedNotification.fromJson(scoreLike, NotificationType.scoreSubstracted);
|
||||
EntityCommented.fromJson(entityCommented, NotificationType.entityCommented);
|
||||
UnknownNotification.fromJson(base, NotificationType.unknown);
|
||||
|
||||
ConsultantConfirmPlanSubscriptionToSubscriberNotification.fromJson(
|
||||
base,
|
||||
NotificationType.consultantConfirmPlanSubscriptionToSubscriber,
|
||||
);
|
||||
ClientPaidPlanSubscriptionToAuthorNotification.fromJson(base, NotificationType.clientPaidPlanSubscriptionToAuthor);
|
||||
ConsultantLeftDiaryCommentNotification.fromJson(base, NotificationType.consultantLeftDiaryComment);
|
||||
ConsultantConfirmPlanSubscriptionToAuthorNotification.fromJson(base, NotificationType.consultantConfirmPlanSubscriptionToAuthor);
|
||||
ClientPaidPlanSubscriptionToSubscriberNotification.fromJson(base, NotificationType.clientPaidPlanSubscriptionToSubscriber);
|
||||
ClientStartPlanSubscriptionToAuthorNotification.fromJson(base, NotificationType.clientStartPlanSubscriptionToAuthor);
|
||||
ClientCancelPlanSubscriptionToAuthorNotification.fromJson(base, NotificationType.clientCancelPlanSubscriptionToAuthor);
|
||||
ClientCancelPlanSubscriptionToSubscriberNotification.fromJson(base, NotificationType.clientCancelPlanSubscriptionToSubscriber);
|
||||
UserDailyReportCompletedNotification.fromJson(base, NotificationType.userDailyReportCompleted);
|
||||
StayOnTrackToCustomerNotification.fromJson(base, NotificationType.stayOnTrackToCustomer);
|
||||
PhoneChangedNotification.fromJson(base, NotificationType.phoneChanged);
|
||||
SubscriptionNotification.fromJson(base, NotificationType.subscription);
|
||||
BadgeAwardedNotification.fromJson(base, NotificationType.badgeAwarded);
|
||||
BadgeRevokedNotification.fromJson(base, NotificationType.badgeRevoked);
|
||||
ConsultantRequestStartedNotification.fromJson(base, NotificationType.consultantRequestStarted);
|
||||
ClientCloseComplaintSuccessNotification.fromJson(base, NotificationType.clientCloseComplaintSuccess);
|
||||
ClientRefundSuccessNotification.fromJson(base, NotificationType.clientRefundSuccess);
|
||||
ConsultantRefundSuccessNotification.fromJson(base, NotificationType.consultantRefundSuccess);
|
||||
ClientRejectSuccessNotification.fromJson(base, NotificationType.clientRejectSuccess);
|
||||
ConsultantRejectSuccessNotification.fromJson(base, NotificationType.consultantRejectSuccess);
|
||||
ProductStatusUpdatedForAuthorNotification.fromJson(base, NotificationType.productStatusUpdatedForAuthor);
|
||||
DiaryReminderForUsersNotification.fromJson(base, NotificationType.diaryReminderForUsers);
|
||||
DiaryReminderForUsersNotification.fromJson({'created_at': c, 'data': <String, dynamic>{}}, NotificationType.diaryReminderForUsers);
|
||||
PersonalDietIsDefinedToCustomerNotification.fromJson(base, NotificationType.personalDietIsDefinedToCustomer);
|
||||
ConsultantEducationUpdatedNotification.fromJson(base, NotificationType.consultantEducationUpdated);
|
||||
ComplaintRegisteredNotification.fromJson(base, NotificationType.complaintRegistered);
|
||||
ContentUnpublishedNotification.fromJson(base, NotificationType.contentUnpublished);
|
||||
FinanceQuestionCreatedNotification.fromJson(base, NotificationType.financeQuestionCreated);
|
||||
NewChatMessageNotification.fromJson(base, NotificationType.newChatMessage);
|
||||
CompleteRegistrationNotification.fromJson(base, NotificationType.completeRegistration);
|
||||
ChosenDietExpiredNotification.fromJson(base, NotificationType.chosenDietExpired);
|
||||
MotivateUserNotification.fromJson(base, NotificationType.motivateUser);
|
||||
CommentComplaintNotification.fromJson(base, NotificationType.commentComplaint);
|
||||
CommentDeletedNotification.fromJson(base, NotificationType.commentDeleted);
|
||||
UserSubscribedNotification.fromJson(base, NotificationType.userSubscribed);
|
||||
});
|
||||
|
||||
test('executes additional factories used by legacy and plan flows', () {
|
||||
ClientStartPlanSubscriptionToSubscriberNotification.fromJson(base, NotificationType.clientStartPlanSubscriptionToSubscriber);
|
||||
ChosenPlanFinishedToClientNotification.fromJson(base, NotificationType.chosenPlanFinishedToClient);
|
||||
PlanSubscriptionCanceledToCustomerNotification.fromJson(base, NotificationType.planSubscriptionCanceledToCustomer);
|
||||
PlanSubscriptionCanceledToAuthorNotification.fromJson(planAndUser, NotificationType.planSubscriptionCanceledToAuthor);
|
||||
ChosenPlanStartedToClientNotification.fromJson(planAndUser, NotificationType.chosenPlanStartedToClient);
|
||||
ChosenPlanTomorrowReminderToClientNotification.fromJson(planAndUser, NotificationType.chosenPlanTomorrowReminderToClient);
|
||||
DiaryReminderNotification.fromJson(base, NotificationType.diaryReminderNotification);
|
||||
ConsultantLeftCommentNotification.fromJson(base, NotificationType.consultantLeftComment);
|
||||
PersonalizedDietIsDefinedNotification.fromJson(base, NotificationType.personalizedDietIsDefined);
|
||||
PlanSubscriptionStartedToAuthorNotification.fromJson(planAndUser, NotificationType.planSubscriptionStartedToAuthor);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -71,7 +71,7 @@ void main() {
|
||||
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
|
||||
expect(aboutPlanCubit!.state.planModel, isNotNull);
|
||||
expect(aboutPlanCubit!.state.planModel!.id, 1);
|
||||
@@ -157,7 +157,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => {});
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
await aboutPlanCubit!.likePlan(index: null, slug: 'test-plan');
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 1200));
|
||||
@@ -170,7 +170,7 @@ void main() {
|
||||
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenThrow(Exception('Ошибка загрузки'));
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
|
||||
expect(aboutPlanCubit!.state.error, isNotNull);
|
||||
expect(aboutPlanCubit!.state.isLoading, isFalse);
|
||||
@@ -220,7 +220,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => {});
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
await aboutPlanCubit!.likePlan(index: null, slug: 'test-plan');
|
||||
|
||||
expect(aboutPlanCubit!.state.planModel!.wasLikedByMe, isFalse);
|
||||
@@ -263,7 +263,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.addToFavorite('test-plan')).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
await aboutPlanCubit!.addToFavorite(slug: 'test-plan', index: null);
|
||||
|
||||
expect(aboutPlanCubit!.state.planModel!.isFavorite, true);
|
||||
@@ -305,7 +305,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.removeFromFavorite('test-plan')).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
await aboutPlanCubit!.removeFromFavorite(slug: 'test-plan', index: null);
|
||||
|
||||
expect(aboutPlanCubit!.state.planModel!.isFavorite, false);
|
||||
@@ -357,7 +357,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.chosePlan('test-plan', any)).thenAnswer((_) async => chosenPlan);
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
aboutPlanCubit!.choseOption(0);
|
||||
|
||||
final result = await aboutPlanCubit!.chosePlan();
|
||||
@@ -405,7 +405,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
aboutPlanCubit!.choseOption(0);
|
||||
|
||||
final result = await aboutPlanCubit!.chosePlan();
|
||||
@@ -447,7 +447,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => {});
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
await aboutPlanCubit!.likePlan(index: 5, slug: 'test-plan');
|
||||
|
||||
expect(aboutPlanCubit!.state.planModel!.wasLikedByMe, isTrue);
|
||||
@@ -490,7 +490,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => {});
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
await aboutPlanCubit!.likePlan(index: 5, slug: 'test-plan');
|
||||
|
||||
expect(aboutPlanCubit!.state.planModel!.wasLikedByMe, isFalse);
|
||||
@@ -533,7 +533,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.likePlan('test-plan')).thenThrow(Exception('Ошибка'));
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
await aboutPlanCubit!.likePlan(index: 5, slug: 'test-plan');
|
||||
|
||||
// Проверяем, что состояние изменилось сразу после вызова
|
||||
@@ -583,7 +583,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.addToFavorite('test-plan')).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
await aboutPlanCubit!.addToFavorite(slug: 'test-plan', index: 5);
|
||||
|
||||
expect(aboutPlanCubit!.state.planModel!.isFavorite, true);
|
||||
@@ -624,7 +624,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.addToFavorite('test-plan')).thenThrow(Exception('Ошибка'));
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
await aboutPlanCubit!.addToFavorite(slug: 'test-plan', index: 5);
|
||||
|
||||
// Проверяем, что состояние изменилось сразу после вызова
|
||||
@@ -673,7 +673,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.removeFromFavorite('test-plan')).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
await aboutPlanCubit!.removeFromFavorite(slug: 'test-plan', index: 5);
|
||||
|
||||
expect(aboutPlanCubit!.state.planModel!.isFavorite, false);
|
||||
@@ -714,7 +714,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.removeFromFavorite('test-plan')).thenThrow(Exception('Ошибка'));
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
await aboutPlanCubit!.removeFromFavorite(slug: 'test-plan', index: 5);
|
||||
|
||||
// Проверяем, что состояние изменилось сразу после вызова
|
||||
@@ -773,7 +773,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.chosePlan('test-plan', any)).thenAnswer((_) async => chosenPlan);
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
aboutPlanCubit!.choseOption(1);
|
||||
|
||||
final result = await aboutPlanCubit!.chosePlan();
|
||||
@@ -826,7 +826,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.chosePlan('test-plan', any)).thenAnswer((_) async => chosenPlan);
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
aboutPlanCubit!.choseOption(2);
|
||||
|
||||
final result = await aboutPlanCubit!.chosePlan();
|
||||
@@ -884,7 +884,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
aboutPlanCubit!.choseOption(0);
|
||||
|
||||
final result = await aboutPlanCubit!.chosePlan();
|
||||
@@ -926,7 +926,7 @@ void main() {
|
||||
when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan);
|
||||
when(mockPlanRepository.chosePlan('test-plan', any)).thenThrow(Exception('Общая ошибка'));
|
||||
|
||||
await aboutPlanCubit!.getPlan('test-plan');
|
||||
await aboutPlanCubit!.getPlan(slug: 'test-plan');
|
||||
aboutPlanCubit!.choseOption(0);
|
||||
|
||||
final result = await aboutPlanCubit!.chosePlan();
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/bookmark_button.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
@@ -167,7 +168,7 @@ void main() {
|
||||
when(planCubit.state).thenReturn(state);
|
||||
when(planCubit.stream).thenAnswer((_) => const Stream<AboutPlanState>.empty());
|
||||
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(planCubit.getPlan(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(planCubit.getPlan(slug: anyNamed('slug'), index: anyNamed('index'))).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
@@ -183,6 +184,95 @@ void main() {
|
||||
expect(find.byType(AboutPlanScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('error state try again calls getPlan', (tester) async {
|
||||
final planCubit = MockAboutPlanCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
const state = AboutPlanState(isLoading: false, error: 'Error loading plan', planModel: testPlan, chosenOptionIndex: 0);
|
||||
|
||||
when(planCubit.state).thenReturn(state);
|
||||
when(planCubit.stream).thenAnswer((_) => const Stream<AboutPlanState>.empty());
|
||||
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(planCubit.getPlan(slug: anyNamed('slug'), index: anyNamed('index'))).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.state).thenReturn(const CommentsState());
|
||||
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
|
||||
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
registerMocks(planCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('Попробовать снова'));
|
||||
await tester.pump();
|
||||
|
||||
verify(planCubit.getPlan(slug: 'test', index: null)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('loading state back button does not throw', (tester) async {
|
||||
final planCubit = MockAboutPlanCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
const state = AboutPlanState(isLoading: true, error: null, planModel: null, chosenOptionIndex: 0);
|
||||
|
||||
when(planCubit.state).thenReturn(state);
|
||||
when(planCubit.stream).thenAnswer((_) => const Stream<AboutPlanState>.empty());
|
||||
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.state).thenReturn(const CommentsState());
|
||||
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
|
||||
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
registerMocks(planCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byType(BackButton));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(AboutPlanScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('loaded state bookmark tap calls addToFavorite when not favorite', (tester) async {
|
||||
final planCubit = MockAboutPlanCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
const state = AboutPlanState(isLoading: false, planModel: testPlan, chosenOptionIndex: 0, error: null);
|
||||
|
||||
when(planCubit.state).thenReturn(state);
|
||||
when(planCubit.stream).thenAnswer((_) => const Stream<AboutPlanState>.empty());
|
||||
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(planCubit.addToFavorite(slug: anyNamed('slug'), index: anyNamed('index'))).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.state).thenReturn(const CommentsState());
|
||||
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
|
||||
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
registerMocks(planCubit, commentsCubit);
|
||||
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test-plan', navigateToComments: false, index: 7, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
});
|
||||
|
||||
await tester.tap(find.byType(BookmarkButton));
|
||||
await tester.pump();
|
||||
|
||||
verify(planCubit.addToFavorite(slug: 'test-plan', index: 7)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('renders with navigateToComments true', (tester) async {
|
||||
final planCubit = MockAboutPlanCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
|
||||
@@ -194,6 +194,63 @@ void main() {
|
||||
expect(find.byType(AboutRecipeScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('error state try again calls getRecipe', (tester) async {
|
||||
final recipeCubit = MockAboutRecipeCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
const state = AboutRecipeState.error(message: 'Error loading recipe');
|
||||
|
||||
when(recipeCubit.state).thenReturn(state);
|
||||
when(recipeCubit.stream).thenAnswer((_) => const Stream<AboutRecipeState>.empty());
|
||||
when(recipeCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(recipeCubit.getRecipe(any, index: anyNamed('index'))).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewRecipe(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.state).thenReturn(const CommentsState());
|
||||
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
|
||||
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
registerMocks(recipeCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(recipeCubit, feedCubit, commentsCubit, const AboutRecipeScreen(recipeSlug: 'test', index: 3, navigateToComments: false)));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('Попробовать снова'));
|
||||
await tester.pump();
|
||||
|
||||
verify(recipeCubit.getRecipe('test', index: 3)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('loading state back button does not throw', (tester) async {
|
||||
final recipeCubit = MockAboutRecipeCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
const state = AboutRecipeState.loading();
|
||||
|
||||
when(recipeCubit.state).thenReturn(state);
|
||||
when(recipeCubit.stream).thenAnswer((_) => const Stream<AboutRecipeState>.empty());
|
||||
when(recipeCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewRecipe(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.state).thenReturn(const CommentsState());
|
||||
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
|
||||
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
registerMocks(recipeCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(recipeCubit, feedCubit, commentsCubit, const AboutRecipeScreen(recipeSlug: 'test', index: null, navigateToComments: false)));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byType(BackButton));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(AboutRecipeScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders with navigateToComments true', (tester) async {
|
||||
final recipeCubit = MockAboutRecipeCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
|
||||
Reference in New Issue
Block a user