fix: issue-1049, issue-1039, issue-1035, issue-1032, issue-995, issue-993

This commit is contained in:
misha
2026-08-26 08:42:47 +03:00
parent 6265e865e9
commit de7bf8dc19
22 changed files with 509 additions and 192 deletions

View File

@@ -0,0 +1,64 @@
import 'dart:io';
import 'package:eatvio_app/core/notifications/notification_permission_requester.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('NotificationPermissionRequester', () {
test('вызывает requestPermission один раз', () async {
final requester = NotificationPermissionRequester();
var calls = 0;
await requester.requestOnce(() async {
calls++;
});
expect(calls, 1);
});
test('параллельные вызовы ждут один и тот же запрос', () async {
final requester = NotificationPermissionRequester();
var calls = 0;
Future<void> request() {
return requester.requestOnce(() async {
calls++;
await Future<void>.delayed(const Duration(milliseconds: 50));
});
}
await Future.wait<void>([request(), request(), request()]);
expect(calls, 1);
});
test('после завершения можно запросить снова', () async {
final requester = NotificationPermissionRequester();
var calls = 0;
Future<void> request() {
return requester.requestOnce(() async {
calls++;
});
}
await request();
await request();
expect(calls, 2);
});
});
group('единая точка запроса разрешения на уведомления', () {
test('старт приложения и CallKit не показывают отдельные системные окна', () {
final initializer = File('lib/core/app_initializer.dart').readAsStringSync();
final callkit = File('lib/features/video_calls/services/video_call_callkit_coordinator.dart').readAsStringSync();
final firebaseApi = File('lib/core/firebase_api.dart').readAsStringSync();
expect(initializer.contains('requestPermission'), isFalse);
expect(callkit.contains('requestNotificationPermission'), isFalse);
expect(firebaseApi.contains('_requestNotificationPermission'), isTrue);
expect(firebaseApi.contains('_firebaseMessaging.requestPermission()'), isTrue);
});
});
}

View File

