forked from eatvio/eatvio_app
feat(diary): tests
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import 'dart:async';
|
||||
|
||||
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:eatvio_app/core/local_db/services/glossary_sync_runner.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/glossary_sync_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/glossary_sync_state.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
@GenerateNiceMocks([
|
||||
MockSpec<GlossarySyncService>(),
|
||||
MockSpec<InternetConnectionCubit>(),
|
||||
])
|
||||
import 'glossary_sync_cubit_test.mocks.dart';
|
||||
|
||||
void main() {
|
||||
late MockGlossarySyncService mockSyncService;
|
||||
late MockInternetConnectionCubit mockConnectionCubit;
|
||||
|
||||
setUp(() {
|
||||
mockSyncService = MockGlossarySyncService();
|
||||
mockConnectionCubit = MockInternetConnectionCubit();
|
||||
when(mockConnectionCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: false));
|
||||
});
|
||||
|
||||
GlossarySyncCubit buildCubit() => GlossarySyncCubit(
|
||||
syncService: mockSyncService,
|
||||
connectionCubit: mockConnectionCubit,
|
||||
);
|
||||
|
||||
group('GlossarySyncCubit', () {
|
||||
test('initial state is idle', () {
|
||||
final cubit = buildCubit();
|
||||
expect(cubit.state, const GlossarySyncState.idle());
|
||||
cubit.close();
|
||||
});
|
||||
|
||||
test('syncNow emits completed with count when sync returns > 0', () async {
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenAnswer((_) async => 100);
|
||||
|
||||
final cubit = buildCubit();
|
||||
final states = <GlossarySyncState>[];
|
||||
final sub = cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.syncNow();
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(states.last, isA<GlossarySyncState>());
|
||||
await sub.cancel();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('syncNow emits upToDate when sync returns 0', () async {
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenAnswer((_) async => 0);
|
||||
|
||||
final cubit = buildCubit();
|
||||
final states = <GlossarySyncState>[];
|
||||
final sub = cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.syncNow();
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(states.last, const GlossarySyncState.upToDate());
|
||||
await sub.cancel();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('syncNow emits error on exception', () async {
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenThrow(Exception('Test error'));
|
||||
|
||||
final cubit = buildCubit();
|
||||
final states = <GlossarySyncState>[];
|
||||
final sub = cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.syncNow();
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(states.isNotEmpty, true);
|
||||
await sub.cancel();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('syncNow emits error with socket message', () async {
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenThrow(Exception('SocketException: connection refused'));
|
||||
|
||||
final cubit = buildCubit();
|
||||
final states = <GlossarySyncState>[];
|
||||
final sub = cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.syncNow();
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(states.isNotEmpty, true);
|
||||
await sub.cancel();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('syncNow emits error with timeout message', () async {
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenThrow(TimeoutException('timeout'));
|
||||
|
||||
final cubit = buildCubit();
|
||||
final states = <GlossarySyncState>[];
|
||||
final sub = cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.syncNow();
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(states.isNotEmpty, true);
|
||||
await sub.cancel();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('start() listens to connectionCubit', () {
|
||||
final cubit = buildCubit();
|
||||
cubit.start();
|
||||
verify(mockConnectionCubit.stream).called(1);
|
||||
cubit.close();
|
||||
});
|
||||
|
||||
test('duplicate syncNow calls are ignored', () async {
|
||||
final completer = Completer<int>();
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenAnswer((_) => completer.future);
|
||||
|
||||
final cubit = buildCubit();
|
||||
cubit.syncNow();
|
||||
cubit.syncNow(); // Should be ignored
|
||||
|
||||
completer.complete(5);
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
verify(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).called(1);
|
||||
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('stage callbacks emit correct states', () async {
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenAnswer((invocation) async {
|
||||
final stageCallback = invocation.namedArguments[#onStageChanged] as Function(GlossarySyncStage);
|
||||
final downloadCallback = invocation.namedArguments[#onDownloadProgress] as Function(double);
|
||||
final processingCallback = invocation.namedArguments[#onProcessingProgress] as Function(int, int);
|
||||
|
||||
stageCallback(GlossarySyncStage.checking);
|
||||
stageCallback(GlossarySyncStage.downloading);
|
||||
downloadCallback(0.5);
|
||||
stageCallback(GlossarySyncStage.processing);
|
||||
processingCallback(50, 100);
|
||||
stageCallback(GlossarySyncStage.saving);
|
||||
|
||||
return 100;
|
||||
});
|
||||
|
||||
final cubit = buildCubit();
|
||||
final states = <GlossarySyncState>[];
|
||||
final sub = cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.syncNow();
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
expect(states.length, greaterThanOrEqualTo(4));
|
||||
await sub.cancel();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('close cancels subscriptions', () async {
|
||||
final cubit = buildCubit();
|
||||
cubit.start();
|
||||
await cubit.close();
|
||||
// Should not crash
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/glossary_sync_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/glossary_sync_state.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/glossary_sync_banner_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<GlossarySyncCubit>()])
|
||||
import 'glossary_sync_banner_widget_test.mocks.dart';
|
||||
|
||||
void main() {
|
||||
late MockGlossarySyncCubit mockCubit;
|
||||
|
||||
setUp(() {
|
||||
mockCubit = MockGlossarySyncCubit();
|
||||
when(mockCubit.stream).thenAnswer((_) => const Stream<GlossarySyncState>.empty());
|
||||
when(mockCubit.close()).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
Widget buildWidget() {
|
||||
return MaterialApp(
|
||||
home: BlocProvider<GlossarySyncCubit>.value(
|
||||
value: mockCubit,
|
||||
child: const Scaffold(
|
||||
body: GlossarySyncBannerWidget(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
group('GlossarySyncBannerWidget', () {
|
||||
testWidgets('idle state shows nothing', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.idle());
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.byType(GlossarySyncBannerWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('upToDate state shows nothing', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.upToDate());
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.byType(GlossarySyncBannerWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('checking state shows checking text', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.checking());
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Проверяю'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('downloading with progress shows percentage', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.downloading(progress: 0.45));
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('45%'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('downloading without progress shows dots', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.downloading());
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Загрузка'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('processing shows progress', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.processing(processed: 12500, total: 200000));
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Обработка'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('processing with 0 total shows generic text', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.processing(processed: 0, total: 0));
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Обработка данных'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('completed shows count', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.completed(syncedCount: 200000));
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Синхронизировано'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('error shows error message', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.error(message: 'Ошибка синхронизации'));
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Ошибка'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
|
||||
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/offline_banner_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<DiaryCubit>()])
|
||||
import 'offline_banner_widget_test.mocks.dart';
|
||||
|
||||
DiaryDayEntity _makeDiary() => DiaryDayEntity(
|
||||
id: 1,
|
||||
date: '2025-01-01',
|
||||
wakeup: '06:30:00',
|
||||
retire: '22:30:00',
|
||||
closed: 0,
|
||||
comment: '',
|
||||
typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: false, diaryState: null),
|
||||
meals: [
|
||||
const MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: []),
|
||||
],
|
||||
chosenPlanId: null,
|
||||
dayOfChosenDiet: null,
|
||||
diet: null,
|
||||
);
|
||||
|
||||
void main() {
|
||||
late MockDiaryCubit mockDiaryCubit;
|
||||
|
||||
setUp(() {
|
||||
mockDiaryCubit = MockDiaryCubit();
|
||||
when(mockDiaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(mockDiaryCubit.close()).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
Widget buildWidget() {
|
||||
return MaterialApp(
|
||||
home: BlocProvider<DiaryCubit>.value(
|
||||
value: mockDiaryCubit,
|
||||
child: const Scaffold(
|
||||
body: OfflineBannerWidget(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
group('OfflineBannerWidget', () {
|
||||
testWidgets('shows nothing when online and no pending ops', (tester) async {
|
||||
when(mockDiaryCubit.state).thenReturn(
|
||||
DiaryState.loaded(
|
||||
diary: _makeDiary(),
|
||||
selectedDay: DateTime(2025, 1, 1),
|
||||
selectedMeal: 1,
|
||||
isOfflineMode: false,
|
||||
pendingOpsCount: 0,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
// SizedBox.shrink should be rendered — no banner text visible
|
||||
expect(find.textContaining('Офлайн'), findsNothing);
|
||||
expect(find.textContaining('Синхронизация'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows offline banner with pending ops', (tester) async {
|
||||
when(mockDiaryCubit.state).thenReturn(
|
||||
DiaryState.loaded(
|
||||
diary: _makeDiary(),
|
||||
selectedDay: DateTime(2025, 1, 1),
|
||||
selectedMeal: 1,
|
||||
isOfflineMode: true,
|
||||
pendingOpsCount: 3,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Офлайн'), findsOneWidget);
|
||||
expect(find.textContaining('3'), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('shows offline without pending ops', (tester) async {
|
||||
when(mockDiaryCubit.state).thenReturn(
|
||||
DiaryState.loaded(
|
||||
diary: _makeDiary(),
|
||||
selectedDay: DateTime(2025, 1, 1),
|
||||
selectedMeal: 1,
|
||||
isOfflineMode: true,
|
||||
pendingOpsCount: 0,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('кэша'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows sync banner when online with pending ops', (tester) async {
|
||||
when(mockDiaryCubit.state).thenReturn(
|
||||
DiaryState.loaded(
|
||||
diary: _makeDiary(),
|
||||
selectedDay: DateTime(2025, 1, 1),
|
||||
selectedMeal: 1,
|
||||
isOfflineMode: false,
|
||||
pendingOpsCount: 5,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Синхронизация'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('hides when state is not loaded', (tester) async {
|
||||
when(mockDiaryCubit.state).thenReturn(const DiaryState.loading());
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
// SizedBox.shrink should be rendered
|
||||
expect(find.textContaining('Офлайн'), findsNothing);
|
||||
expect(find.textContaining('Синхронизация'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user