added TimerInteractor

This commit is contained in:
Vadim 2024-05-03 12:43:45 +02:00
parent 2fc24cccbb
commit 9929d2a5b8
7 changed files with 254 additions and 25 deletions

View file

@ -0,0 +1,22 @@
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
class TimerInteractor {
final UserPreferencesService _userPreferencesService;
final HapticsService _hapticsService;
TimerInteractor(
this._userPreferencesService,
this._hapticsService,
);
/// Executes vibration if haptics are enabled in settings
Future<void> quickVibration() async {
if (_userPreferencesService.haptics) await _hapticsService.quickVibration();
}
/// Executes vibration if haptics are enabled in settings
Future<void> responseVibration() async {
if (_userPreferencesService.haptics) await _hapticsService.responseVibration();
}
}

View file

@ -1,30 +1,35 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/interactors/timer_interactor.dart';
import 'package:lightmeter/screens/timer/event_timer.dart'; import 'package:lightmeter/screens/timer/event_timer.dart';
import 'package:lightmeter/screens/timer/state_timer.dart'; import 'package:lightmeter/screens/timer/state_timer.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> { class TimerBloc extends Bloc<TimerEvent, TimerState> {
final MeteringInteractor _meteringInteractor; final TimerInteractor _timerInteractor;
final Duration duration; final Duration duration;
TimerBloc(this._meteringInteractor, this.duration) : super(const TimerStoppedState()) { TimerBloc(this._timerInteractor, this.duration) : super(const TimerStoppedState()) {
on<StartTimerEvent>(_onStartTimer); on<StartTimerEvent>(_onStartTimer);
on<SetTimeLeftEvent>(_onSetTimeLeft); on<TimerEndedEvent>(_onTimerEnded);
on<StopTimerEvent>(_onStopTimer); on<StopTimerEvent>(_onStopTimer);
on<ResetTimerEvent>(_onResetTimer); on<ResetTimerEvent>(_onResetTimer);
} }
Future<void> _onStartTimer(StartTimerEvent _, Emitter emit) async { Future<void> _onStartTimer(StartTimerEvent _, Emitter emit) async {
_timerInteractor.quickVibration();
emit(const TimerResumedState()); emit(const TimerResumedState());
} }
Future<void> _onSetTimeLeft(SetTimeLeftEvent event, Emitter emit) async { Future<void> _onTimerEnded(TimerEndedEvent event, Emitter emit) async {
emit(const TimerResumedState()); if (state is! TimerResetState) {
_timerInteractor.responseVibration();
emit(const TimerStoppedState());
}
} }
Future<void> _onStopTimer(StopTimerEvent _, Emitter emit) async { Future<void> _onStopTimer(StopTimerEvent _, Emitter emit) async {
_timerInteractor.quickVibration();
emit(const TimerStoppedState()); emit(const TimerStoppedState());
} }

View file