@@ -1,8 +1,11 @@
import 'dart:async';
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/core/widgets/loader.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';
@@ -12,6 +15,7 @@ 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 'complaining_bottom_sheet_smoke_test.mocks.dart';
@@ -59,7 +63,40 @@ void main() {
await tester.pumpAndSettle();
expect(find.byType(ComplainingBottomSheet), findsOneWidget);
});
testWidgets('replaces send button with Loader while request is in progress', (tester) async {
final completer = Completer<void>();
when(
repository.sendPlanComplain(
slug: anyNamed('slug'),
category: anyNamed('category'),
body: anyNamed('body'),
),
).thenAnswer((_) => completer.future);
await tester.pumpWidget(
wrap(
const ComplainingBottomSheet(
complainedContentType: ComplainedContent.plan,
slug: 'test-slug',
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Спам'));
await tester.pumpAndSettle();
expect(find.text('Отправить жалобу'), findsOneWidget);
await tester.tap(find.text('Отправить жалобу'));
await tester.pump();
expect(find.byType(Loader), findsOneWidget);
expect(find.text('Отправить жалобу'), findsNothing);
completer.complete();
await tester.pump();
});
});
}

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/features/complaining/models/complain_model.dart';
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
@@ -97,6 +99,7 @@ void main() {
await cubit.complain();
expect(cubit.state.wasSentSuccessfully, true);
expect(cubit.state.isLoading, false);
verify(() => mockRepository.sendDietComplain(
id: id,
category: any(named: 'category'),
@@ -104,6 +107,30 @@ void main() {
)).called(1);
});
test('complain - ставит isLoading на время запроса', () async {
const id = 1;
final completer = Completer<void>();
when(() => mockRepository.sendDietComplain(
id: id,
category: any(named: 'category'),
body: any(named: 'body'),
)).thenAnswer((_) => completer.future);
cubit.setContentTypeAndIdentifier(contentType: ComplainedContent.diet, id: id);
cubit.setCategory(ComplainType.spam);
final future = cubit.complain();
expect(cubit.state.isLoading, true);
expect(cubit.state.wasSentSuccessfully, isNull);
completer.complete();
await future;
expect(cubit.state.isLoading, false);
expect(cubit.state.wasSentSuccessfully, true);
});
test('complain - возвращает false при ошибке', () async {
const id = 1;
@@ -119,6 +146,7 @@ void main() {
await cubit.complain();
expect(cubit.state.wasSentSuccessfully, false);
expect(cubit.state.isLoading, false);
});
test('complain - отправляет жалобу на plan', () async {

View File

@@ -4,9 +4,12 @@ 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/rendering.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:table_calendar/table_calendar.dart';
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
FakeDiaryCubit(super.initialState);
@@ -16,9 +19,20 @@ class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
void main() {
group('CalendarWidget smoke', () {
Widget wrap(Widget child, DiaryCubit cubit) {
setUpAll(() async {
await initializeDateFormatting('ru', null);
await initializeDateFormatting('ru_RU', null);
});
Widget wrap(Widget child, DiaryCubit cubit, {double textScale = 1}) {
return MaterialApp(
theme: ThemeHelper().themeData(),
builder: (context, appChild) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(textScale)),
child: appChild!,
);
},
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
@@ -37,9 +51,26 @@ void main() {
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();
});
testWidgets('scales weekday row when system font is large', (tester) async {
const textScale = 2.0;
final cubit = FakeDiaryCubit(const DiaryState.loading());
await tester.pumpWidget(wrap(const CalendarWidget(), cubit, textScale: textScale));
await tester.pump();
final calendar = tester.widget<TableCalendar<dynamic>>(find.byType(TableCalendar<dynamic>));
expect(calendar.daysOfWeekHeight, greaterThan(16 * textScale * 0.9));
expect(calendar.rowHeight, 52 * textScale);
expect(find.text('пн'), findsWidgets);
final weekday = tester.renderObject<RenderParagraph>(find.text('пн').first);
expect(weekday.size.height, lessThanOrEqualTo(calendar.daysOfWeekHeight + 0.5));
expect(tester.takeException(), isNull);
await cubit.close();
});
});
}

View File

@@ -156,6 +156,44 @@ void main() {
expect(dietsCubit!.state.errorMessage, isNull);
});
test('loadDiets - сбрасывает currentPage при повторной загрузке', () async {
dietsCubit = DietsCubit(
feedRepository: mockFeedRepository,
dietsRepository: mockDietRepository,
);
const dietsPagination = DietsPagination(
data: [],
links: Links(
first: 'https://example.com?page=1',
last: 'https://example.com?page=1',
prev: null,
next: null,
),
meta: Meta(
currentPage: 1,
from: null,
lastPage: 1,
links: [],
perPage: 10,
to: null,
total: 0,
),
);
when(mockFeedRepository.getDiets(
query: anyNamed('query'),
types: anyNamed('types'),
duration: anyNamed('duration'),
sorting: anyNamed('sorting'),
)).thenAnswer((_) async => dietsPagination);
dietsCubit!.currentPage = 4;
await dietsCubit!.loadDiets();
expect(dietsCubit!.currentPage, 1);
});
test('loadDiets - обработка ошибки', () async {
dietsCubit = DietsCubit(
feedRepository: mockFeedRepository,

View File

@@ -131,6 +131,31 @@ void main() {
expect(plansCubit!.state.errorMessage, isNull);
});
test('loadPlans - сбрасывает currentPage при повторной загрузке', () async {
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
const plansPagination = PlanPaginationModel(
data: [],
links: Links(first: 'https://example.com?page=1', last: 'https://example.com?page=1', prev: null, next: null),
meta: Meta(currentPage: 1, from: 1, lastPage: 1, links: [], perPage: 10, to: 1, total: 0),
);
when(
mockFeedRepository.getPlans(
query: anyNamed('query'),
types: anyNamed('types'),
duration: anyNamed('duration'),
minPrice: anyNamed('minPrice'),
maxPrice: anyNamed('maxPrice'),
),
).thenAnswer((_) async => plansPagination);
plansCubit!.currentPage = 4;
await plansCubit!.loadPlans();
expect(plansCubit!.currentPage, 1);
});
test('loadPlans - обработка ошибки', () async {
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);

View File

@@ -110,6 +110,18 @@ void main() {
expect(recipesCubit!.state.errorMessage, isNull);
});
test('loadRecipesList - сбрасывает currentPage при повторной загрузке', () async {
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
when(mockFeedRepository.getRecipes(tags: anyNamed('tags'), query: anyNamed('query'))).thenAnswer((_) async => []);
when(mockRecipeRepository.getRecipesTags()).thenAnswer((_) async => []);
recipesCubit!.currentPage = 4;
await recipesCubit!.loadRecipesList();
expect(recipesCubit!.currentPage, 1);
});
test('updateTags - добавляет и удаляет теги', () {
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);

View File

@@ -136,4 +136,19 @@ void main() {
expect(find.byType(FeedScreen), findsOneWidget);
});
});
group('shouldReloadFeedAfterCreate', () {
test('перезагружает ленту после публикации диеты или плана', () {
expect(shouldReloadFeedAfterCreate(true), isTrue);
});
test('перезагружает ленту после публикации рецепта', () {
expect(shouldReloadFeedAfterCreate('active'), isTrue);
});
test('не перезагружает ленту при отмене или черновике рецепта', () {
expect(shouldReloadFeedAfterCreate(null), isFalse);
expect(shouldReloadFeedAfterCreate('drafted'), isFalse);
});
});
}

View File

@@ -1,7 +1,10 @@
import 'dart:async';
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/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';
@@ -130,5 +133,52 @@ void main() {
expect(find.byType(GeneralInfoScreen), findsOneWidget);
});
testWidgets('лоадер при сохранении появляется вместо кнопки, а не поверх экрана', (tester) async {
final cubit = MockSettingsCubit();
const state = SettingsState.loaded(
profileSettings: mockProfileSettings,
profileDetails: mockProfileDetails,
profile: mockProfile,
hasActiveConsultation: false,
);
final saveCompleter = Completer<void>();
when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state));
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.updateProfileDetails(profileDetailsModel: anyNamed('profileDetailsModel'))).thenAnswer((_) => saveCompleter.future);
when(cubit.updateProfileSettings(profileSettingsModel: anyNamed('profileSettingsModel'))).thenAnswer((_) async {});
final settingsEntity = SettingsEntity(profileSettingsModel: mockProfileSettings, profileDetailsModel: mockProfileDetails, settingsCubit: cubit);
await tester.pumpWidget(
BlocProvider<SettingsCubit>.value(
value: cubit,
child: wrap(GeneralInfoScreen(settingsEntity: settingsEntity)),
),
);
await tester.pump();
expect(find.text('Сохранить'), findsOneWidget);
expect(find.byType(Loader), findsNothing);
await tester.tap(find.text('Сохранить'));
await tester.pump();
expect(find.text('Сохранить'), findsNothing);
expect(find.byType(Loader), findsOneWidget);
expect(find.text('Введите имя'), findsOneWidget);
final loaderSize = tester.getSize(find.byType(Loader));
expect(loaderSize.height, lessThan(60));
saveCompleter.complete();
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(find.text('Сохранить'), findsOneWidget);
expect(find.byType(Loader), findsNothing);
});
});
}