forked from eatvio/eatvio_app
chore(version): update app version to 1.3.5+85 and improve test coverage reporting in CI
This commit is contained in:
@@ -1,29 +1,37 @@
|
||||
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/models/view_model.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 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'views_cubit_test.mocks.dart';
|
||||
class MockViewsRepository extends Mock implements ViewsRepository {}
|
||||
|
||||
class FakeViewModel extends Fake implements ViewModel {}
|
||||
|
||||
@GenerateNiceMocks([MockSpec<ViewsRepository>()])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(FakeViewModel());
|
||||
});
|
||||
group('ViewsCubit', () {
|
||||
late ViewsCubit cubit;
|
||||
late MockViewsRepository mockViewsRepository;
|
||||
late MockViewsRepository mockRepository;
|
||||
|
||||
setUp(() {
|
||||
mockViewsRepository = MockViewsRepository();
|
||||
cubit = ViewsCubit(viewsRepository: mockViewsRepository);
|
||||
mockRepository = MockViewsRepository();
|
||||
cubit = ViewsCubit(viewsRepository: mockRepository);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
if (!cubit.isClosed) {
|
||||
await cubit.close();
|
||||
try {
|
||||
await cubit.close();
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки при закрытии (AppLifecycleListener может быть уже disposed)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -31,124 +39,58 @@ void main() {
|
||||
expect(cubit.state, isA<ViewsCubitState>());
|
||||
});
|
||||
|
||||
test('handleVisibility - добавляет ID при visibleFraction >= 0.5', () async {
|
||||
when(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))).thenAnswer((_) async => Future.value());
|
||||
test('handleVisibility - добавляет id когда visibleFraction >= 0.5', () async {
|
||||
when(() => mockRepository.viewContent(viewModel: any(named: 'viewModel')))
|
||||
.thenAnswer((_) async => {});
|
||||
|
||||
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);
|
||||
cubit.handleVisibility(
|
||||
visibleFraction: 0.6,
|
||||
id: 1,
|
||||
type: ViewableContentType.posts,
|
||||
);
|
||||
|
||||
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));
|
||||
expect(cubit.postsIds.contains(1), true);
|
||||
});
|
||||
|
||||
test('_sendViews - отправляет просмотры через repository', () async {
|
||||
when(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))).thenAnswer((_) async => Future.value());
|
||||
test('handleVisibility - не добавляет id если visibleFraction < 0.5', () {
|
||||
cubit.handleVisibility(
|
||||
visibleFraction: 0.3,
|
||||
id: 1,
|
||||
type: ViewableContentType.posts,
|
||||
);
|
||||
|
||||
// Добавляем 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));
|
||||
expect(cubit.postsIds.contains(1), false);
|
||||
});
|
||||
|
||||
test('close - отправляет оставшиеся просмотры и освобождает ресурсы', () async {
|
||||
when(mockViewsRepository.viewContent(viewModel: anyNamed('viewModel'))).thenAnswer((_) async => Future.value());
|
||||
test('handleVisibility - работает с разными типами контента', () async {
|
||||
when(() => mockRepository.viewContent(viewModel: any(named: 'viewModel')))
|
||||
.thenAnswer((_) async => {});
|
||||
|
||||
cubit.handleVisibility(
|
||||
visibleFraction: 0.6,
|
||||
id: 1,
|
||||
type: ViewableContentType.plans,
|
||||
);
|
||||
|
||||
// Добавляем ID
|
||||
cubit.handleVisibility(visibleFraction: 0.6, id: 1, type: ViewableContentType.posts);
|
||||
await Future.delayed(const Duration(milliseconds: 1100));
|
||||
|
||||
// Очищаем предыдущие взаимодействия перед проверкой
|
||||
clearInteractions(mockViewsRepository);
|
||||
expect(cubit.plansIds.contains(1), true);
|
||||
});
|
||||
|
||||
test('close отправляет оставшиеся просмотры', () async {
|
||||
when(() => mockRepository.viewContent(viewModel: any(named: 'viewModel')))
|
||||
.thenAnswer((_) async => {});
|
||||
|
||||
cubit.postsIds.add(1);
|
||||
cubit.plansIds.add(2);
|
||||
cubit.dietsIds.add(3);
|
||||
cubit.recipesIds.add(4);
|
||||
|
||||
// Закрываем 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')));
|
||||
verify(() => mockRepository.viewContent(viewModel: any(named: 'viewModel'))).called(greaterThan(0));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
21
test/core/views/cubit/views_state_test.dart
Normal file
21
test/core/views/cubit/views_state_test.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:eatvio_app/core/views/cubit/views_state.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('ViewsCubitState', () {
|
||||
test('creates initial state', () {
|
||||
final state = ViewsCubitState();
|
||||
|
||||
expect(state, isA<ViewsCubitState>());
|
||||
});
|
||||
|
||||
test('can create multiple instances', () {
|
||||
final state1 = ViewsCubitState();
|
||||
final state2 = ViewsCubitState();
|
||||
|
||||
expect(state1, isA<ViewsCubitState>());
|
||||
expect(state2, isA<ViewsCubitState>());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,65 +7,36 @@ import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'views_repository_test.mocks.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<ViewsService>()])
|
||||
@GenerateMocks([ViewsService])
|
||||
void main() {
|
||||
group('ViewsRepository', () {
|
||||
late ViewsRepository viewsRepository;
|
||||
late MockViewsService mockViewsService;
|
||||
late ViewsRepository repository;
|
||||
|
||||
setUp(() {
|
||||
mockViewsService = MockViewsService();
|
||||
viewsRepository = ViewsRepository(viewsService: mockViewsService);
|
||||
repository = ViewsRepository(viewsService: mockViewsService);
|
||||
});
|
||||
|
||||
test('viewContent - успешная отправка просмотров', () async {
|
||||
final viewModel = ViewModel(
|
||||
type: 'blog_posts',
|
||||
ids: [1, 2, 3],
|
||||
);
|
||||
test('viewContent calls viewsService.viewContent', () async {
|
||||
final viewModel = ViewModel(type: 'posts', ids: [1, 2, 3]);
|
||||
|
||||
when(mockViewsService.viewContent(viewModel: viewModel))
|
||||
.thenAnswer((_) async => Future.value());
|
||||
|
||||
await viewsRepository.viewContent(viewModel: viewModel);
|
||||
await repository.viewContent(viewModel: viewModel);
|
||||
|
||||
verify(mockViewsService.viewContent(viewModel: viewModel)).called(1);
|
||||
});
|
||||
|
||||
test('viewContent - пробрасывает исключение', () async {
|
||||
final viewModel = ViewModel(
|
||||
type: 'blog_posts',
|
||||
ids: [1, 2, 3],
|
||||
);
|
||||
test('viewContent rethrows exception when service throws', () async {
|
||||
final viewModel = ViewModel(type: 'posts', ids: [1, 2, 3]);
|
||||
final exception = Exception('Test error');
|
||||
|
||||
when(mockViewsService.viewContent(viewModel: viewModel))
|
||||
.thenThrow(Exception('Ошибка отправки'));
|
||||
.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);
|
||||
expect(() => repository.viewContent(viewModel: viewModel), throwsException);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user