@ -10,10 +10,8 @@ class StopTimerEvent extends TimerEvent {
const StopTimerEvent(); const StopTimerEvent();
} }
class SetTimeLeftEvent extends TimerEvent { class TimerEndedEvent extends TimerEvent {
final Duration timeLeft; const TimerEndedEvent();
const SetTimeLeftEvent(this.timeLeft);
} }
class ResetTimerEvent extends TimerEvent { class ResetTimerEvent extends TimerEvent {

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/interactors/timer_interactor.dart';
import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/screens/timer/bloc_timer.dart'; import 'package:lightmeter/screens/timer/bloc_timer.dart';
import 'package:lightmeter/screens/timer/screen_timer.dart'; import 'package:lightmeter/screens/timer/screen_timer.dart';
@ -15,18 +16,14 @@ class TimerFlow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MeteringInteractorProvider( return TimerInteractorProvider(
data: MeteringInteractor( data: TimerInteractor(
ServicesProvider.of(context).userPreferencesService, ServicesProvider.of(context).userPreferencesService,
ServicesProvider.of(context).caffeineService,
ServicesProvider.of(context).hapticsService, ServicesProvider.of(context).hapticsService,
ServicesProvider.of(context).permissionsService, ),
ServicesProvider.of(context).lightSensorService,
ServicesProvider.of(context).volumeEventsService,
)..initialize(),
child: BlocProvider( child: BlocProvider(
create: (context) => TimerBloc( create: (context) => TimerBloc(
MeteringInteractorProvider.of(context), TimerInteractorProvider.of(context),
_duration, _duration,
), ),
child: TimerScreen( child: TimerScreen(
@ -38,19 +35,19 @@ class TimerFlow extends StatelessWidget {
} }
} }
class MeteringInteractorProvider extends InheritedWidget { class TimerInteractorProvider extends InheritedWidget {
final MeteringInteractor data; final TimerInteractor data;
const MeteringInteractorProvider({ const TimerInteractorProvider({
required this.data, required this.data,
required super.child, required super.child,
super.key, super.key,
}); });
static MeteringInteractor of(BuildContext context) { static TimerInteractor of(BuildContext context) {
return context.findAncestorWidgetOfExactType<MeteringInteractorProvider>()!.data; return context.findAncestorWidgetOfExactType<TimerInteractorProvider>()!.data;
} }
@override @override
bool updateShouldNotify(MeteringInteractorProvider oldWidget) => false; bool updateShouldNotify(TimerInteractorProvider oldWidget) => false;
} }

View file

@ -39,7 +39,7 @@ class _TimerScreenState extends State<TimerScreen> with TickerProviderStateMixin
timelineAnimation = Tween<double>(begin: 1, end: 0).animate(timelineController); timelineAnimation = Tween<double>(begin: 1, end: 0).animate(timelineController);
timelineController.addStatusListener((status) { timelineController.addStatusListener((status) {
if (status == AnimationStatus.completed) { if (status == AnimationStatus.completed) {
context.read<TimerBloc>().add(const StopTimerEvent()); context.read<TimerBloc>().add(const TimerEndedEvent());
} }
}); });

View file

@ -0,0 +1,63 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/timer_interactor.dart';
import 'package:mocktail/mocktail.dart';
class _MockUserPreferencesService extends Mock implements UserPreferencesService {}
class _MockHapticsService extends Mock implements HapticsService {}
void main() {
late _MockUserPreferencesService mockUserPreferencesService;
late _MockHapticsService mockHapticsService;
late TimerInteractor interactor;
setUp(() {
mockUserPreferencesService = _MockUserPreferencesService();
mockHapticsService = _MockHapticsService();
interactor = TimerInteractor(
mockUserPreferencesService,
mockHapticsService,
);
});
group(
'Haptics',
() {
test('quickVibration() - true', () async {
when(() => mockUserPreferencesService.haptics).thenReturn(true);
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
interactor.quickVibration();
verify(() => mockUserPreferencesService.haptics).called(1);
verify(() => mockHapticsService.quickVibration()).called(1);
});
test('quickVibration() - false', () async {
when(() => mockUserPreferencesService.haptics).thenReturn(false);
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
interactor.quickVibration();
verify(() => mockUserPreferencesService.haptics).called(1);
verifyNever(() => mockHapticsService.quickVibration());
});
test('responseVibration() - true', () async {
when(() => mockUserPreferencesService.haptics).thenReturn(true);
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
interactor.responseVibration();
verify(() => mockUserPreferencesService.haptics).called(1);
verify(() => mockHapticsService.responseVibration()).called(1);
});
test('responseVibration() - false', () async {
when(() => mockUserPreferencesService.haptics).thenReturn(false);
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
interactor.responseVibration();
verify(() => mockUserPreferencesService.haptics).called(1);
verifyNever(() => mockHapticsService.responseVibration());
});
},
);
}

View file

@ -0,0 +1,144 @@
import 'dart:convert';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/metering/flow_metering.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../integration_test/utils/finder_actions.dart';
import '../../../integration_test/utils/platform_channel_mock.dart';
import '../../application_mock.dart';
class _MeteringScreenConfig {
final IAPProductStatus iapProductStatus;
final EvSourceType evSourceType;
_MeteringScreenConfig(
this.iapProductStatus,
this.evSourceType,
);
@override
String toString() {
final buffer = StringBuffer();
buffer.write(iapProductStatus.toString().split('.')[1]);
buffer.write(' - ');
buffer.write(evSourceType.toString().split('.')[1]);
return buffer.toString();
}
}
final _testScenarios = [IAPProductStatus.purchased, IAPProductStatus.purchasable].expand(
(iapProductStatus) => EvSourceType.values.map(
(evSourceType) => _MeteringScreenConfig(iapProductStatus, evSourceType),
),
);
void main() {
Future<void> setEvSource(WidgetTester tester, Key scenarioWidgetKey, EvSourceType evSourceType) async {
final flow = find.descendant(
of: find.byKey(scenarioWidgetKey),
matching: find.byType(MeteringFlow),
);
final BuildContext context = tester.element(flow);
if (UserPreferencesProvider.evSourceTypeOf(context) != evSourceType) {
UserPreferencesProvider.of(context).toggleEvSourceType();
}
await tester.pumpAndSettle();
}
Future<void> setTheme(WidgetTester tester, Key scenarioWidgetKey, ThemeType themeType) async {
final flow = find.descendant(
of: find.byKey(scenarioWidgetKey),
matching: find.byType(MeteringFlow),
);
final BuildContext context = tester.element(flow);
UserPreferencesProvider.of(context).setThemeType(themeType);
await tester.pumpAndSettle();
}
Future<void> takePhoto(WidgetTester tester, Key scenarioWidgetKey) async {
final button = find.descendant(
of: find.byKey(scenarioWidgetKey),
matching: find.measureButton(),
);
await tester.tap(button);
await tester.pump(const Duration(seconds: 2)); // wait for circular progress indicator
await tester.pump(const Duration(seconds: 1)); // wait for circular progress indicator
await tester.pumpAndSettle();
}
Future<void> toggleIncidentMetering(WidgetTester tester, Key scenarioWidgetKey, double ev) async {
final button = find.descendant(
of: find.byKey(scenarioWidgetKey),
matching: find.measureButton(),
);
await tester.tap(button);
await sendMockIncidentEv(ev);
await tester.tap(button);
await tester.pumpAndSettle();
}
setUpAll(() {
SharedPreferences.setMockInitialValues({
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
UserPreferencesService.meteringScreenLayoutKey: json.encode(
{
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
}.toJson(),
),
});
});
testGoldens(
'MeteringScreen golden test',
(tester) async {
final builder = DeviceBuilder();
for (final scenario in _testScenarios) {
builder.addScenario(
name: scenario.toString(),
widget: _MockMeteringFlow(productStatus: scenario.iapProductStatus),
onCreate: (scenarioWidgetKey) async {
await setEvSource(tester, scenarioWidgetKey, scenario.evSourceType);
if (scenarioWidgetKey.toString().contains('Dark')) {
await setTheme(tester, scenarioWidgetKey, ThemeType.dark);
}
if (scenario.evSourceType == EvSourceType.camera) {
await takePhoto(tester, scenarioWidgetKey);
} else {
await toggleIncidentMetering(tester, scenarioWidgetKey, 7.3);
}
},
);
}
await tester.pumpDeviceBuilder(builder);
await screenMatchesGolden(
tester,
'metering_screen',
);
},
);
}
class _MockMeteringFlow extends StatelessWidget {
final IAPProductStatus productStatus;
const _MockMeteringFlow({required this.productStatus});
@override
Widget build(BuildContext context) {
return GoldenTestApplicationMock(
productStatus: productStatus,
child: const MeteringFlow(),
);
}
}