From 965ae2f02bebcdda84357598e1bb7c7cbb959fae Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 1 Dec 2025 23:51:40 +0300 Subject: [PATCH] feat(test): update oauth handling and improve preferences management --- test/core/app_config_test.dart | 74 +++++++++ .../models/comment_update_model_test.dart | 36 ++++ test/core/cubit/base_cubit_test.dart | 73 +++++++++ .../cubit/marketing/marketing_cubit_test.dart | 98 +++++++++++ .../internet_connection_cubit_test.dart | 80 +++++++++ test/core/token_storage_test.dart | 112 +++++++++++++ test/core/utils/toasts_test.dart | 59 +++++++ test/core/views/cubit/views_cubit_test.dart | 154 ++++++++++++++++++ .../repository/views_repository_test.dart | 71 ++++++++ .../new_messages_cubit_test.dart | 42 +++++ .../consultants_cubit_test.dart | 103 ++++++++++++ 11 files changed, 902 insertions(+) create mode 100644 test/core/app_config_test.dart create mode 100644 test/core/comments/models/comment_update_model_test.dart create mode 100644 test/core/cubit/base_cubit_test.dart create mode 100644 test/core/cubit/marketing/marketing_cubit_test.dart create mode 100644 test/core/internet_connection_cubit/internet_connection_cubit_test.dart create mode 100644 test/core/token_storage_test.dart create mode 100644 test/core/utils/toasts_test.dart create mode 100644 test/core/views/cubit/views_cubit_test.dart create mode 100644 test/core/views/repository/views_repository_test.dart create mode 100644 test/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit_test.dart create mode 100644 test/features/feed/presentation/bloc/consultants_cubit/consultants_cubit_test.dart diff --git a/test/core/app_config_test.dart b/test/core/app_config_test.dart new file mode 100644 index 00000000..146a6262 --- /dev/null +++ b/test/core/app_config_test.dart @@ -0,0 +1,74 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AppConfig', () { + setUp(() async { + // Инициализируем dotenv для тестов + dotenv.loadFromString(envString: ''' +BASE_URL=https://api.example.com +DEV_BASE_URL=https://dev-api.example.com +'''); + }); + + test('создание с buildType.prod - использует baseUrl', () { + final config = AppConfig(buildType: AppBuildType.prod); + + expect(config.buildType, AppBuildType.prod); + expect(config.baseUrl, isNotEmpty); + }); + + test('создание с buildType.dev - использует devBaseUrl', () { + final config = AppConfig(buildType: AppBuildType.dev); + + expect(config.buildType, AppBuildType.dev); + expect(config.baseUrl, isNotEmpty); + }); + + test('создание с buildType.tests - использует devBaseUrl', () { + final config = AppConfig(buildType: AppBuildType.tests); + + expect(config.buildType, AppBuildType.tests); + expect(config.baseUrl, isNotEmpty); + }); + + test('setBaseUrl - изменяет baseUrl', () { + final config = AppConfig(buildType: AppBuildType.tests); + const newUrl = 'http://test.example.com'; + + config.setBaseUrl(newUrl); + + expect(config.baseUrl, newUrl); + }); + + test('globalBlocProviders - возвращает список провайдеров', () { + final config = AppConfig(buildType: AppBuildType.tests); + + final providers = config.globalBlocProviders; + + expect(providers, isNotEmpty); + expect(providers.length, greaterThan(0)); + }); + + test('localizationDelegates - возвращает список делегатов', () { + final config = AppConfig(buildType: AppBuildType.tests); + + final delegates = config.localizationDelegates; + + expect(delegates, isNotEmpty); + expect(delegates.length, greaterThan(0)); + }); + + test('supportedLocales - возвращает список локалей', () { + final config = AppConfig(buildType: AppBuildType.tests); + + final locales = config.supportedLocales; + + expect(locales, isNotEmpty); + expect(locales.length, greaterThan(0)); + }); + }); +} + diff --git a/test/core/comments/models/comment_update_model_test.dart b/test/core/comments/models/comment_update_model_test.dart new file mode 100644 index 00000000..d4e19f02 --- /dev/null +++ b/test/core/comments/models/comment_update_model_test.dart @@ -0,0 +1,36 @@ +import 'package:eatvio_app/core/comments/models/comment_update_model.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('CommentUpdateModel', () { + test('создание модели через конструктор', () { + final model = CommentUpdateModel(body: 'Обновлённый текст комментария'); + + expect(model.body, 'Обновлённый текст комментария'); + }); + + test('создание модели из JSON', () { + final json = {'body': 'Текст комментария'}; + final model = CommentUpdateModel.fromJson(json); + + expect(model.body, 'Текст комментария'); + }); + + test('преобразование модели в JSON', () { + final model = CommentUpdateModel(body: 'Текст комментария'); + final json = model.toJson(); + + expect(json['body'], 'Текст комментария'); + }); + + test('равенство моделей', () { + final model1 = CommentUpdateModel(body: 'Текст комментария'); + final model2 = CommentUpdateModel(body: 'Текст комментария'); + final model3 = CommentUpdateModel(body: 'Другой текст'); + + expect(model1, equals(model2)); + expect(model1, isNot(equals(model3))); + }); + }); +} + diff --git a/test/core/cubit/base_cubit_test.dart b/test/core/cubit/base_cubit_test.dart new file mode 100644 index 00000000..52b581fb --- /dev/null +++ b/test/core/cubit/base_cubit_test.dart @@ -0,0 +1,73 @@ +import 'package:eatvio_app/core/cubit/base_cubit.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// Простой тестовый cubit для проверки BaseCubit +class TestState { + final String? value; + TestState({this.value}); +} + +class TestCubit extends BaseCubit { + TestCubit() : super(TestState()); + + void testHandleError(Object error, [StackTrace? stackTrace]) { + handleError(error, stackTrace); + } + + Future testSafeExecute() async { + return await safeExecute(() async => 'success'); + } + + Future testSafeExecuteWithError() async { + return await safeExecute(() async => throw Exception('Test error')); + } + + Future testSafeExecuteWithState() async { + return await safeExecuteWithState( + () async => 'result', + loadingState: TestState(value: 'loading'), + successState: (result) => TestState(value: result), + errorState: (error) => TestState(value: 'error'), + ); + } +} + +void main() { + group('BaseCubit', () { + late TestCubit testCubit; + + setUp(() { + testCubit = TestCubit(); + }); + + tearDown(() async { + if (!testCubit.isClosed) { + await testCubit.close(); + } + }); + + test('handleError - обрабатывает ошибку без исключений', () { + expect(() => testCubit.testHandleError(Exception('Test error')), returnsNormally); + }); + + test('safeExecute - успешно выполняет операцию', () async { + final result = await testCubit.testSafeExecute(); + + expect(result, 'success'); + }); + + test('safeExecute - возвращает null при ошибке', () async { + final result = await testCubit.testSafeExecuteWithError(); + + expect(result, isNull); + }); + + test('safeExecuteWithState - устанавливает состояния правильно', () async { + final result = await testCubit.testSafeExecuteWithState(); + + expect(result, 'result'); + expect(testCubit.state.value, 'result'); + }); + }); +} + diff --git a/test/core/cubit/marketing/marketing_cubit_test.dart b/test/core/cubit/marketing/marketing_cubit_test.dart new file mode 100644 index 00000000..859fb4e4 --- /dev/null +++ b/test/core/cubit/marketing/marketing_cubit_test.dart @@ -0,0 +1,98 @@ +import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart'; +import 'package:eatvio_app/core/cubit/marketing/marketing_cubit_state.dart'; +import 'package:eatvio_app/core/prefs.dart'; +import 'package:eatvio_app/core/prefs_keys.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'marketing_cubit_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('MarketingCubit', () { + late MarketingCubit marketingCubit; + late MockPrefs mockPrefs; + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + mockPrefs = MockPrefs(); + marketingCubit = MarketingCubit(mockPrefs); + }); + + tearDown(() async { + if (!marketingCubit.isClosed) { + await marketingCubit.close(); + } + }); + + test('начальное состояние - MarketingCubitState', () { + expect(marketingCubit.state, const MarketingCubitState()); + }); + + test('checkRegistrationDate - возвращает false если registrationDate отсутствует', () async { + when(mockPrefs.getInt(PrefsKeys.registrationDate)).thenReturn(null); + + final result = await marketingCubit.checkRegistrationDate(); + + expect(result, isFalse); + verify(mockPrefs.getInt(PrefsKeys.registrationDate)).called(1); + }); + + test('checkRegistrationDate - возвращает false если маркетинговый bottom sheet уже был показан', () async { + when(mockPrefs.getInt(PrefsKeys.registrationDate)).thenReturn(DateTime.now().millisecondsSinceEpoch); + when(mockPrefs.getBool(PrefsKeys.marketingBottomSheetHasBeenShown)).thenReturn(true); + + final result = await marketingCubit.checkRegistrationDate(); + + expect(result, isFalse); + verify(mockPrefs.getInt(PrefsKeys.registrationDate)).called(1); + verify(mockPrefs.getBool(PrefsKeys.marketingBottomSheetHasBeenShown)).called(1); + }); + + test('checkRegistrationDate - возвращает false если прошло менее 14 дней', () async { + final registrationDate = DateTime.now().subtract(const Duration(days: 10)); + when(mockPrefs.getInt(PrefsKeys.registrationDate)).thenReturn(registrationDate.millisecondsSinceEpoch); + when(mockPrefs.getBool(PrefsKeys.marketingBottomSheetHasBeenShown)).thenReturn(false); + + final result = await marketingCubit.checkRegistrationDate(); + + expect(result, isFalse); + verify(mockPrefs.getInt(PrefsKeys.registrationDate)).called(1); + verify(mockPrefs.getBool(PrefsKeys.marketingBottomSheetHasBeenShown)).called(1); + verifyNever(mockPrefs.setBool(any, any)); + }); + + test('checkRegistrationDate - возвращает true если прошло 14 дней или больше', () async { + final registrationDate = DateTime.now().subtract(const Duration(days: 14)); + when(mockPrefs.getInt(PrefsKeys.registrationDate)).thenReturn(registrationDate.millisecondsSinceEpoch); + when(mockPrefs.getBool(PrefsKeys.marketingBottomSheetHasBeenShown)).thenReturn(false); + when(mockPrefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true)).thenAnswer((_) async => Future.value()); + + final result = await marketingCubit.checkRegistrationDate(); + + expect(result, isTrue); + verify(mockPrefs.getInt(PrefsKeys.registrationDate)).called(1); + verify(mockPrefs.getBool(PrefsKeys.marketingBottomSheetHasBeenShown)).called(1); + verify(mockPrefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true)).called(1); + }); + + test('checkRegistrationDate - устанавливает флаг после показа bottom sheet', () async { + final registrationDate = DateTime.now().subtract(const Duration(days: 15)); + when(mockPrefs.getInt(PrefsKeys.registrationDate)).thenReturn(registrationDate.millisecondsSinceEpoch); + when(mockPrefs.getBool(PrefsKeys.marketingBottomSheetHasBeenShown)).thenReturn(false); + when(mockPrefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true)).thenAnswer((_) async => Future.value()); + + await marketingCubit.checkRegistrationDate(); + + verify(mockPrefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true)).called(1); + }); + + test('checkRegistrationDate - пробрасывает исключение', () async { + when(mockPrefs.getInt(PrefsKeys.registrationDate)).thenThrow(Exception('Ошибка чтения')); + + expect(() => marketingCubit.checkRegistrationDate(), throwsException); + }); + }); +} diff --git a/test/core/internet_connection_cubit/internet_connection_cubit_test.dart b/test/core/internet_connection_cubit/internet_connection_cubit_test.dart new file mode 100644 index 00000000..78da97a4 --- /dev/null +++ b/test/core/internet_connection_cubit/internet_connection_cubit_test.dart @@ -0,0 +1,80 @@ +import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart'; +import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_state.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('InternetConnectionCubit', () { + late InternetConnectionCubit cubit; + const MethodChannel connectivityChannel = MethodChannel('dev.fluttercommunity.plus/connectivity'); + + setUp(() { + // Мокируем MethodChannel для connectivity + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + connectivityChannel, + (MethodCall methodCall) async { + if (methodCall.method == 'check') { + return ['wifi']; // ConnectivityResult.wifi как строка + } + if (methodCall.method == 'onConnectivityChanged') { + return null; + } + return null; + }, + ); + }); + + tearDown(() async { + // Очищаем моки + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + connectivityChannel, + null, + ); + if (!cubit.isClosed) { + await cubit.close(); + } + }); + + test('начальное состояние - isInternetAvailable = true', () { + cubit = InternetConnectionCubit(); + expect(cubit.state.isInternetAvailable, isTrue); + }); + + test('checkConnection - успешная проверка с интернетом', () async { + cubit = InternetConnectionCubit(); + + // Ждем немного, чтобы cubit инициализировался + await Future.delayed(const Duration(milliseconds: 100)); + + // Проверяем, что метод checkConnection не выбрасывает исключений + await cubit.checkConnection(); + + // Состояние должно быть определено (true или false в зависимости от реального состояния) + expect(cubit.state, isA()); + }); + + test('checkConnection - обработка PlatformException', () async { + // Этот тест сложно выполнить без реального мока Connectivity, + // так как Connectivity создается внутри cubit + // Оставим базовую проверку, что метод не падает + cubit = InternetConnectionCubit(); + await Future.delayed(const Duration(milliseconds: 100)); + + // Просто проверяем, что метод выполняется без ошибок + await cubit.checkConnection(); + expect(cubit.state, isA()); + }); + + test('close - корректно закрывает cubit и освобождает ресурсы', () async { + cubit = InternetConnectionCubit(); + await Future.delayed(const Duration(milliseconds: 100)); + + await cubit.close(); + + expect(cubit.isClosed, isTrue); + }); + }); +} + diff --git a/test/core/token_storage_test.dart b/test/core/token_storage_test.dart new file mode 100644 index 00000000..d839ba85 --- /dev/null +++ b/test/core/token_storage_test.dart @@ -0,0 +1,112 @@ +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + group('TokenStorage', () { + setUp(() async { + SharedPreferences.setMockInitialValues({}); + }); + + test('haveTokenInStorage - возвращает false если токен отсутствует', () async { + final result = await TokenStorage.haveTokenInStorage(); + + expect(result, isFalse); + }); + + test('haveTokenInStorage - возвращает true если токен присутствует', () async { + SharedPreferences.setMockInitialValues({'token': 'test_token_123'}); + + final result = await TokenStorage.haveTokenInStorage(); + + expect(result, isTrue); + }); + + test('getTokenFromStorage - возвращает null если токен отсутствует', () async { + final result = await TokenStorage.getTokenFromStorage(); + + expect(result, isNull); + }); + + test('getTokenFromStorage - возвращает токен если он присутствует', () async { + const testToken = 'test_token_123'; + SharedPreferences.setMockInitialValues({'token': testToken}); + + final result = await TokenStorage.getTokenFromStorage(); + + expect(result, testToken); + }); + + test('saveTokenInStorage - сохраняет токен', () async { + const testToken = 'new_token_456'; + + await TokenStorage.saveTokenInStorage(testToken); + + final savedToken = await TokenStorage.getTokenFromStorage(); + expect(savedToken, testToken); + }); + + test('removeTokenInStorage - удаляет токен', () async { + SharedPreferences.setMockInitialValues({'token': 'test_token_123'}); + + await TokenStorage.removeTokenInStorage(); + + final result = await TokenStorage.getTokenFromStorage(); + expect(result, isNull); + }); + }); + + group('SocketIdStorage', () { + setUp(() async { + SharedPreferences.setMockInitialValues({}); + }); + + test('haveSocketIdInStorage - возвращает false если socketId отсутствует', () async { + final result = await SocketIdStorage.haveSocketIdInStorage(); + + expect(result, isFalse); + }); + + test('haveSocketIdInStorage - возвращает true если socketId присутствует', () async { + SharedPreferences.setMockInitialValues({'socketId': 'socket_id_123'}); + + final result = await SocketIdStorage.haveSocketIdInStorage(); + + expect(result, isTrue); + }); + + test('getSocketIdFromStorage - возвращает null если socketId отсутствует', () async { + final result = await SocketIdStorage.getSocketIdFromStorage(); + + expect(result, isNull); + }); + + test('getSocketIdFromStorage - возвращает socketId если он присутствует', () async { + const testSocketId = 'socket_id_123'; + SharedPreferences.setMockInitialValues({'socketId': testSocketId}); + + final result = await SocketIdStorage.getSocketIdFromStorage(); + + expect(result, testSocketId); + }); + + test('saveSocketIdInStorage - сохраняет socketId', () async { + const testSocketId = 'new_socket_id_456'; + + await SocketIdStorage.saveSocketIdInStorage(testSocketId); + + final savedSocketId = await SocketIdStorage.getSocketIdFromStorage(); + expect(savedSocketId, testSocketId); + }); + + test('removeSocketIdInStorage - удаляет socketId', () async { + SharedPreferences.setMockInitialValues({'socketId': 'socket_id_123'}); + + await SocketIdStorage.removeSocketIdInStorage(); + + final result = await SocketIdStorage.getSocketIdFromStorage(); + expect(result, isNull); + }); + }); +} + diff --git a/test/core/utils/toasts_test.dart b/test/core/utils/toasts_test.dart new file mode 100644 index 00000000..635cc253 --- /dev/null +++ b/test/core/utils/toasts_test.dart @@ -0,0 +1,59 @@ +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Toasts', () { + setUp(() { + // Mock Fluttertoast plugin + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel('PonnamKarthik/fluttertoast'), + (MethodCall methodCall) async { + return null; + }, + ); + }); + + tearDown(() { + // Clear mock handlers + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel('PonnamKarthik/fluttertoast'), + null, + ); + }); + + test('showInfoToast - вызывает Fluttertoast без ошибок', () { + expect(() => showInfoToast('Информационное сообщение'), returnsNormally); + }); + + test('showErrorToast - вызывает Fluttertoast без ошибок', () { + expect(() => showErrorToast('Сообщение об ошибке'), returnsNormally); + }); + + test('showSuccessToast - вызывает Fluttertoast без ошибок', () { + expect(() => showSuccessToast('Успешное сообщение'), returnsNormally); + }); + + test('showInternetConnectionToast - вызывает Fluttertoast без ошибок', () { + expect(() => showInternetConnectionToast(), returnsNormally); + }); + + test('showInfoToast - работает с пустой строкой', () { + expect(() => showInfoToast(''), returnsNormally); + }); + + test('showErrorToast - работает с длинным сообщением', () { + final longMessage = 'Очень длинное сообщение об ошибке ' * 10; + expect(() => showErrorToast(longMessage), returnsNormally); + }); + + test('showSuccessToast - работает с различными сообщениями', () { + expect(() => showSuccessToast('Операция выполнена успешно'), returnsNormally); + expect(() => showSuccessToast('Данные сохранены'), returnsNormally); + expect(() => showSuccessToast('Загрузка завершена'), returnsNormally); + }); + }); +} + diff --git a/test/core/views/cubit/views_cubit_test.dart b/test/core/views/cubit/views_cubit_test.dart new file mode 100644 index 00000000..512a589a --- /dev/null +++ b/test/core/views/cubit/views_cubit_test.dart @@ -0,0 +1,154 @@ +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; +import 'package:eatvio_app/core/views/cubit/views_state.dart'; +import 'package:eatvio_app/core/views/repository/views_repository.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'views_cubit_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('ViewsCubit', () { + late ViewsCubit cubit; + late MockViewsRepository mockViewsRepository; + + setUp(() { + mockViewsRepository = MockViewsRepository(); + cubit = ViewsCubit(viewsRepository: mockViewsRepository); + }); + + tearDown(() async { + if (!cubit.isClosed) { + await cubit.close(); + } + }); + + test('начальное состояние - ViewsCubitState', () { + expect(cubit.state, isA()); + }); + + test('handleVisibility - добавляет ID при visibleFraction >= 0.5', () async { + when(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))).thenAnswer((_) async => Future.value()); + + cubit.handleVisibility(visibleFraction: 0.6, id: 1, type: ViewableContentType.posts); + + // Ждем выполнения таймера (1 секунда) + await Future.delayed(const Duration(milliseconds: 1100)); + + expect(cubit.postsIds, contains(1)); + }); + + test('handleVisibility - не добавляет ID при visibleFraction < 0.5', () async { + cubit.handleVisibility(visibleFraction: 0.3, id: 1, type: ViewableContentType.posts); + + await Future.delayed(const Duration(milliseconds: 100)); + + expect(cubit.postsIds, isEmpty); + }); + + test('handleVisibility - отменяет таймер при уменьшении visibleFraction', () async { + when(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))).thenAnswer((_) async => Future.value()); + + // Сначала видимость >= 0.5 + cubit.handleVisibility(visibleFraction: 0.6, id: 1, type: ViewableContentType.posts); + + await Future.delayed(const Duration(milliseconds: 500)); + + // Затем видимость < 0.5 + cubit.handleVisibility(visibleFraction: 0.3, id: 1, type: ViewableContentType.posts); + + await Future.delayed(const Duration(milliseconds: 700)); + + // ID не должен быть добавлен, так как таймер был отменен + expect(cubit.postsIds, isEmpty); + }); + + test('handleVisibility - не добавляет уже отправленный ID', () async { + when(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))).thenAnswer((_) async => Future.value()); + + // Добавляем и отправляем ID + cubit.handleVisibility(visibleFraction: 0.6, id: 1, type: ViewableContentType.posts); + + await Future.delayed(const Duration(milliseconds: 1100)); + await Future.delayed(const Duration(milliseconds: 4100)); // Ждем debounce и отправку + + // Пытаемся добавить тот же ID снова + cubit.handleVisibility(visibleFraction: 0.6, id: 1, type: ViewableContentType.posts); + + await Future.delayed(const Duration(milliseconds: 100)); + + // Таймер не должен быть создан, так как ID уже отправлен + expect(cubit.postsIds, isEmpty); + }); + + test('handleVisibility - работает для разных типов контента', () async { + when(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))).thenAnswer((_) async => Future.value()); + + cubit.handleVisibility(visibleFraction: 0.6, id: 1, type: ViewableContentType.posts); + cubit.handleVisibility(visibleFraction: 0.6, id: 2, type: ViewableContentType.plans); + cubit.handleVisibility(visibleFraction: 0.6, id: 3, type: ViewableContentType.diets); + cubit.handleVisibility(visibleFraction: 0.6, id: 4, type: ViewableContentType.recipes); + + await Future.delayed(const Duration(milliseconds: 1100)); + + expect(cubit.postsIds, contains(1)); + expect(cubit.plansIds, contains(2)); + expect(cubit.dietsIds, contains(3)); + expect(cubit.recipesIds, contains(4)); + }); + + test('_sendViews - отправляет просмотры через repository', () async { + when(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))).thenAnswer((_) async => Future.value()); + + // Добавляем ID для разных типов + cubit.handleVisibility(visibleFraction: 0.6, id: 1, type: ViewableContentType.posts); + cubit.handleVisibility(visibleFraction: 0.6, id: 2, type: ViewableContentType.plans); + + await Future.delayed(const Duration(milliseconds: 1100)); + await Future.delayed(const Duration(milliseconds: 4100)); // Ждем debounce и отправку + + // Проверяем, что repository был вызван минимум 2 раза (для posts и plans) + verify(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))).called(greaterThanOrEqualTo(2)); + }); + + test('close - отправляет оставшиеся просмотры и освобождает ресурсы', () async { + when(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))).thenAnswer((_) async => Future.value()); + + // Добавляем ID + cubit.handleVisibility(visibleFraction: 0.6, id: 1, type: ViewableContentType.posts); + await Future.delayed(const Duration(milliseconds: 1100)); + + // Очищаем предыдущие взаимодействия перед проверкой + clearInteractions(mockViewsRepository); + + // Закрываем cubit + await cubit.close(); + + // Проверяем, что repository был вызван для отправки оставшихся просмотров + verify(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))).called(greaterThanOrEqualTo(1)); + expect(cubit.isClosed, isTrue); + }); + + test('_sendViews - не отправляет уже отправленные ID', () async { + when(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))).thenAnswer((_) async => Future.value()); + + // Добавляем и отправляем ID + cubit.handleVisibility(visibleFraction: 0.6, id: 1, type: ViewableContentType.posts); + await Future.delayed(const Duration(milliseconds: 1100)); + await Future.delayed(const Duration(milliseconds: 4100)); + + // Очищаем моки + clearInteractions(mockViewsRepository); + + // Закрываем cubit + await cubit.close(); + + // Repository не должен быть вызван, так как все ID уже отправлены + verifyNever(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))); + }); + }); +} diff --git a/test/core/views/repository/views_repository_test.dart b/test/core/views/repository/views_repository_test.dart new file mode 100644 index 00000000..11a5748c --- /dev/null +++ b/test/core/views/repository/views_repository_test.dart @@ -0,0 +1,71 @@ +import 'package:eatvio_app/core/views/repository/views_repository.dart'; +import 'package:eatvio_app/core/views/service/views_service.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'views_repository_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('ViewsRepository', () { + late ViewsRepository viewsRepository; + late MockViewsService mockViewsService; + + setUp(() { + mockViewsService = MockViewsService(); + viewsRepository = ViewsRepository(viewsService: mockViewsService); + }); + + test('viewContent - успешная отправка просмотров', () async { + final viewModel = ViewModel( + type: 'blog_posts', + ids: [1, 2, 3], + ); + + when(mockViewsService.viewContent(viewModel: viewModel)) + .thenAnswer((_) async => Future.value()); + + await viewsRepository.viewContent(viewModel: viewModel); + + verify(mockViewsService.viewContent(viewModel: viewModel)).called(1); + }); + + test('viewContent - пробрасывает исключение', () async { + final viewModel = ViewModel( + type: 'blog_posts', + ids: [1, 2, 3], + ); + + when(mockViewsService.viewContent(viewModel: viewModel)) + .thenThrow(Exception('Ошибка отправки')); + + expect( + () => viewsRepository.viewContent(viewModel: viewModel), + throwsException, + ); + }); + + test('viewContent - работает с разными типами контента', () async { + final postsModel = ViewModel(type: 'blog_posts', ids: [1]); + final plansModel = ViewModel(type: 'plans', ids: [2]); + final dietsModel = ViewModel(type: 'diets', ids: [3]); + final recipesModel = ViewModel(type: 'recipes', ids: [4]); + + when(mockViewsService.viewContent(viewModel: anyNamed('viewModel'))) + .thenAnswer((_) async => Future.value()); + + await viewsRepository.viewContent(viewModel: postsModel); + await viewsRepository.viewContent(viewModel: plansModel); + await viewsRepository.viewContent(viewModel: dietsModel); + await viewsRepository.viewContent(viewModel: recipesModel); + + verify(mockViewsService.viewContent(viewModel: postsModel)).called(1); + verify(mockViewsService.viewContent(viewModel: plansModel)).called(1); + verify(mockViewsService.viewContent(viewModel: dietsModel)).called(1); + verify(mockViewsService.viewContent(viewModel: recipesModel)).called(1); + }); + }); +} + diff --git a/test/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit_test.dart b/test/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit_test.dart new file mode 100644 index 00000000..c17e05bf --- /dev/null +++ b/test/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit_test.dart @@ -0,0 +1,42 @@ +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_state.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('NewMessagesCubit', () { + late NewMessagesCubit newMessagesCubit; + + setUp(() { + newMessagesCubit = NewMessagesCubit(); + }); + + tearDown(() async { + if (!newMessagesCubit.isClosed) { + await newMessagesCubit.close(); + } + }); + + test('начальное состояние - NewMessagesState.empty', () { + expect(newMessagesCubit.state, const NewMessagesState.empty()); + }); + + test('incrementCounter - увеличивает счётчик сообщений', () { + newMessagesCubit.incrementCounter(); + + expect(newMessagesCubit.state, isA()); + final loadedState = newMessagesCubit.state as NewMessagesStateLoaded; + expect(loadedState.messagesCount, 1); + }); + + test('incrementCounter - увеличивает счётчик несколько раз', () { + newMessagesCubit.incrementCounter(); + newMessagesCubit.incrementCounter(); + newMessagesCubit.incrementCounter(); + + expect(newMessagesCubit.state, isA()); + final loadedState = newMessagesCubit.state as NewMessagesStateLoaded; + expect(loadedState.messagesCount, 3); + }); + }); +} + diff --git a/test/features/feed/presentation/bloc/consultants_cubit/consultants_cubit_test.dart b/test/features/feed/presentation/bloc/consultants_cubit/consultants_cubit_test.dart new file mode 100644 index 00000000..f1f7fc0b --- /dev/null +++ b/test/features/feed/presentation/bloc/consultants_cubit/consultants_cubit_test.dart @@ -0,0 +1,103 @@ +import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_state.dart'; +import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'consultants_cubit_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('ConsultantsCubit', () { + late ConsultantsCubit consultantsCubit; + late MockFeedRepository mockFeedRepository; + + setUp(() { + mockFeedRepository = MockFeedRepository(); + consultantsCubit = ConsultantsCubit(feedRepository: mockFeedRepository); + }); + + tearDown(() async { + if (!consultantsCubit.isClosed) { + await consultantsCubit.close(); + } + }); + + test('начальное состояние - ConsultantsState', () { + expect(consultantsCubit.state, const ConsultantsState()); + }); + + test('getConsultants - успешная загрузка консультантов', () async { + final testConsultants = [ + FeedConsultantModel( + slug: 'consultant-1', + name: 'Консультант 1', + description: 'Описание консультанта 1', + totalScores: 100, + photo: 'https://example.com/icon1.jpg', + badges: [], + ratingPlace: 1, + ), + FeedConsultantModel( + slug: 'consultant-2', + name: 'Консультант 2', + description: 'Описание консультанта 2', + totalScores: 200, + photo: 'https://example.com/icon2.jpg', + badges: [], + ratingPlace: 2, + ), + ]; + + when(mockFeedRepository.getConsultants()).thenAnswer((_) async => testConsultants); + + await consultantsCubit.getConsultants(); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(consultantsCubit.state, isA()); + final loadedState = consultantsCubit.state as ConsultantsStateLoaded; + expect(loadedState.consultantsList.length, 2); + expect(loadedState.isLoading, false); + verify(mockFeedRepository.getConsultants()).called(1); + }); + + test('getConsultants - переход в состояние loading', () async { + when(mockFeedRepository.getConsultants()).thenAnswer((_) async { + await Future.delayed(const Duration(milliseconds: 50)); + return []; + }); + + final future = consultantsCubit.getConsultants(); + await Future.delayed(const Duration(milliseconds: 10)); + + expect(consultantsCubit.state, const ConsultantsState.loading()); + await future; + }); + + test('getConsultants - обработка ошибки', () async { + when(mockFeedRepository.getConsultants()).thenThrow(Exception('Ошибка загрузки')); + + await consultantsCubit.getConsultants(); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(consultantsCubit.state, isA()); + final errorState = consultantsCubit.state as ConsultantsStateError; + expect(errorState.errorMessage, contains('Exception')); + }); + + test('getConsultants - загрузка с пустым списком', () async { + when(mockFeedRepository.getConsultants()).thenAnswer((_) async => []); + + await consultantsCubit.getConsultants(); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(consultantsCubit.state, isA()); + final loadedState = consultantsCubit.state as ConsultantsStateLoaded; + expect(loadedState.consultantsList, isEmpty); + expect(loadedState.isLoading, false); + }); + }); +} +