mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-24 16:30:40 +00:00
ML-62 Bloc's tests (#78)
* removed redundant `UserPreferencesService` from `MeteringBloc` * wip * post-merge fixes * `MeasureEvent` tests * `MeasureEvent` tests revision * `MeasureEvent` tests added timeout * added stubs for other `MeteringBloc` events * rewritten `MeteringBloc` logic * wip * `IsoChangedEvent` tests * refined `IsoChangedEvent` tests * `NdChangedEvent` tests * `FilmChangedEvent` tests * `MeteringCommunicationBloc` tests * added test run to ci * overriden `==` for `MeasuredState` * `LuxMeteringEvent` tests * refined `LuxMeteringEvent` tests * rename * wip * wip * `InitializeEvent`/`DeinitializeEvent` tests * clamp minZoomLevel * fixed `MeteringCommunicationBloc` tests * wip * `ZoomChangedEvent` tests * `ExposureOffsetChangedEvent`/`ExposureOffsetResetEvent` tests * renamed test groups * added test coverage script * improved `CameraContainerBloc` test coverage * `EquipmentProfileChangedEvent` tests * verify response vibration * fixed running all tests * `MeteringCommunicationBloc` equality tests * `CameraContainerBloc` equality tests * removed generated code from coverage
This commit is contained in:
parent
0013125d68
commit
74d0a7101c
31 changed files with 1872 additions and 280 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
@ -51,3 +51,6 @@ jobs:
|
|||
|
||||
- name: Analyze project source
|
||||
run: flutter analyze lib --fatal-infos
|
||||
|
||||
- name: Run tests
|
||||
run: flutter test
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -58,3 +58,5 @@ android/app/google-services.json
|
|||
ios/firebase_app_id_file.json
|
||||
ios/Runner/GoogleService-Info.plist
|
||||
lib/firebase_options.dart
|
||||
|
||||
coverage/
|
|
@ -31,6 +31,7 @@ class MeteringInteractor {
|
|||
|
||||
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
|
||||
double get lightSensorEvCalibration => _userPreferencesService.lightSensorEvCalibration;
|
||||
bool get isHapticsEnabled => _userPreferencesService.haptics;
|
||||
|
||||
IsoValue get iso => _userPreferencesService.iso;
|
||||
set iso(IsoValue value) => _userPreferencesService.iso = value;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
|
@ -15,115 +15,130 @@ import 'package:lightmeter/screens/metering/state_metering.dart';
|
|||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||
final MeteringCommunicationBloc _communicationBloc;
|
||||
final MeteringInteractor _meteringInteractor;
|
||||
final MeteringCommunicationBloc _communicationBloc;
|
||||
late final StreamSubscription<communication_states.ScreenState> _communicationSubscription;
|
||||
|
||||
List<ApertureValue> get _apertureValues =>
|
||||
_equipmentProfileData.apertureValues.whereStopType(stopType);
|
||||
List<ShutterSpeedValue> get _shutterSpeedValues =>
|
||||
_equipmentProfileData.shutterSpeedValues.whereStopType(stopType);
|
||||
|
||||
EquipmentProfileData _equipmentProfileData;
|
||||
StopType stopType;
|
||||
|
||||
late IsoValue _iso = _meteringInteractor.iso;
|
||||
late NdValue _nd = _meteringInteractor.ndFilter;
|
||||
late Film _film = _meteringInteractor.film;
|
||||
double? _ev100 = 0.0;
|
||||
bool _isMeteringInProgress = false;
|
||||
|
||||
MeteringBloc(
|
||||
this._communicationBloc,
|
||||
this._meteringInteractor,
|
||||
this._equipmentProfileData,
|
||||
this.stopType,
|
||||
this._communicationBloc,
|
||||
) : super(
|
||||
MeteringDataState(
|
||||
ev: null,
|
||||
ev100: null,
|
||||
film: _meteringInteractor.film,
|
||||
iso: _meteringInteractor.iso,
|
||||
nd: _meteringInteractor.ndFilter,
|
||||
exposurePairs: const [],
|
||||
continuousMetering: false,
|
||||
isMetering: false,
|
||||
),
|
||||
) {
|
||||
_communicationSubscription = _communicationBloc.stream
|
||||
.where((state) => state is communication_states.ScreenState)
|
||||
.map((state) => state as communication_states.ScreenState)
|
||||
.listen(_onCommunicationState);
|
||||
.listen(onCommunicationState);
|
||||
|
||||
on<EquipmentProfileChangedEvent>(_onEquipmentProfileChanged);
|
||||
on<StopTypeChangedEvent>(_onStopTypeChanged);
|
||||
on<FilmChangedEvent>(_onFilmChanged);
|
||||
on<IsoChangedEvent>(_onIsoChanged);
|
||||
on<NdChangedEvent>(_onNdChanged);
|
||||
on<MeasureEvent>(_onMeasure);
|
||||
on<MeasureEvent>(_onMeasure, transformer: droppable());
|
||||
on<MeasuredEvent>(_onMeasured);
|
||||
on<MeasureErrorEvent>(_onMeasureError);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTransition(Transition<MeteringEvent, MeteringState> transition) {
|
||||
super.onTransition(transition);
|
||||
if (transition.nextState is MeteringDataState) {
|
||||
final nextState = transition.nextState as MeteringDataState;
|
||||
if (transition.currentState is LoadingState ||
|
||||
transition.currentState is MeteringDataState &&
|
||||
(transition.currentState as MeteringDataState).ev != nextState.ev) {
|
||||
if (nextState.hasError) {
|
||||
_meteringInteractor.errorVibration();
|
||||
} else {
|
||||
_meteringInteractor.responseVibration();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _communicationSubscription.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _onCommunicationState(communication_states.ScreenState communicationState) {
|
||||
@visibleForTesting
|
||||
void onCommunicationState(communication_states.ScreenState communicationState) {
|
||||
if (communicationState is communication_states.MeasuredState) {
|
||||
_isMeteringInProgress = communicationState is communication_states.MeteringInProgressState;
|
||||
_handleEv100(communicationState.ev100);
|
||||
}
|
||||
}
|
||||
|
||||
void _onStopTypeChanged(StopTypeChangedEvent event, Emitter emit) {
|
||||
if (stopType != event.stopType) {
|
||||
stopType = event.stopType;
|
||||
_updateMeasurements();
|
||||
_handleEv100(
|
||||
communicationState.ev100,
|
||||
isMetering: communicationState is communication_states.MeteringInProgressState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onEquipmentProfileChanged(EquipmentProfileChangedEvent event, Emitter emit) {
|
||||
_equipmentProfileData = event.equipmentProfileData;
|
||||
bool willUpdateMeasurements = false;
|
||||
|
||||
/// Update selected ISO value, if selected equipment profile
|
||||
/// Update selected ISO value and discard selected film, if selected equipment profile
|
||||
/// doesn't contain currently selected value
|
||||
if (!event.equipmentProfileData.isoValues.any((v) => _iso.value == v.value)) {
|
||||
IsoValue iso = state.iso;
|
||||
Film film = state.film;
|
||||
if (!event.equipmentProfileData.isoValues.any((v) => state.iso.value == v.value)) {
|
||||
_meteringInteractor.iso = event.equipmentProfileData.isoValues.first;
|
||||
_iso = event.equipmentProfileData.isoValues.first;
|
||||
willUpdateMeasurements &= true;
|
||||
iso = event.equipmentProfileData.isoValues.first;
|
||||
_meteringInteractor.film = Film.values.first;
|
||||
film = Film.values.first;
|
||||
willUpdateMeasurements = true;
|
||||
}
|
||||
|
||||
/// The same for ND filter
|
||||
if (!event.equipmentProfileData.ndValues.any((v) => _nd.value == v.value)) {
|
||||
NdValue nd = state.nd;
|
||||
if (!event.equipmentProfileData.ndValues.any((v) => state.nd.value == v.value)) {
|
||||
_meteringInteractor.ndFilter = event.equipmentProfileData.ndValues.first;
|
||||
_nd = event.equipmentProfileData.ndValues.first;
|
||||
willUpdateMeasurements &= true;
|
||||
nd = event.equipmentProfileData.ndValues.first;
|
||||
willUpdateMeasurements = true;
|
||||
}
|
||||
|
||||
if (willUpdateMeasurements) {
|
||||
_updateMeasurements();
|
||||
emit(
|
||||
MeteringDataState(
|
||||
ev100: state.ev100,
|
||||
film: film,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
isMetering: state.isMetering,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFilmChanged(FilmChangedEvent event, Emitter emit) {
|
||||
if (_film.name != event.data.name) {
|
||||
_meteringInteractor.film = event.data;
|
||||
_film = event.data;
|
||||
if (state.film.name != event.film.name) {
|
||||
_meteringInteractor.film = event.film;
|
||||
|
||||
/// Find `IsoValue` with matching value
|
||||
IsoValue iso = state.iso;
|
||||
if (state.iso.value != event.film.iso && event.film != const Film.other()) {
|
||||
iso = IsoValue.values.firstWhere(
|
||||
(e) => e.value == event.film.iso,
|
||||
orElse: () => state.iso,
|
||||
);
|
||||
_meteringInteractor.iso = iso;
|
||||
}
|
||||
|
||||
/// If user selects 'Other' film we preserve currently selected ISO
|
||||
/// and therefore only discard reciprocity formula
|
||||
if (_iso.value != event.data.iso && event.data != const Film.other()) {
|
||||
final newIso = IsoValue.values.firstWhere(
|
||||
(e) => e.value == event.data.iso,
|
||||
orElse: () => _iso,
|
||||
);
|
||||
_meteringInteractor.iso = newIso;
|
||||
_iso = newIso;
|
||||
}
|
||||
|
||||
_updateMeasurements();
|
||||
emit(
|
||||
MeteringDataState(
|
||||
ev100: state.ev100,
|
||||
film: event.film,
|
||||
iso: iso,
|
||||
nd: state.nd,
|
||||
isMetering: state.isMetering,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,131 +147,77 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
/// because, for example, Fomapan 400 and any Ilford 400
|
||||
/// have different reciprocity formulas
|
||||
_meteringInteractor.film = Film.values.first;
|
||||
_film = Film.values.first;
|
||||
|
||||
if (_iso != event.isoValue) {
|
||||
if (state.iso != event.isoValue) {
|
||||
_meteringInteractor.iso = event.isoValue;
|
||||
_iso = event.isoValue;
|
||||
_updateMeasurements();
|
||||
emit(
|
||||
MeteringDataState(
|
||||
ev100: state.ev100,
|
||||
film: Film.values.first,
|
||||
iso: event.isoValue,
|
||||
nd: state.nd,
|
||||
isMetering: state.isMetering,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onNdChanged(NdChangedEvent event, Emitter emit) {
|
||||
if (_nd != event.ndValue) {
|
||||
if (state.nd != event.ndValue) {
|
||||
_meteringInteractor.ndFilter = event.ndValue;
|
||||
_nd = event.ndValue;
|
||||
_updateMeasurements();
|
||||
emit(
|
||||
MeteringDataState(
|
||||
ev100: state.ev100,
|
||||
film: state.film,
|
||||
iso: state.iso,
|
||||
nd: event.ndValue,
|
||||
isMetering: state.isMetering,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onMeasure(MeasureEvent _, Emitter emit) {
|
||||
_meteringInteractor.quickVibration();
|
||||
_communicationBloc.add(const communication_events.MeasureEvent());
|
||||
_isMeteringInProgress = true;
|
||||
emit(
|
||||
LoadingState(
|
||||
film: _film,
|
||||
iso: _iso,
|
||||
nd: _nd,
|
||||
film: state.film,
|
||||
iso: state.iso,
|
||||
nd: state.nd,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateMeasurements() => _handleEv100(_ev100);
|
||||
|
||||
void _handleEv100(double? ev100) {
|
||||
void _handleEv100(double? ev100, {required bool isMetering}) {
|
||||
if (ev100 == null || ev100.isNaN || ev100.isInfinite) {
|
||||
add(const MeasureErrorEvent());
|
||||
add(MeasureErrorEvent(isMetering: isMetering));
|
||||
} else {
|
||||
add(MeasuredEvent(ev100));
|
||||
add(MeasuredEvent(ev100, isMetering: isMetering));
|
||||
}
|
||||
}
|
||||
|
||||
void _onMeasured(MeasuredEvent event, Emitter emit) {
|
||||
_meteringInteractor.responseVibration();
|
||||
_ev100 = event.ev100;
|
||||
final ev = event.ev100 + log2(_iso.value / 100) - _nd.stopReduction;
|
||||
emit(
|
||||
MeteringDataState(
|
||||
ev: ev,
|
||||
film: _film,
|
||||
iso: _iso,
|
||||
nd: _nd,
|
||||
exposurePairs: _buildExposureValues(ev),
|
||||
continuousMetering: _isMeteringInProgress,
|
||||
ev100: event.ev100,
|
||||
film: state.film,
|
||||
iso: state.iso,
|
||||
nd: state.nd,
|
||||
isMetering: event.isMetering,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onMeasureError(MeasureErrorEvent _, Emitter emit) {
|
||||
_meteringInteractor.errorVibration();
|
||||
_ev100 = null;
|
||||
void _onMeasureError(MeasureErrorEvent event, Emitter emit) {
|
||||
emit(
|
||||
MeteringDataState(
|
||||
ev: null,
|
||||
film: _film,
|
||||
iso: _iso,
|
||||
nd: _nd,
|
||||
exposurePairs: const [],
|
||||
continuousMetering: _isMeteringInProgress,
|
||||
ev100: null,
|
||||
film: state.film,
|
||||
iso: state.iso,
|
||||
nd: state.nd,
|
||||
isMetering: event.isMetering,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<ExposurePair> _buildExposureValues(double ev) {
|
||||
if (ev.isNaN || ev.isInfinite) {
|
||||
return List.empty();
|
||||
}
|
||||
|
||||
/// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3
|
||||
final int evSteps = (ev * (stopType.index + 1)).round();
|
||||
|
||||
/// Basically we use 1" shutter speed as an anchor point for building the exposure pairs list.
|
||||
/// But user can exclude this value from the list using custom equipment profile.
|
||||
/// So we have to restore the index of the anchor value.
|
||||
const ShutterSpeedValue anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full);
|
||||
int anchorIndex = _shutterSpeedValues.indexOf(anchorShutterSpeed);
|
||||
if (anchorIndex < 0) {
|
||||
final filteredFullList = ShutterSpeedValue.values.whereStopType(stopType);
|
||||
final customListStartIndex = filteredFullList.indexOf(_shutterSpeedValues.first);
|
||||
final fullListAnchor = filteredFullList.indexOf(anchorShutterSpeed);
|
||||
if (customListStartIndex < fullListAnchor) {
|
||||
/// This means, that user excluded anchor value at the end,
|
||||
/// i.e. all shutter speed values are shorter than 1".
|
||||
anchorIndex = fullListAnchor - customListStartIndex;
|
||||
} else {
|
||||
/// In case user excludes anchor value at the start,
|
||||
/// we can do no adjustment.
|
||||
}
|
||||
}
|
||||
final int evOffset = anchorIndex - evSteps;
|
||||
|
||||
late final int apertureOffset;
|
||||
late final int shutterSpeedOffset;
|
||||
if (evOffset >= 0) {
|
||||
apertureOffset = 0;
|
||||
shutterSpeedOffset = evOffset;
|
||||
} else {
|
||||
apertureOffset = -evOffset;
|
||||
shutterSpeedOffset = 0;
|
||||
}
|
||||
|
||||
final int itemsCount = min(
|
||||
_apertureValues.length + shutterSpeedOffset,
|
||||
_shutterSpeedValues.length + apertureOffset,
|
||||
) -
|
||||
max(apertureOffset, shutterSpeedOffset);
|
||||
|
||||
if (itemsCount < 0) {
|
||||
return List.empty();
|
||||
}
|
||||
return List.generate(
|
||||
itemsCount,
|
||||
(index) => ExposurePair(
|
||||
_apertureValues[index + apertureOffset],
|
||||
_film.reciprocityFailure(_shutterSpeedValues[index + shutterSpeedOffset]),
|
||||
),
|
||||
growable: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,28 @@ abstract class MeasuredEvent extends SourceEvent {
|
|||
|
||||
class MeteringInProgressEvent extends MeasuredEvent {
|
||||
const MeteringInProgressEvent(super.ev100);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is MeteringInProgressEvent && other.ev100 == ev100;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(ev100, runtimeType);
|
||||
}
|
||||
|
||||
class MeteringEndedEvent extends MeasuredEvent {
|
||||
const MeteringEndedEvent(super.ev100);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is MeteringEndedEvent && other.ev100 == ev100;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(ev100, runtimeType);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
abstract class MeteringCommunicationState {
|
||||
sealed class MeteringCommunicationState {
|
||||
const MeteringCommunicationState();
|
||||
}
|
||||
|
||||
|
@ -6,11 +6,11 @@ class InitState extends MeteringCommunicationState {
|
|||
const InitState();
|
||||
}
|
||||
|
||||
abstract class SourceState extends MeteringCommunicationState {
|
||||
sealed class SourceState extends MeteringCommunicationState {
|
||||
const SourceState();
|
||||
}
|
||||
|
||||
abstract class ScreenState extends MeteringCommunicationState {
|
||||
sealed class ScreenState extends MeteringCommunicationState {
|
||||
const ScreenState();
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ class MeasureState extends SourceState {
|
|||
const MeasureState();
|
||||
}
|
||||
|
||||
abstract class MeasuredState extends ScreenState {
|
||||
sealed class MeasuredState extends ScreenState {
|
||||
final double? ev100;
|
||||
|
||||
const MeasuredState(this.ev100);
|
||||
|
@ -26,8 +26,28 @@ abstract class MeasuredState extends ScreenState {
|
|||
|
||||
class MeteringInProgressState extends MeasuredState {
|
||||
const MeteringInProgressState(super.ev100);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is MeteringInProgressState && other.ev100 == ev100;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(ev100, runtimeType);
|
||||
}
|
||||
|
||||
class MeteringEndedState extends MeasuredState {
|
||||
const MeteringEndedState(super.ev100);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is MeteringEndedState && other.ev100 == ev100;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(ev100, runtimeType);
|
||||
}
|
||||
|
|
|
@ -6,13 +6,11 @@ import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dar
|
|||
class MeteringMeasureButton extends StatefulWidget {
|
||||
final double? ev;
|
||||
final bool isMetering;
|
||||
final bool hasError;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const MeteringMeasureButton({
|
||||
required this.ev,
|
||||
required this.isMetering,
|
||||
required this.hasError,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
@ -34,58 +32,49 @@ class _MeteringMeasureButtonState extends State<MeteringMeasureButton> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
ignoring: widget.isMetering && widget.ev == null && !widget.hasError,
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
onTapDown: (_) {
|
||||
setState(() {
|
||||
_isPressed = true;
|
||||
});
|
||||
},
|
||||
onTapUp: (_) {
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
},
|
||||
onTapCancel: () {
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
},
|
||||
child: SizedBox.fromSize(
|
||||
size: const Size.square(Dimens.grid72),
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: AnimatedScale(
|
||||
duration: Dimens.durationS,
|
||||
scale: _isPressed ? 0.9 : 1.0,
|
||||
child: FilledCircle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
size: Dimens.grid72 - Dimens.grid8,
|
||||
child: Center(
|
||||
child: widget.hasError
|
||||
? Icon(
|
||||
Icons.error_outline,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
size: Dimens.grid24,
|
||||
)
|
||||
: (widget.ev != null ? _EvValueText(ev: widget.ev!) : null),
|
||||
),
|
||||
return GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
onTapDown: (_) {
|
||||
setState(() {
|
||||
_isPressed = true;
|
||||
});
|
||||
},
|
||||
onTapUp: (_) {
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
},
|
||||
onTapCancel: () {
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
},
|
||||
child: SizedBox.fromSize(
|
||||
size: const Size.square(Dimens.grid72),
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: AnimatedScale(
|
||||
duration: Dimens.durationS,
|
||||
scale: _isPressed ? 0.9 : 1.0,
|
||||
child: FilledCircle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
size: Dimens.grid72 - Dimens.grid8,
|
||||
child: Center(
|
||||
child: widget.ev != null ? _EvValueText(ev: widget.ev!) : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: CircularProgressIndicator(
|
||||
/// This key is needed to make indicator start from the same point every time
|
||||
key: ValueKey(widget.isMetering),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
value: widget.isMetering ? null : 1,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: CircularProgressIndicator(
|
||||
/// This key is needed to make indicator start from the same point every time
|
||||
key: ValueKey(widget.isMetering),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
value: widget.isMetering ? null : 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -6,7 +6,6 @@ import 'package:lightmeter/screens/metering/components/bottom_controls/widget_bo
|
|||
class MeteringBottomControlsProvider extends StatelessWidget {
|
||||
final double? ev;
|
||||
final bool isMetering;
|
||||
final bool hasError;
|
||||
final VoidCallback? onSwitchEvSourceType;
|
||||
final VoidCallback onMeasure;
|
||||
final VoidCallback onSettings;
|
||||
|
@ -14,7 +13,6 @@ class MeteringBottomControlsProvider extends StatelessWidget {
|
|||
const MeteringBottomControlsProvider({
|
||||
required this.ev,
|
||||
required this.isMetering,
|
||||
required this.hasError,
|
||||
required this.onSwitchEvSourceType,
|
||||
required this.onMeasure,
|
||||
required this.onSettings,
|
||||
|
@ -38,7 +36,6 @@ class MeteringBottomControlsProvider extends StatelessWidget {
|
|||
child: MeteringBottomControls(
|
||||
ev: ev,
|
||||
isMetering: isMetering,
|
||||
hasError: hasError,
|
||||
onSwitchEvSourceType: onSwitchEvSourceType,
|
||||
onMeasure: onMeasure,
|
||||
onSettings: onSettings,
|
||||
|
|
|
@ -7,7 +7,6 @@ import 'package:lightmeter/utils/inherited_generics.dart';
|
|||
class MeteringBottomControls extends StatelessWidget {
|
||||
final double? ev;
|
||||
final bool isMetering;
|
||||
final bool hasError;
|
||||
final VoidCallback? onSwitchEvSourceType;
|
||||
final VoidCallback onMeasure;
|
||||
final VoidCallback onSettings;
|
||||
|
@ -15,7 +14,6 @@ class MeteringBottomControls extends StatelessWidget {
|
|||
const MeteringBottomControls({
|
||||
required this.ev,
|
||||
required this.isMetering,
|
||||
required this.hasError,
|
||||
required this.onSwitchEvSourceType,
|
||||
required this.onMeasure,
|
||||
required this.onSettings,
|
||||
|
@ -56,7 +54,6 @@ class MeteringBottomControls extends StatelessWidget {
|
|||
MeteringMeasureButton(
|
||||
ev: ev,
|
||||
isMetering: isMetering,
|
||||
hasError: hasError,
|
||||
onTap: onMeasure,
|
||||
),
|
||||
Expanded(
|
||||
|
|
|
@ -53,8 +53,6 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
on<ZoomChangedEvent>(_onZoomChanged);
|
||||
on<ExposureOffsetChangedEvent>(_onExposureOffsetChanged);
|
||||
on<ExposureOffsetResetEvent>(_onExposureOffsetResetEvent);
|
||||
|
||||
add(const RequestPermissionEvent());
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -124,7 +122,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
_zoomRange = await Future.wait<double>([
|
||||
_cameraController!.getMinZoomLevel(),
|
||||
_cameraController!.getMaxZoomLevel(),
|
||||
]).then((levels) => RangeValues(levels[0], math.min(_maxZoom, levels[1])));
|
||||
]).then((levels) => RangeValues(math.max(1.0, levels[0]), math.min(_maxZoom, levels[1])));
|
||||
_currentZoom = _zoomRange!.start;
|
||||
|
||||
_exposureOffsetRange = await Future.wait<double>([
|
||||
|
@ -210,8 +208,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
final speed = speedValueRatio.numerator / speedValueRatio.denominator;
|
||||
|
||||
return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100);
|
||||
} on CameraException catch (e) {
|
||||
log('Error: ${e.code}\nError Message: ${e.description}');
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,12 +22,32 @@ class ZoomChangedEvent extends CameraContainerEvent {
|
|||
final double value;
|
||||
|
||||
const ZoomChangedEvent(this.value);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is ZoomChangedEvent && other.value == value;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(value, runtimeType);
|
||||
}
|
||||
|
||||
class ExposureOffsetChangedEvent extends CameraContainerEvent {
|
||||
final double value;
|
||||
|
||||
const ExposureOffsetChangedEvent(this.value);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is ExposureOffsetChangedEvent && other.value == value;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(value, runtimeType);
|
||||
}
|
||||
|
||||
class ExposureOffsetResetEvent extends CameraContainerEvent {
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:lightmeter/data/models/film.dart';
|
|||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/widget_container_camera.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
@ -40,7 +41,7 @@ class CameraContainerProvider extends StatelessWidget {
|
|||
create: (context) => CameraContainerBloc(
|
||||
context.get<MeteringInteractor>(),
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
),
|
||||
)..add(const RequestPermissionEvent()),
|
||||
child: CameraContainer(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
|
|
|
@ -34,10 +34,42 @@ class CameraActiveState extends CameraContainerState {
|
|||
required this.exposureOffsetStep,
|
||||
required this.currentExposureOffset,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is CameraActiveState &&
|
||||
other.zoomRange == zoomRange &&
|
||||
other.currentZoom == currentZoom &&
|
||||
other.exposureOffsetRange == exposureOffsetRange &&
|
||||
other.exposureOffsetStep == exposureOffsetStep &&
|
||||
other.currentExposureOffset == currentExposureOffset;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
zoomRange,
|
||||
currentZoom,
|
||||
exposureOffsetRange,
|
||||
exposureOffsetStep,
|
||||
currentExposureOffset,
|
||||
);
|
||||
}
|
||||
|
||||
class CameraErrorState extends CameraContainerState {
|
||||
final CameraErrorType error;
|
||||
|
||||
const CameraErrorState(this.error);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is CameraErrorState && other.error == error;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(error, runtimeType);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
|
||||
|
@ -16,35 +17,46 @@ class LightSensorContainerBloc
|
|||
final MeteringInteractor _meteringInteractor;
|
||||
|
||||
StreamSubscription<int>? _luxSubscriptions;
|
||||
double _ev100 = 0.0;
|
||||
|
||||
LightSensorContainerBloc(
|
||||
this._meteringInteractor,
|
||||
MeteringCommunicationBloc communicationBloc,
|
||||
) : super(
|
||||
communicationBloc,
|
||||
const LightSensorInitState(),
|
||||
);
|
||||
const LightSensorContainerState(null),
|
||||
) {
|
||||
on<LuxMeteringEvent>(_onLuxMeteringEvent);
|
||||
}
|
||||
|
||||
@override
|
||||
void onCommunicationState(communication_states.SourceState communicationState) {
|
||||
if (communicationState is communication_states.MeasureState) {
|
||||
if (_luxSubscriptions == null) {
|
||||
_luxSubscriptions = _meteringInteractor.luxStream().listen((event) {
|
||||
_ev100 = log2(event.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration;
|
||||
communicationBloc.add(communication_event.MeteringInProgressEvent(_ev100));
|
||||
});
|
||||
_startMetering();
|
||||
} else {
|
||||
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
|
||||
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
|
||||
_cancelMetering();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
|
||||
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
|
||||
_cancelMetering();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _onLuxMeteringEvent(LuxMeteringEvent event, Emitter<LightSensorContainerState> emit) {
|
||||
final ev100 = log2(event.lux.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration;
|
||||
emit(LightSensorContainerState(ev100));
|
||||
communicationBloc.add(communication_event.MeteringInProgressEvent(ev100));
|
||||
}
|
||||
|
||||
void _startMetering() {
|
||||
_luxSubscriptions = _meteringInteractor.luxStream().listen((lux) => add(LuxMeteringEvent(lux)));
|
||||
}
|
||||
|
||||
void _cancelMetering() {
|
||||
communicationBloc.add(communication_event.MeteringEndedEvent(state.ev100));
|
||||
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
abstract class LightSensorContainerEvent {
|
||||
const LightSensorContainerEvent();
|
||||
}
|
||||
|
||||
class LuxMeteringEvent extends LightSensorContainerEvent {
|
||||
final int lux;
|
||||
|
||||
const LuxMeteringEvent(this.lux);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
abstract class LightSensorContainerState {
|
||||
const LightSensorContainerState();
|
||||
}
|
||||
class LightSensorContainerState {
|
||||
final double? ev100;
|
||||
|
||||
class LightSensorInitState extends LightSensorContainerState {
|
||||
const LightSensorInitState();
|
||||
const LightSensorContainerState(this.ev100);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
|
||||
|
@ -21,5 +22,6 @@ abstract class EvSourceBlocBase<E, S> extends Bloc<E, S> {
|
|||
return super.close();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void onCommunicationState(communication_states.SourceState communicationState);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
abstract class MeteringEvent {
|
||||
sealed class MeteringEvent {
|
||||
const MeteringEvent();
|
||||
}
|
||||
|
||||
class StopTypeChangedEvent extends MeteringEvent {
|
||||
final StopType stopType;
|
||||
|
||||
const StopTypeChangedEvent(this.stopType);
|
||||
}
|
||||
|
||||
class EquipmentProfileChangedEvent extends MeteringEvent {
|
||||
final EquipmentProfileData equipmentProfileData;
|
||||
|
||||
|
@ -18,9 +12,9 @@ class EquipmentProfileChangedEvent extends MeteringEvent {
|
|||
}
|
||||
|
||||
class FilmChangedEvent extends MeteringEvent {
|
||||
final Film data;
|
||||
final Film film;
|
||||
|
||||
const FilmChangedEvent(this.data);
|
||||
const FilmChangedEvent(this.film);
|
||||
}
|
||||
|
||||
class IsoChangedEvent extends MeteringEvent {
|
||||
|
@ -41,10 +35,13 @@ class MeasureEvent extends MeteringEvent {
|
|||
|
||||
class MeasuredEvent extends MeteringEvent {
|
||||
final double ev100;
|
||||
final bool isMetering;
|
||||
|
||||
const MeasuredEvent(this.ev100);
|
||||
const MeasuredEvent(this.ev100, {required this.isMetering});
|
||||
}
|
||||
|
||||
class MeasureErrorEvent extends MeteringEvent {
|
||||
const MeasureErrorEvent();
|
||||
final bool isMetering;
|
||||
|
||||
const MeasureErrorEvent({required this.isMetering});
|
||||
}
|
||||
|
|
|
@ -6,12 +6,10 @@ import 'package:lightmeter/data/light_sensor_service.dart';
|
|||
import 'package:lightmeter/data/permissions_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
||||
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/screen_metering.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class MeteringFlow extends StatefulWidget {
|
||||
const MeteringFlow({super.key});
|
||||
|
@ -36,10 +34,8 @@ class _MeteringFlowState extends State<MeteringFlow> {
|
|||
BlocProvider(create: (_) => MeteringCommunicationBloc()),
|
||||
BlocProvider(
|
||||
create: (context) => MeteringBloc(
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
context.get<MeteringInteractor>(),
|
||||
context.get<EquipmentProfile>(),
|
||||
context.get<StopType>(),
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
|
@ -29,9 +31,7 @@ class MeteringScreen extends StatelessWidget {
|
|||
Expanded(
|
||||
child: BlocBuilder<MeteringBloc, MeteringState>(
|
||||
builder: (_, state) => _MeteringContainerBuidler(
|
||||
fastest: state is MeteringDataState ? state.fastest : null,
|
||||
slowest: state is MeteringDataState ? state.slowest : null,
|
||||
exposurePairs: state is MeteringDataState ? state.exposurePairs : [],
|
||||
ev: state is MeteringDataState ? state.ev : null,
|
||||
film: state.film,
|
||||
iso: state.iso,
|
||||
nd: state.nd,
|
||||
|
@ -45,9 +45,7 @@ class MeteringScreen extends StatelessWidget {
|
|||
BlocBuilder<MeteringBloc, MeteringState>(
|
||||
builder: (context, state) => MeteringBottomControlsProvider(
|
||||
ev: state is MeteringDataState ? state.ev : null,
|
||||
isMetering:
|
||||
state is LoadingState || state is MeteringDataState && state.continuousMetering,
|
||||
hasError: state is MeteringDataState && state.hasError,
|
||||
isMetering: state.isMetering,
|
||||
onSwitchEvSourceType: context.get<Environment>().hasLightSensor
|
||||
? EvSourceTypeProvider.of(context).toggleType
|
||||
: null,
|
||||
|
@ -73,47 +71,41 @@ class _InheritedListeners extends StatelessWidget {
|
|||
onDidChangeDependencies: (value) {
|
||||
context.read<MeteringBloc>().add(EquipmentProfileChangedEvent(value));
|
||||
},
|
||||
child: InheritedWidgetListener<StopType>(
|
||||
child: InheritedModelAspectListener<MeteringScreenLayoutFeature, bool>(
|
||||
aspect: MeteringScreenLayoutFeature.filmPicker,
|
||||
onDidChangeDependencies: (value) {
|
||||
context.read<MeteringBloc>().add(StopTypeChangedEvent(value));
|
||||
if (!value) context.read<MeteringBloc>().add(const FilmChangedEvent(Film.other()));
|
||||
},
|
||||
child: InheritedModelAspectListener<MeteringScreenLayoutFeature, bool>(
|
||||
aspect: MeteringScreenLayoutFeature.filmPicker,
|
||||
onDidChangeDependencies: (value) {
|
||||
if (!value) context.read<MeteringBloc>().add(const FilmChangedEvent(Film.other()));
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MeteringContainerBuidler extends StatelessWidget {
|
||||
final ExposurePair? fastest;
|
||||
final ExposurePair? slowest;
|
||||
final double? ev;
|
||||
final Film film;
|
||||
final IsoValue iso;
|
||||
final NdValue nd;
|
||||
final ValueChanged<Film> onFilmChanged;
|
||||
final ValueChanged<IsoValue> onIsoChanged;
|
||||
final ValueChanged<NdValue> onNdChanged;
|
||||
final List<ExposurePair> exposurePairs;
|
||||
|
||||
const _MeteringContainerBuidler({
|
||||
required this.fastest,
|
||||
required this.slowest,
|
||||
required this.ev,
|
||||
required this.film,
|
||||
required this.iso,
|
||||
required this.nd,
|
||||
required this.onFilmChanged,
|
||||
required this.onIsoChanged,
|
||||
required this.onNdChanged,
|
||||
required this.exposurePairs,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final exposurePairs = ev != null ? buildExposureValues(context, ev!, film) : <ExposurePair>[];
|
||||
final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null;
|
||||
final slowest = exposurePairs.isNotEmpty ? exposurePairs.last : null;
|
||||
return context.listen<EvSourceType>() == EvSourceType.camera
|
||||
? CameraContainerProvider(
|
||||
fastest: fastest,
|
||||
|
@ -138,4 +130,68 @@ class _MeteringContainerBuidler extends StatelessWidget {
|
|||
exposurePairs: exposurePairs,
|
||||
);
|
||||
}
|
||||
|
||||
List<ExposurePair> buildExposureValues(BuildContext context, double ev, Film film) {
|
||||
if (ev.isNaN || ev.isInfinite) {
|
||||
return List.empty();
|
||||
}
|
||||
|
||||
/// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3
|
||||
final StopType stopType = context.listen<StopType>();
|
||||
final int evSteps = (ev * (stopType.index + 1)).round();
|
||||
|
||||
final EquipmentProfile equipmentProfile = context.listen<EquipmentProfile>();
|
||||
final List<ApertureValue> apertureValues =
|
||||
equipmentProfile.apertureValues.whereStopType(stopType);
|
||||
final List<ShutterSpeedValue> shutterSpeedValues =
|
||||
equipmentProfile.shutterSpeedValues.whereStopType(stopType);
|
||||
|
||||
/// Basically we use 1" shutter speed as an anchor point for building the exposure pairs list.
|
||||
/// But user can exclude this value from the list using custom equipment profile.
|
||||
/// So we have to restore the index of the anchor value.
|
||||
const ShutterSpeedValue anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full);
|
||||
int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed);
|
||||
if (anchorIndex < 0) {
|
||||
final filteredFullList = ShutterSpeedValue.values.whereStopType(stopType);
|
||||
final customListStartIndex = filteredFullList.indexOf(shutterSpeedValues.first);
|
||||
final fullListAnchor = filteredFullList.indexOf(anchorShutterSpeed);
|
||||
if (customListStartIndex < fullListAnchor) {
|
||||
/// This means, that user excluded anchor value at the end,
|
||||
/// i.e. all shutter speed values are shorter than 1".
|
||||
anchorIndex = fullListAnchor - customListStartIndex;
|
||||
} else {
|
||||
/// In case user excludes anchor value at the start,
|
||||
/// we can do no adjustment.
|
||||
}
|
||||
}
|
||||
final int evOffset = anchorIndex - evSteps;
|
||||
|
||||
late final int apertureOffset;
|
||||
late final int shutterSpeedOffset;
|
||||
if (evOffset >= 0) {
|
||||
apertureOffset = 0;
|
||||
shutterSpeedOffset = evOffset;
|
||||
} else {
|
||||
apertureOffset = -evOffset;
|
||||
shutterSpeedOffset = 0;
|
||||
}
|
||||
|
||||
final int itemsCount = min(
|
||||
apertureValues.length + shutterSpeedOffset,
|
||||
shutterSpeedValues.length + apertureOffset,
|
||||
) -
|
||||
max(apertureOffset, shutterSpeedOffset);
|
||||
|
||||
if (itemsCount < 0) {
|
||||
return List.empty();
|
||||
}
|
||||
return List.generate(
|
||||
itemsCount,
|
||||
(index) => ExposurePair(
|
||||
apertureValues[index + apertureOffset],
|
||||
film.reciprocityFailure(shutterSpeedValues[index + shutterSpeedOffset]),
|
||||
),
|
||||
growable: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
@immutable
|
||||
abstract class MeteringState {
|
||||
final double? ev100;
|
||||
final Film film;
|
||||
final IsoValue iso;
|
||||
final NdValue nd;
|
||||
final bool isMetering;
|
||||
|
||||
const MeteringState({
|
||||
this.ev100,
|
||||
required this.film,
|
||||
required this.iso,
|
||||
required this.nd,
|
||||
required this.isMetering,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -21,24 +24,18 @@ class LoadingState extends MeteringState {
|
|||
required super.film,
|
||||
required super.iso,
|
||||
required super.nd,
|
||||
});
|
||||
}) : super(isMetering: true);
|
||||
}
|
||||
|
||||
class MeteringDataState extends MeteringState {
|
||||
final double? ev;
|
||||
final List<ExposurePair> exposurePairs;
|
||||
final bool continuousMetering;
|
||||
|
||||
const MeteringDataState({
|
||||
required this.ev,
|
||||
required super.ev100,
|
||||
required super.film,
|
||||
required super.iso,
|
||||
required super.nd,
|
||||
required this.exposurePairs,
|
||||
required this.continuousMetering,
|
||||
required super.isMetering,
|
||||
});
|
||||
|
||||
ExposurePair? get fastest => exposurePairs.isEmpty ? null : exposurePairs.first;
|
||||
ExposurePair? get slowest => exposurePairs.isEmpty ? null : exposurePairs.last;
|
||||
double? get ev => ev100 != null ? ev100! + log2(iso.value / 100) - nd.stopReduction : null;
|
||||
bool get hasError => ev == null;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ environment:
|
|||
|
||||
dependencies:
|
||||
app_settings: 4.2.0
|
||||
bloc_concurrency: 0.2.2
|
||||
camera: 0.10.5
|
||||
clipboard: 0.1.3
|
||||
dynamic_color: 1.6.5
|
||||
|
@ -16,7 +17,7 @@ dependencies:
|
|||
firebase_crashlytics: 3.3.1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: 8.1.2
|
||||
flutter_bloc: 8.1.3
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
intl: 0.18.0
|
||||
|
@ -35,10 +36,15 @@ dependencies:
|
|||
vibration: 1.7.7
|
||||
|
||||
dev_dependencies:
|
||||
bloc_test: 9.1.3
|
||||
build_runner: ^2.1.7
|
||||
flutter_launcher_icons: 0.11.0
|
||||
flutter_native_splash: 2.2.16
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
google_fonts: 3.0.1
|
||||
lint: 2.1.2
|
||||
mocktail: 0.3.0
|
||||
test: 1.24.1
|
||||
|
||||
flutter:
|
||||
|
|
609
test/screens/metering/bloc_metering_test.dart
Normal file
609
test/screens/metering/bloc_metering_test.dart
Normal file
|
@ -0,0 +1,609 @@
|
|||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
|
||||
as communication_events;
|
||||
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
|
||||
as communication_states;
|
||||
import 'package:lightmeter/screens/metering/event_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/state_metering.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockMeteringCommunicationBloc extends MockBloc<
|
||||
communication_events.MeteringCommunicationEvent,
|
||||
communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {}
|
||||
|
||||
class _MockMeteringInteractor extends Mock implements MeteringInteractor {}
|
||||
|
||||
void main() {
|
||||
late _MockMeteringInteractor meteringInteractor;
|
||||
late _MockMeteringCommunicationBloc communicationBloc;
|
||||
late MeteringBloc bloc;
|
||||
const iso100 = IsoValue(100, StopType.full);
|
||||
|
||||
setUp(() {
|
||||
meteringInteractor = _MockMeteringInteractor();
|
||||
when<IsoValue>(() => meteringInteractor.iso).thenReturn(iso100);
|
||||
when<NdValue>(() => meteringInteractor.ndFilter).thenReturn(NdValue.values.first);
|
||||
when<Film>(() => meteringInteractor.film).thenReturn(Film.values.first);
|
||||
when(meteringInteractor.quickVibration).thenAnswer((_) async {});
|
||||
when(meteringInteractor.responseVibration).thenAnswer((_) async {});
|
||||
when(meteringInteractor.errorVibration).thenAnswer((_) async {});
|
||||
|
||||
communicationBloc = _MockMeteringCommunicationBloc();
|
||||
|
||||
bloc = MeteringBloc(
|
||||
meteringInteractor,
|
||||
communicationBloc,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
bloc.close();
|
||||
communicationBloc.close();
|
||||
});
|
||||
|
||||
group(
|
||||
'`MeasureEvent`',
|
||||
() {
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'`MeasureEvent` -> success',
|
||||
build: () => bloc,
|
||||
act: (bloc) async {
|
||||
bloc.add(const MeasureEvent());
|
||||
bloc.onCommunicationState(const communication_states.MeteringEndedState(2));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.quickVibration()).called(1);
|
||||
verify(() => communicationBloc.add(const communication_events.MeasureEvent())).called(1);
|
||||
verify(() => meteringInteractor.responseVibration()).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<LoadingState>(),
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.isMetering, 'isMetering', false)
|
||||
.having((state) => state.ev, 'ev', 2),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'`MeasureEvent` -> error',
|
||||
build: () => bloc,
|
||||
act: (bloc) async {
|
||||
bloc.add(const MeasureEvent());
|
||||
bloc.onCommunicationState(const communication_states.MeteringEndedState(null));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.quickVibration()).called(1);
|
||||
verify(() => communicationBloc.add(const communication_events.MeasureEvent())).called(1);
|
||||
verify(() => meteringInteractor.errorVibration()).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<LoadingState>(),
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.isMetering, 'isMetering', false)
|
||||
.having((state) => state.ev100, 'ev100', null)
|
||||
.having((state) => state.ev, 'ev', null),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'`MeasureEvent` -> continuous metering',
|
||||
build: () => bloc,
|
||||
act: (bloc) async {
|
||||
// delays here simulate light sensor behaviour
|
||||
// when sensor does not fire new LUX events when value is not changed
|
||||
bloc.add(const MeasureEvent());
|
||||
bloc.onCommunicationState(const communication_states.MeteringInProgressState(null));
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
bloc.onCommunicationState(const communication_states.MeteringInProgressState(2));
|
||||
bloc.onCommunicationState(const communication_states.MeteringInProgressState(5.5));
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
bloc.onCommunicationState(const communication_states.MeteringInProgressState(null));
|
||||
bloc.onCommunicationState(const communication_states.MeteringInProgressState(4));
|
||||
bloc.add(const MeasureEvent());
|
||||
bloc.onCommunicationState(const communication_states.MeteringEndedState(4));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.quickVibration()).called(2);
|
||||
verify(() => communicationBloc.add(const communication_events.MeasureEvent())).called(2);
|
||||
verify(() => meteringInteractor.responseVibration()).called(4);
|
||||
verify(() => meteringInteractor.errorVibration()).called(2);
|
||||
},
|
||||
expect: () => [
|
||||
isA<LoadingState>(),
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.isMetering, 'isMetering', true)
|
||||
.having((state) => state.ev, 'ev', null),
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.isMetering, 'isMetering', true)
|
||||
.having((state) => state.ev, 'ev', 2),
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.isMetering, 'isMetering', true)
|
||||
.having((state) => state.ev, 'ev', 5.5),
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.isMetering, 'isMetering', true)
|
||||
.having((state) => state.ev, 'ev', null),
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.isMetering, 'isMetering', true)
|
||||
.having((state) => state.ev, 'ev', 4),
|
||||
isA<LoadingState>(),
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.isMetering, 'isMetering', false)
|
||||
.having((state) => state.ev, 'ev', 4),
|
||||
],
|
||||
);
|
||||
},
|
||||
timeout: const Timeout(Duration(seconds: 4)),
|
||||
);
|
||||
|
||||
group(
|
||||
'`IsoChangedEvent`',
|
||||
() {
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'Pick different ISO (ev100 != null)',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: Film.values[1],
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(const IsoChangedEvent(IsoValue(200, StopType.full)));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.film = Film.values.first).called(1);
|
||||
verify(() => meteringInteractor.iso = const IsoValue(200, StopType.full)).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', 1.0)
|
||||
.having((state) => state.ev, 'ev', 2.0)
|
||||
.having((state) => state.film, 'film', Film.values.first)
|
||||
.having((state) => state.iso, 'iso', const IsoValue(200, StopType.full))
|
||||
.having((state) => state.nd, 'nd', NdValue.values.first)
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'Pick different ISO (ev100 = null)',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: null,
|
||||
film: Film.values[1],
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(const IsoChangedEvent(IsoValue(200, StopType.full)));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.film = Film.values.first).called(1);
|
||||
verify(() => meteringInteractor.iso = const IsoValue(200, StopType.full)).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', null)
|
||||
.having((state) => state.ev, 'ev', null)
|
||||
.having((state) => state.film, 'film', Film.values.first)
|
||||
.having((state) => state.iso, 'iso', const IsoValue(200, StopType.full))
|
||||
.having((state) => state.nd, 'nd', NdValue.values.first)
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'Pick same ISO',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: Film.values[1],
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(const IsoChangedEvent(IsoValue(100, StopType.full)));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.film = Film.values.first).called(1);
|
||||
verifyNever(() => meteringInteractor.iso = const IsoValue(100, StopType.full));
|
||||
},
|
||||
expect: () => [],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'Pick different ISO & measure',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: Film.values[1],
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(const IsoChangedEvent(IsoValue(200, StopType.full)));
|
||||
bloc.add(const MeasureEvent());
|
||||
bloc.onCommunicationState(const communication_states.MeteringEndedState(2));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.film = Film.values.first).called(1);
|
||||
verify(() => meteringInteractor.iso = const IsoValue(200, StopType.full)).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', 1.0)
|
||||
.having((state) => state.ev, 'ev', 2.0)
|
||||
.having((state) => state.film, 'film', Film.values.first)
|
||||
.having((state) => state.iso, 'iso', const IsoValue(200, StopType.full))
|
||||
.having((state) => state.nd, 'nd', NdValue.values.first)
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
isA<LoadingState>(),
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', 2.0)
|
||||
.having((state) => state.ev, 'ev', 3.0)
|
||||
.having((state) => state.film, 'film', Film.values.first)
|
||||
.having((state) => state.iso, 'iso', const IsoValue(200, StopType.full))
|
||||
.having((state) => state.nd, 'nd', NdValue.values.first)
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`NdChangedEvent`',
|
||||
() {
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'Pick different ND (ev100 != null)',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: Film.values[1],
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(const NdChangedEvent(NdValue(2)));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.ndFilter = const NdValue(2)).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', 1.0)
|
||||
.having((state) => state.ev, 'ev', 0.0)
|
||||
.having((state) => state.film, 'film', Film.values[1])
|
||||
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
|
||||
.having((state) => state.nd, 'nd', const NdValue(2))
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'Pick different ND (ev100 = null)',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: null,
|
||||
film: Film.values[1],
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(const NdChangedEvent(NdValue(2)));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.ndFilter = const NdValue(2)).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', null)
|
||||
.having((state) => state.ev, 'ev', null)
|
||||
.having((state) => state.film, 'film', Film.values[1])
|
||||
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
|
||||
.having((state) => state.nd, 'nd', const NdValue(2))
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'Pick same ND',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: Film.values[1],
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(NdChangedEvent(NdValue.values.first));
|
||||
},
|
||||
verify: (_) {
|
||||
verifyNever(() => meteringInteractor.ndFilter = NdValue.values.first);
|
||||
},
|
||||
expect: () => [],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'Pick different ND & measure',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: Film.values[1],
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(const NdChangedEvent(NdValue(2)));
|
||||
bloc.add(const MeasureEvent());
|
||||
bloc.onCommunicationState(const communication_states.MeteringEndedState(2));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.ndFilter = const NdValue(2)).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', 1.0)
|
||||
.having((state) => state.ev, 'ev', 0.0)
|
||||
.having((state) => state.film, 'film', Film.values[1])
|
||||
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
|
||||
.having((state) => state.nd, 'nd', const NdValue(2))
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
isA<LoadingState>(),
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', 2.0)
|
||||
.having((state) => state.ev, 'ev', 1.0)
|
||||
.having((state) => state.film, 'film', Film.values[1])
|
||||
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
|
||||
.having((state) => state.nd, 'nd', const NdValue(2))
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`FilmChangedEvent`',
|
||||
() {
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'Pick different film with different ISO',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: const FomapanFilm.creative100(),
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(const FilmChangedEvent(FomapanFilm.creative200()));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.film = const FomapanFilm.creative200()).called(1);
|
||||
verify(() => meteringInteractor.iso = const IsoValue(200, StopType.full)).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', 1.0)
|
||||
.having((state) => state.ev, 'ev', 2.0)
|
||||
.having((state) => state.film, 'film', const FomapanFilm.creative200())
|
||||
.having((state) => state.iso, 'iso', const IsoValue(200, StopType.full))
|
||||
.having((state) => state.nd, 'nd', NdValue.values.first)
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'Pick different film with same ISO',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: const FomapanFilm.creative100(),
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(const FilmChangedEvent(IlfordFilm.delta100()));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.film = const IlfordFilm.delta100()).called(1);
|
||||
verifyNever(() => meteringInteractor.iso = const IsoValue(100, StopType.full));
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', 1.0)
|
||||
.having((state) => state.ev, 'ev', 1.0)
|
||||
.having((state) => state.film, 'film', const IlfordFilm.delta100())
|
||||
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
|
||||
.having((state) => state.nd, 'nd', NdValue.values.first)
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'Pick same film',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: const FomapanFilm.creative100(),
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(const FilmChangedEvent(FomapanFilm.creative100()));
|
||||
},
|
||||
verify: (_) {
|
||||
verifyNever(() => meteringInteractor.film = const FomapanFilm.creative100());
|
||||
},
|
||||
expect: () => [],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'Pick `Film.other()`',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: const FomapanFilm.creative100(),
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(const FilmChangedEvent(Film.other()));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.film = const Film.other()).called(1);
|
||||
verifyNever(() => meteringInteractor.iso = const IsoValue(0, StopType.full));
|
||||
verifyNever(() => meteringInteractor.responseVibration());
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', 1.0)
|
||||
.having((state) => state.ev, 'ev', 1.0)
|
||||
.having((state) => state.film, 'film', const Film.other())
|
||||
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
|
||||
.having((state) => state.nd, 'nd', NdValue.values.first)
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`EquipmentProfileChangedEvent`',
|
||||
() {
|
||||
final reducedProfile = EquipmentProfileData(
|
||||
id: '0',
|
||||
name: 'Reduced',
|
||||
apertureValues: ApertureValue.values,
|
||||
ndValues: NdValue.values.getRange(0, 3).toList(),
|
||||
shutterSpeedValues: ShutterSpeedValue.values,
|
||||
isoValues: IsoValue.values.getRange(4, 23).toList(),
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'New profile has current ISO & ND',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: Film.values[1],
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(EquipmentProfileChangedEvent(reducedProfile));
|
||||
},
|
||||
verify: (_) {
|
||||
verifyNever(() => meteringInteractor.film = const Film.other());
|
||||
verifyNever(() => meteringInteractor.iso = reducedProfile.isoValues.first);
|
||||
verifyNever(() => meteringInteractor.ndFilter = reducedProfile.ndValues.first);
|
||||
verifyNever(() => meteringInteractor.responseVibration());
|
||||
},
|
||||
expect: () => [],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'New profile has new ISO & current ND',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: Film.values[1],
|
||||
iso: IsoValue.values[2],
|
||||
nd: NdValue.values.first,
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(EquipmentProfileChangedEvent(reducedProfile));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.film = const Film.other()).called(1);
|
||||
verify(() => meteringInteractor.iso = reducedProfile.isoValues.first).called(1);
|
||||
verifyNever(() => meteringInteractor.ndFilter = reducedProfile.ndValues.first);
|
||||
verify(() => meteringInteractor.responseVibration()).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', 1.0)
|
||||
.having((state) => state.film, 'film', const Film.other())
|
||||
.having((state) => state.iso, 'iso', reducedProfile.isoValues.first)
|
||||
.having((state) => state.nd, 'nd', NdValue.values.first)
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'New profile has current ISO & new ND',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: Film.values[1],
|
||||
iso: const IsoValue(100, StopType.full),
|
||||
nd: NdValue.values[4],
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(EquipmentProfileChangedEvent(reducedProfile));
|
||||
},
|
||||
verify: (_) {
|
||||
verifyNever(() => meteringInteractor.film = const Film.other());
|
||||
verifyNever(() => meteringInteractor.iso = reducedProfile.isoValues.first);
|
||||
verify(() => meteringInteractor.ndFilter = reducedProfile.ndValues.first).called(1);
|
||||
verify(() => meteringInteractor.responseVibration()).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', 1.0)
|
||||
.having((state) => state.film, 'film', Film.values[1])
|
||||
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
|
||||
.having((state) => state.nd, 'nd', reducedProfile.ndValues.first)
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<MeteringBloc, MeteringState>(
|
||||
'New profile has new ISO & new ND',
|
||||
build: () => bloc,
|
||||
seed: () => MeteringDataState(
|
||||
ev100: 1.0,
|
||||
film: Film.values[1],
|
||||
iso: IsoValue.values[2],
|
||||
nd: NdValue.values[4],
|
||||
isMetering: false,
|
||||
),
|
||||
act: (bloc) async {
|
||||
bloc.add(EquipmentProfileChangedEvent(reducedProfile));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.film = const Film.other()).called(1);
|
||||
verify(() => meteringInteractor.iso = reducedProfile.isoValues.first).called(1);
|
||||
verify(() => meteringInteractor.ndFilter = reducedProfile.ndValues.first).called(1);
|
||||
verify(() => meteringInteractor.responseVibration()).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringDataState>()
|
||||
.having((state) => state.ev100, 'ev100', 1.0)
|
||||
.having((state) => state.film, 'film', const Film.other())
|
||||
.having((state) => state.iso, 'iso', reducedProfile.isoValues.first)
|
||||
.having((state) => state.nd, 'nd', reducedProfile.ndValues.first)
|
||||
.having((state) => state.isMetering, 'isMetering', false),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
late MeteringCommunicationBloc bloc;
|
||||
|
||||
setUp(() {
|
||||
bloc = MeteringCommunicationBloc();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
bloc.close();
|
||||
});
|
||||
|
||||
group(
|
||||
'`MeasureEvent`',
|
||||
() {
|
||||
blocTest<MeteringCommunicationBloc, MeteringCommunicationState>(
|
||||
'Multiple consequtive measure events',
|
||||
build: () => bloc,
|
||||
act: (bloc) async {
|
||||
bloc.add(const MeasureEvent());
|
||||
bloc.add(const MeasureEvent());
|
||||
bloc.add(const MeasureEvent());
|
||||
bloc.add(const MeasureEvent());
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeasureState>(),
|
||||
isA<MeasureState>(),
|
||||
isA<MeasureState>(),
|
||||
isA<MeasureState>(),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<MeteringCommunicationBloc, MeteringCommunicationState>(
|
||||
'Continuous metering simulation',
|
||||
build: () => bloc,
|
||||
act: (bloc) async {
|
||||
bloc.add(const MeasureEvent());
|
||||
bloc.add(const MeteringInProgressEvent(1));
|
||||
bloc.add(const MeteringInProgressEvent(null));
|
||||
bloc.add(const MeteringInProgressEvent(null));
|
||||
bloc.add(const MeteringInProgressEvent(2));
|
||||
bloc.add(const MeasureEvent());
|
||||
bloc.add(const MeteringEndedEvent(2));
|
||||
bloc.add(const MeteringEndedEvent(2));
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeasureState>(),
|
||||
isA<MeteringInProgressState>().having((state) => state.ev100, 'ev100', 1),
|
||||
isA<MeteringInProgressState>().having((state) => state.ev100, 'ev100', null),
|
||||
isA<MeteringInProgressState>().having((state) => state.ev100, 'ev100', 2),
|
||||
isA<MeasureState>(),
|
||||
isA<MeteringEndedState>().having((state) => state.ev100, 'ev100', 2),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`MeteringInProgressEvent`',
|
||||
() {
|
||||
blocTest<MeteringCommunicationBloc, MeteringCommunicationState>(
|
||||
'Multiple consequtive in progress events',
|
||||
build: () => bloc,
|
||||
act: (bloc) async {
|
||||
bloc.add(const MeteringInProgressEvent(1));
|
||||
bloc.add(const MeteringInProgressEvent(1));
|
||||
bloc.add(const MeteringInProgressEvent(1));
|
||||
bloc.add(const MeteringInProgressEvent(null));
|
||||
bloc.add(const MeteringInProgressEvent(null));
|
||||
bloc.add(const MeteringInProgressEvent(2));
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringInProgressState>().having((state) => state.ev100, 'ev100', 1),
|
||||
isA<MeteringInProgressState>().having((state) => state.ev100, 'ev100', null),
|
||||
isA<MeteringInProgressState>().having((state) => state.ev100, 'ev100', 2),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`MeteringEndedEvent`',
|
||||
() {
|
||||
blocTest<MeteringCommunicationBloc, MeteringCommunicationState>(
|
||||
'Multiple consequtive ended events',
|
||||
build: () => bloc,
|
||||
act: (bloc) async {
|
||||
bloc.add(const MeteringEndedEvent(1));
|
||||
},
|
||||
expect: () => [
|
||||
isA<MeteringEndedState>().having((state) => state.ev100, 'ev100', 1),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group(
|
||||
'`MeteringInProgressEvent`',
|
||||
() {
|
||||
final a = MeteringInProgressEvent(1.0);
|
||||
final b = MeteringInProgressEvent(1.0);
|
||||
final c = MeteringInProgressEvent(2.0);
|
||||
test('==', () {
|
||||
expect(a == b && b == a, true);
|
||||
expect(a != c && c != a, true);
|
||||
expect(b != c && c != b, true);
|
||||
});
|
||||
test('hashCode', () {
|
||||
expect(a.hashCode == b.hashCode, true);
|
||||
expect(a.hashCode != c.hashCode, true);
|
||||
expect(b.hashCode != c.hashCode, true);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`MeteringEndedEvent`',
|
||||
() {
|
||||
final a = MeteringEndedEvent(1.0);
|
||||
final b = MeteringEndedEvent(1.0);
|
||||
final c = MeteringEndedEvent(2.0);
|
||||
test('==', () {
|
||||
expect(a == b && b == a, true);
|
||||
expect(a != c && c != a, true);
|
||||
expect(b != c && c != b, true);
|
||||
});
|
||||
test('hashCode', () {
|
||||
expect(a.hashCode == b.hashCode, true);
|
||||
expect(a.hashCode != c.hashCode, true);
|
||||
expect(b.hashCode != c.hashCode, true);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group(
|
||||
'`MeteringInProgressState`',
|
||||
() {
|
||||
final a = MeteringInProgressState(1.0);
|
||||
final b = MeteringInProgressState(1.0);
|
||||
final c = MeteringInProgressState(2.0);
|
||||
test('==', () {
|
||||
expect(a == b && b == a, true);
|
||||
expect(a != c && c != a, true);
|
||||
expect(b != c && c != b, true);
|
||||
});
|
||||
test('hashCode', () {
|
||||
expect(a.hashCode == b.hashCode, true);
|
||||
expect(a.hashCode != c.hashCode, true);
|
||||
expect(b.hashCode != c.hashCode, true);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`MeteringEndedState`',
|
||||
() {
|
||||
final a = MeteringEndedState(1.0);
|
||||
final b = MeteringEndedState(1.0);
|
||||
final c = MeteringEndedState(2.0);
|
||||
test('==', () {
|
||||
expect(a == b && b == a, true);
|
||||
expect(a != c && c != a, true);
|
||||
expect(b != c && c != b, true);
|
||||
});
|
||||
test('hashCode', () {
|
||||
expect(a.hashCode == b.hashCode, true);
|
||||
expect(a.hashCode != c.hashCode, true);
|
||||
expect(b.hashCode != c.hashCode, true);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
|
@ -0,0 +1,490 @@
|
|||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
|
||||
as communication_events;
|
||||
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
|
||||
as communication_states;
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class _MockMeteringCommunicationBloc extends MockBloc<
|
||||
communication_events.MeteringCommunicationEvent,
|
||||
communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {}
|
||||
|
||||
class _MockMeteringInteractor extends Mock implements MeteringInteractor {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late _MockMeteringInteractor meteringInteractor;
|
||||
late _MockMeteringCommunicationBloc communicationBloc;
|
||||
late CameraContainerBloc bloc;
|
||||
|
||||
const cameraMethodChannel = MethodChannel('plugins.flutter.io/camera');
|
||||
const cameraIdMethodChannel = MethodChannel('flutter.io/cameraPlugin/camera1');
|
||||
const availableCameras = [
|
||||
{
|
||||
"name": "front",
|
||||
"lensFacing": "front",
|
||||
"sensorOrientation": 0,
|
||||
},
|
||||
{
|
||||
"name": "back",
|
||||
"lensFacing": "back",
|
||||
"sensorOrientation": 0,
|
||||
},
|
||||
];
|
||||
const frontCameras = [
|
||||
{
|
||||
"name": "front",
|
||||
"lensFacing": "front",
|
||||
"sensorOrientation": 0,
|
||||
},
|
||||
{
|
||||
"name": "front2",
|
||||
"lensFacing": "front",
|
||||
"sensorOrientation": 0,
|
||||
},
|
||||
];
|
||||
Future<Object?>? cameraMethodCallSuccessHandler(
|
||||
MethodCall methodCall, {
|
||||
List<Map<String, Object>> cameras = availableCameras,
|
||||
}) async {
|
||||
switch (methodCall.method) {
|
||||
case "availableCameras":
|
||||
return cameras;
|
||||
case "create":
|
||||
return {"cameraId": 1};
|
||||
case "initialize":
|
||||
await cameraIdMethodChannel.invokeMockMethod("initialized", {
|
||||
'cameraId': 1,
|
||||
'previewWidth': 2160.0,
|
||||
'previewHeight': 3840.0,
|
||||
'exposureMode': 'auto',
|
||||
'exposurePointSupported': true,
|
||||
'focusMode': 'auto',
|
||||
'focusPointSupported': true,
|
||||
});
|
||||
return {};
|
||||
case "setFlashMode":
|
||||
return null;
|
||||
case "getMinZoomLevel":
|
||||
return 0.67;
|
||||
case "getMaxZoomLevel":
|
||||
return 7.0;
|
||||
case "getMinExposureOffset":
|
||||
return -4.0;
|
||||
case "getMaxExposureOffset":
|
||||
return 4.0;
|
||||
case "getExposureOffsetStepSize":
|
||||
return 0.1666666;
|
||||
case "takePicture":
|
||||
return "";
|
||||
case "setExposureOffset":
|
||||
// ignore: avoid_dynamic_calls
|
||||
return methodCall.arguments["offset"];
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final initializedStateSequence = [
|
||||
isA<CameraLoadingState>(),
|
||||
isA<CameraInitializedState>(),
|
||||
isA<CameraActiveState>()
|
||||
.having((state) => state.zoomRange, 'zoomRange', const RangeValues(1.0, 7.0))
|
||||
.having((state) => state.currentZoom, 'currentZoom', 1.0)
|
||||
.having(
|
||||
(state) => state.exposureOffsetRange,
|
||||
'exposureOffsetRange',
|
||||
const RangeValues(-4.0, 4.0),
|
||||
)
|
||||
.having((state) => state.exposureOffsetStep, 'exposureOffsetStep', 0.1666666)
|
||||
.having((state) => state.currentExposureOffset, 'currentExposureOffset', 0.0),
|
||||
];
|
||||
|
||||
setUpAll(() {
|
||||
meteringInteractor = _MockMeteringInteractor();
|
||||
communicationBloc = _MockMeteringCommunicationBloc();
|
||||
|
||||
when(() => meteringInteractor.cameraEvCalibration).thenReturn(0.0);
|
||||
when(meteringInteractor.quickVibration).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
bloc = CameraContainerBloc(
|
||||
meteringInteractor,
|
||||
communicationBloc,
|
||||
);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(cameraMethodChannel, cameraMethodCallSuccessHandler);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
bloc.close();
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(cameraMethodChannel, null);
|
||||
});
|
||||
|
||||
group(
|
||||
'`RequestPermissionEvent`',
|
||||
() {
|
||||
blocTest<CameraContainerBloc, CameraContainerState>(
|
||||
'Request denied',
|
||||
build: () => bloc,
|
||||
setUp: () {
|
||||
when(() => meteringInteractor.requestPermission()).thenAnswer((_) async => false);
|
||||
},
|
||||
act: (bloc) => bloc.add(const RequestPermissionEvent()),
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.requestPermission()).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<CameraErrorState>()
|
||||
.having((state) => state.error, "error", CameraErrorType.permissionNotGranted),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<CameraContainerBloc, CameraContainerState>(
|
||||
'Request granted -> check denied',
|
||||
build: () => bloc,
|
||||
setUp: () {
|
||||
when(() => meteringInteractor.requestPermission()).thenAnswer((_) async => true);
|
||||
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => false);
|
||||
},
|
||||
act: (bloc) => bloc.add(const RequestPermissionEvent()),
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.requestPermission()).called(1);
|
||||
verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<CameraLoadingState>(),
|
||||
isA<CameraErrorState>()
|
||||
.having((state) => state.error, "error", CameraErrorType.permissionNotGranted),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<CameraContainerBloc, CameraContainerState>(
|
||||
'Request granted -> check granted',
|
||||
build: () => bloc,
|
||||
setUp: () {
|
||||
when(() => meteringInteractor.requestPermission()).thenAnswer((_) async => true);
|
||||
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
|
||||
},
|
||||
act: (bloc) => bloc.add(const RequestPermissionEvent()),
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.requestPermission()).called(1);
|
||||
verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
||||
},
|
||||
expect: () => initializedStateSequence,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`OpenAppSettingsEvent`',
|
||||
() {
|
||||
blocTest<CameraContainerBloc, CameraContainerState>(
|
||||
'App settings opened',
|
||||
setUp: () {
|
||||
when(() => meteringInteractor.openAppSettings()).thenAnswer((_) {});
|
||||
},
|
||||
build: () => bloc,
|
||||
act: (bloc) async {
|
||||
bloc.add(const OpenAppSettingsEvent());
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.openAppSettings()).called(1);
|
||||
},
|
||||
expect: () => [],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`InitializeEvent`/`DeinitializeEvent`',
|
||||
() {
|
||||
blocTest<CameraContainerBloc, CameraContainerState>(
|
||||
'No cameras detected error',
|
||||
setUp: () {
|
||||
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
cameraMethodChannel,
|
||||
(methodCall) async => cameraMethodCallSuccessHandler(methodCall, cameras: const []),
|
||||
);
|
||||
},
|
||||
tearDown: () {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(cameraMethodChannel, null);
|
||||
},
|
||||
build: () => bloc,
|
||||
act: (bloc) => bloc.add(const InitializeEvent()),
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<CameraLoadingState>(),
|
||||
isA<CameraErrorState>()
|
||||
.having((state) => state.error, "error", CameraErrorType.noCamerasDetected),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<CameraContainerBloc, CameraContainerState>(
|
||||
'No back facing cameras available',
|
||||
setUp: () {
|
||||
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
cameraMethodChannel,
|
||||
(methodCall) async => cameraMethodCallSuccessHandler(methodCall, cameras: frontCameras),
|
||||
);
|
||||
},
|
||||
tearDown: () {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(cameraMethodChannel, null);
|
||||
},
|
||||
build: () => bloc,
|
||||
act: (bloc) => bloc.add(const InitializeEvent()),
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
||||
},
|
||||
expect: () => initializedStateSequence,
|
||||
);
|
||||
|
||||
blocTest<CameraContainerBloc, CameraContainerState>(
|
||||
'Catch other initialization errors',
|
||||
setUp: () {
|
||||
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
cameraMethodChannel,
|
||||
(methodCall) async {
|
||||
switch (methodCall.method) {
|
||||
case "availableCameras":
|
||||
return availableCameras;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
tearDown: () {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(cameraMethodChannel, null);
|
||||
},
|
||||
build: () => bloc,
|
||||
act: (bloc) => bloc.add(const InitializeEvent()),
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
isA<CameraLoadingState>(),
|
||||
isA<CameraErrorState>().having((state) => state.error, "error", CameraErrorType.other),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<CameraContainerBloc, CameraContainerState>(
|
||||
'appLifecycleStateObserver',
|
||||
setUp: () {
|
||||
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
|
||||
},
|
||||
build: () => bloc,
|
||||
act: (bloc) async {
|
||||
bloc.add(const InitializeEvent());
|
||||
await Future.delayed(Duration.zero);
|
||||
TestWidgetsFlutterBinding.instance
|
||||
.handleAppLifecycleStateChanged(AppLifecycleState.detached);
|
||||
TestWidgetsFlutterBinding.instance
|
||||
.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.checkCameraPermission()).called(2);
|
||||
},
|
||||
expect: () => [
|
||||
...initializedStateSequence,
|
||||
...initializedStateSequence,
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`_takePicture()`',
|
||||
() {
|
||||
blocTest<CameraContainerBloc, CameraContainerState>(
|
||||
'Returned ev100 == null',
|
||||
setUp: () {
|
||||
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
|
||||
},
|
||||
build: () => bloc,
|
||||
act: (bloc) async {
|
||||
bloc.add(const InitializeEvent());
|
||||
await Future.delayed(Duration.zero);
|
||||
bloc.onCommunicationState(const communication_states.MeasureState());
|
||||
bloc.onCommunicationState(const communication_states.MeasureState());
|
||||
bloc.onCommunicationState(const communication_states.MeasureState());
|
||||
bloc.onCommunicationState(const communication_states.MeasureState());
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
||||
verifyNever(() => meteringInteractor.cameraEvCalibration);
|
||||
},
|
||||
expect: () => initializedStateSequence,
|
||||
);
|
||||
|
||||
// TODO(vodemn): figure out how to mock `_file.readAsBytes()`
|
||||
// blocTest<CameraContainerBloc, CameraContainerState>(
|
||||
// 'Returned non-null ev100',
|
||||
// setUp: () {
|
||||
// when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
|
||||
// TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
// .setMockMethodCallHandler(cameraMethodChannel, cameraMethodCallSuccessHandler);
|
||||
// },
|
||||
// tearDown: () {
|
||||
// TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
// .setMockMethodCallHandler(cameraMethodChannel, null);
|
||||
// },
|
||||
// build: () => bloc,
|
||||
// act: (bloc) async {
|
||||
// bloc.add(const InitializeEvent());
|
||||
// await Future.delayed(Duration.zero);
|
||||
// bloc.onCommunicationState(const communication_states.MeasureState());
|
||||
// },
|
||||
// verify: (_) {
|
||||
// verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
||||
// verifyNever(() => meteringInteractor.cameraEvCalibration);
|
||||
// verify(() {
|
||||
// communicationBloc.add(const communication_events.MeteringEndedEvent(null));
|
||||
// }).called(2);
|
||||
// },
|
||||
// expect: () => [
|
||||
// ...initializedStateSequence,
|
||||
// ],
|
||||
// );
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`ZoomChangedEvent`',
|
||||
() {
|
||||
blocTest<CameraContainerBloc, CameraContainerState>(
|
||||
'Set zoom multiple times',
|
||||
setUp: () {
|
||||
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
|
||||
},
|
||||
build: () => bloc,
|
||||
act: (bloc) async {
|
||||
bloc.add(const InitializeEvent());
|
||||
await Future.delayed(Duration.zero);
|
||||
bloc.add(const ZoomChangedEvent(2.0));
|
||||
bloc.add(const ZoomChangedEvent(2.0));
|
||||
bloc.add(const ZoomChangedEvent(2.0));
|
||||
bloc.add(const ZoomChangedEvent(3.0));
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
...initializedStateSequence,
|
||||
isA<CameraActiveState>()
|
||||
.having((state) => state.zoomRange, 'zoomRange', const RangeValues(1.0, 7.0))
|
||||
.having((state) => state.currentZoom, 'currentZoom', 2.0)
|
||||
.having(
|
||||
(state) => state.exposureOffsetRange,
|
||||
'exposureOffsetRange',
|
||||
const RangeValues(-4.0, 4.0),
|
||||
)
|
||||
.having((state) => state.exposureOffsetStep, 'exposureOffsetStep', 0.1666666)
|
||||
.having((state) => state.currentExposureOffset, 'currentExposureOffset', 0.0),
|
||||
isA<CameraActiveState>()
|
||||
.having((state) => state.zoomRange, 'zoomRange', const RangeValues(1.0, 7.0))
|
||||
.having((state) => state.currentZoom, 'currentZoom', 3.0)
|
||||
.having(
|
||||
(state) => state.exposureOffsetRange,
|
||||
'exposureOffsetRange',
|
||||
const RangeValues(-4.0, 4.0),
|
||||
)
|
||||
.having((state) => state.exposureOffsetStep, 'exposureOffsetStep', 0.1666666)
|
||||
.having((state) => state.currentExposureOffset, 'currentExposureOffset', 0.0),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`ExposureOffsetChangedEvent`/`ExposureOffsetResetEvent`',
|
||||
() {
|
||||
blocTest<CameraContainerBloc, CameraContainerState>(
|
||||
'Set exposure offset multiple times and reset',
|
||||
setUp: () {
|
||||
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
|
||||
},
|
||||
build: () => bloc,
|
||||
act: (bloc) async {
|
||||
bloc.add(const InitializeEvent());
|
||||
await Future.delayed(Duration.zero);
|
||||
bloc.add(const ExposureOffsetChangedEvent(2.0));
|
||||
bloc.add(const ExposureOffsetChangedEvent(2.0));
|
||||
bloc.add(const ExposureOffsetChangedEvent(2.0));
|
||||
bloc.add(const ExposureOffsetChangedEvent(3.0));
|
||||
bloc.add(const ExposureOffsetResetEvent());
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
||||
},
|
||||
expect: () => [
|
||||
...initializedStateSequence,
|
||||
isA<CameraActiveState>()
|
||||
.having((state) => state.zoomRange, 'zoomRange', const RangeValues(1.0, 7.0))
|
||||
.having((state) => state.currentZoom, 'currentZoom', 1.0)
|
||||
.having(
|
||||
(state) => state.exposureOffsetRange,
|
||||
'exposureOffsetRange',
|
||||
const RangeValues(-4.0, 4.0),
|
||||
)
|
||||
.having((state) => state.exposureOffsetStep, 'exposureOffsetStep', 0.1666666)
|
||||
.having((state) => state.currentExposureOffset, 'currentExposureOffset', 2.0),
|
||||
isA<CameraActiveState>()
|
||||
.having((state) => state.zoomRange, 'zoomRange', const RangeValues(1.0, 7.0))
|
||||
.having((state) => state.currentZoom, 'currentZoom', 1.0)
|
||||
.having(
|
||||
(state) => state.exposureOffsetRange,
|
||||
'exposureOffsetRange',
|
||||
const RangeValues(-4.0, 4.0),
|
||||
)
|
||||
.having((state) => state.exposureOffsetStep, 'exposureOffsetStep', 0.1666666)
|
||||
.having((state) => state.currentExposureOffset, 'currentExposureOffset', 3.0),
|
||||
isA<CameraActiveState>()
|
||||
.having((state) => state.zoomRange, 'zoomRange', const RangeValues(1.0, 7.0))
|
||||
.having((state) => state.currentZoom, 'currentZoom', 1.0)
|
||||
.having(
|
||||
(state) => state.exposureOffsetRange,
|
||||
'exposureOffsetRange',
|
||||
const RangeValues(-4.0, 4.0),
|
||||
)
|
||||
.having((state) => state.exposureOffsetStep, 'exposureOffsetStep', 0.1666666)
|
||||
.having((state) => state.currentExposureOffset, 'currentExposureOffset', 0.0),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
extension _MethodChannelMock on MethodChannel {
|
||||
Future<void> invokeMockMethod(String method, dynamic arguments) async {
|
||||
final data = const StandardMethodCodec().encodeMethodCall(MethodCall(method, arguments));
|
||||
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
||||
name,
|
||||
data,
|
||||
(ByteData? data) {},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group(
|
||||
'`ZoomChangedEvent`',
|
||||
() {
|
||||
final a = ZoomChangedEvent(1.0);
|
||||
final b = ZoomChangedEvent(1.0);
|
||||
final c = ZoomChangedEvent(2.0);
|
||||
test('==', () {
|
||||
expect(a == b && b == a, true);
|
||||
expect(a != c && c != a, true);
|
||||
expect(b != c && c != b, true);
|
||||
});
|
||||
test('hashCode', () {
|
||||
expect(a.hashCode == b.hashCode, true);
|
||||
expect(a.hashCode != c.hashCode, true);
|
||||
expect(b.hashCode != c.hashCode, true);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`ExposureOffsetChangedEvent`',
|
||||
() {
|
||||
final a = ExposureOffsetChangedEvent(1.0);
|
||||
final b = ExposureOffsetChangedEvent(1.0);
|
||||
final c = ExposureOffsetChangedEvent(2.0);
|
||||
test('==', () {
|
||||
expect(a == b && b == a, true);
|
||||
expect(a != c && c != a, true);
|
||||
expect(b != c && c != b, true);
|
||||
});
|
||||
test('hashCode', () {
|
||||
expect(a.hashCode == b.hashCode, true);
|
||||
expect(a.hashCode != c.hashCode, true);
|
||||
expect(b.hashCode != c.hashCode, true);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group(
|
||||
'`CameraActiveState`',
|
||||
() {
|
||||
final a = CameraActiveState(
|
||||
zoomRange: RangeValues(1, 7),
|
||||
currentZoom: 1,
|
||||
exposureOffsetRange: RangeValues(-4, 4),
|
||||
currentExposureOffset: 0,
|
||||
exposureOffsetStep: 0.167,
|
||||
);
|
||||
final b = CameraActiveState(
|
||||
zoomRange: RangeValues(1, 7),
|
||||
currentZoom: 1,
|
||||
exposureOffsetRange: RangeValues(-4, 4),
|
||||
currentExposureOffset: 0,
|
||||
exposureOffsetStep: 0.167,
|
||||
);
|
||||
final c = CameraActiveState(
|
||||
zoomRange: RangeValues(1, 4),
|
||||
currentZoom: 3,
|
||||
exposureOffsetRange: RangeValues(-2, 2),
|
||||
currentExposureOffset: 0,
|
||||
exposureOffsetStep: 0.167,
|
||||
);
|
||||
test('==', () {
|
||||
expect(a == b && b == a, true);
|
||||
expect(a != c && c != a, true);
|
||||
expect(b != c && c != b, true);
|
||||
});
|
||||
test('hashCode', () {
|
||||
expect(a.hashCode == b.hashCode, true);
|
||||
expect(a.hashCode != c.hashCode, true);
|
||||
expect(b.hashCode != c.hashCode, true);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'`CameraErrorState`',
|
||||
() {
|
||||
final a = CameraErrorState(CameraErrorType.noCamerasDetected);
|
||||
final b = CameraErrorState(CameraErrorType.noCamerasDetected);
|
||||
final c = CameraErrorState(CameraErrorType.other);
|
||||
test('==', () {
|
||||
expect(a == b && b == a, true);
|
||||
expect(a != c && c != a, true);
|
||||
expect(b != c && c != b, true);
|
||||
});
|
||||
test('hashCode', () {
|
||||
expect(a.hashCode == b.hashCode, true);
|
||||
expect(a.hashCode != c.hashCode, true);
|
||||
expect(b.hashCode != c.hashCode, true);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
|
||||
as communication_events;
|
||||
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
|
||||
as communication_states;
|
||||
import 'package:lightmeter/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart';
|
||||
import 'package:lightmeter/screens/metering/components/light_sensor_container/state_container_light_sensor.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockMeteringCommunicationBloc extends MockBloc<
|
||||
communication_events.MeteringCommunicationEvent,
|
||||
communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {}
|
||||
|
||||
class _MockMeteringInteractor extends Mock implements MeteringInteractor {}
|
||||
|
||||
void main() {
|
||||
late _MockMeteringInteractor meteringInteractor;
|
||||
late _MockMeteringCommunicationBloc communicationBloc;
|
||||
late LightSensorContainerBloc bloc;
|
||||
|
||||
setUpAll(() {
|
||||
meteringInteractor = _MockMeteringInteractor();
|
||||
communicationBloc = _MockMeteringCommunicationBloc();
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
bloc = LightSensorContainerBloc(
|
||||
meteringInteractor,
|
||||
communicationBloc,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
bloc.close();
|
||||
});
|
||||
|
||||
group(
|
||||
'`LuxMeteringEvent`',
|
||||
() {
|
||||
const List<int> luxIterable = [1, 2, 2, 2, 3];
|
||||
final List<double> resultList = luxIterable.map((lux) => log2(lux / 2.5)).toList();
|
||||
blocTest<LightSensorContainerBloc, LightSensorContainerState>(
|
||||
'Turn measuring on/off',
|
||||
build: () => bloc,
|
||||
setUp: () {
|
||||
when(() => meteringInteractor.luxStream())
|
||||
.thenAnswer((_) => Stream.fromIterable(luxIterable));
|
||||
when(() => meteringInteractor.lightSensorEvCalibration).thenReturn(0.0);
|
||||
},
|
||||
act: (bloc) async {
|
||||
bloc.onCommunicationState(const communication_states.MeasureState());
|
||||
await Future.delayed(Duration.zero);
|
||||
bloc.onCommunicationState(const communication_states.MeasureState());
|
||||
},
|
||||
verify: (_) {
|
||||
verify(() => meteringInteractor.luxStream().listen((_) {})).called(1);
|
||||
verify(() => meteringInteractor.lightSensorEvCalibration).called(5);
|
||||
verify(() {
|
||||
communicationBloc.add(communication_events.MeteringInProgressEvent(resultList.first));
|
||||
}).called(1);
|
||||
verify(() {
|
||||
communicationBloc.add(communication_events.MeteringInProgressEvent(resultList[1]));
|
||||
}).called(3);
|
||||
verify(() {
|
||||
communicationBloc.add(communication_events.MeteringInProgressEvent(resultList.last));
|
||||
}).called(1);
|
||||
verify(() {
|
||||
communicationBloc.add(communication_events.MeteringEndedEvent(resultList.last));
|
||||
}).called(2); // +1 from dispose
|
||||
},
|
||||
expect: () => resultList.map(
|
||||
(e) => isA<LightSensorContainerState>().having((state) => state.ev100, 'ev100', e),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
4
test_coverage.sh
Normal file
4
test_coverage.sh
Normal file
|
@ -0,0 +1,4 @@
|
|||
flutter test --coverage
|
||||
lcov --remove coverage/lcov.info 'lib/generated/*' 'lib/l10n/*' -o coverage/new_lcov.info
|
||||
genhtml coverage/new_lcov.info -o coverage/html
|
||||
open coverage/html/index.html
|
Loading…
Reference in a new issue