mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-22 07:20:39 +00:00
added TimerInteractor
This commit is contained in:
parent
2fc24cccbb
commit
9929d2a5b8
7 changed files with 254 additions and 25 deletions
22
lib/interactors/timer_interactor.dart
Normal file
22
lib/interactors/timer_interactor.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
63
test/interactors/timer_interactor_test.dart
Normal file
63
test/interactors/timer_interactor_test.dart
Normal 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());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
144
test/screens/timer/screen_metering_golden_test.dart
Normal file
144
test/screens/timer/screen_metering_golden_test.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue