mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-25 00:40:39 +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
|
- name: Analyze project source
|
||||||
run: flutter analyze lib --fatal-infos
|
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/firebase_app_id_file.json
|
||||||
ios/Runner/GoogleService-Info.plist
|
ios/Runner/GoogleService-Info.plist
|
||||||
lib/firebase_options.dart
|
lib/firebase_options.dart
|
||||||
|
|
||||||
|
coverage/
|
|
@ -31,6 +31,7 @@ class MeteringInteractor {
|
||||||
|
|
||||||
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
|
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
|
||||||
double get lightSensorEvCalibration => _userPreferencesService.lightSensorEvCalibration;
|
double get lightSensorEvCalibration => _userPreferencesService.lightSensorEvCalibration;
|
||||||
|
bool get isHapticsEnabled => _userPreferencesService.haptics;
|
||||||
|
|
||||||
IsoValue get iso => _userPreferencesService.iso;
|
IsoValue get iso => _userPreferencesService.iso;
|
||||||
set iso(IsoValue value) => _userPreferencesService.iso = value;
|
set iso(IsoValue value) => _userPreferencesService.iso = value;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'dart:async';
|
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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
|
||||||
import 'package:lightmeter/data/models/film.dart';
|
import 'package:lightmeter/data/models/film.dart';
|
||||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.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';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||||
final MeteringCommunicationBloc _communicationBloc;
|
|
||||||
final MeteringInteractor _meteringInteractor;
|
final MeteringInteractor _meteringInteractor;
|
||||||
|
final MeteringCommunicationBloc _communicationBloc;
|
||||||
late final StreamSubscription<communication_states.ScreenState> _communicationSubscription;
|
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(
|
MeteringBloc(
|
||||||
this._communicationBloc,
|
|
||||||
this._meteringInteractor,
|
this._meteringInteractor,
|
||||||
this._equipmentProfileData,
|
this._communicationBloc,
|
||||||
this.stopType,
|
|
||||||
) : super(
|
) : super(
|
||||||
MeteringDataState(
|
MeteringDataState(
|
||||||
ev: null,
|
ev100: null,
|
||||||
film: _meteringInteractor.film,
|
film: _meteringInteractor.film,
|
||||||
iso: _meteringInteractor.iso,
|
iso: _meteringInteractor.iso,
|
||||||
nd: _meteringInteractor.ndFilter,
|
nd: _meteringInteractor.ndFilter,
|
||||||
exposurePairs: const [],
|
isMetering: false,
|
||||||
continuousMetering: false,
|
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
_communicationSubscription = _communicationBloc.stream
|
_communicationSubscription = _communicationBloc.stream
|
||||||
.where((state) => state is communication_states.ScreenState)
|
.where((state) => state is communication_states.ScreenState)
|
||||||
.map((state) => state as communication_states.ScreenState)
|
.map((state) => state as communication_states.ScreenState)
|
||||||
.listen(_onCommunicationState);
|
.listen(onCommunicationState);
|
||||||
|
|
||||||
on<EquipmentProfileChangedEvent>(_onEquipmentProfileChanged);
|
on<EquipmentProfileChangedEvent>(_onEquipmentProfileChanged);
|
||||||
on<StopTypeChangedEvent>(_onStopTypeChanged);
|
|
||||||
on<FilmChangedEvent>(_onFilmChanged);
|
on<FilmChangedEvent>(_onFilmChanged);
|
||||||
on<IsoChangedEvent>(_onIsoChanged);
|
on<IsoChangedEvent>(_onIsoChanged);
|
||||||
on<NdChangedEvent>(_onNdChanged);
|
on<NdChangedEvent>(_onNdChanged);
|
||||||
on<MeasureEvent>(_onMeasure);
|
on<MeasureEvent>(_onMeasure, transformer: droppable());
|
||||||
on<MeasuredEvent>(_onMeasured);
|
on<MeasuredEvent>(_onMeasured);
|
||||||
on<MeasureErrorEvent>(_onMeasureError);
|
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
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await _communicationSubscription.cancel();
|
await _communicationSubscription.cancel();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onCommunicationState(communication_states.ScreenState communicationState) {
|
@visibleForTesting
|
||||||
|
void onCommunicationState(communication_states.ScreenState communicationState) {
|
||||||
if (communicationState is communication_states.MeasuredState) {
|
if (communicationState is communication_states.MeasuredState) {
|
||||||
_isMeteringInProgress = communicationState is communication_states.MeteringInProgressState;
|
_handleEv100(
|
||||||
_handleEv100(communicationState.ev100);
|
communicationState.ev100,
|
||||||
}
|
isMetering: communicationState is communication_states.MeteringInProgressState,
|
||||||
}
|
);
|
||||||
|
|
||||||
void _onStopTypeChanged(StopTypeChangedEvent event, Emitter emit) {
|
|
||||||
if (stopType != event.stopType) {
|
|
||||||
stopType = event.stopType;
|
|
||||||
_updateMeasurements();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEquipmentProfileChanged(EquipmentProfileChangedEvent event, Emitter emit) {
|
void _onEquipmentProfileChanged(EquipmentProfileChangedEvent event, Emitter emit) {
|
||||||
_equipmentProfileData = event.equipmentProfileData;
|
|
||||||
bool willUpdateMeasurements = false;
|
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
|
/// 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;
|
_meteringInteractor.iso = event.equipmentProfileData.isoValues.first;
|
||||||
_iso = event.equipmentProfileData.isoValues.first;
|
iso = event.equipmentProfileData.isoValues.first;
|
||||||
willUpdateMeasurements &= true;
|
_meteringInteractor.film = Film.values.first;
|
||||||
|
film = Film.values.first;
|
||||||
|
willUpdateMeasurements = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The same for ND filter
|
/// 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;
|
_meteringInteractor.ndFilter = event.equipmentProfileData.ndValues.first;
|
||||||
_nd = event.equipmentProfileData.ndValues.first;
|
nd = event.equipmentProfileData.ndValues.first;
|
||||||
willUpdateMeasurements &= true;
|
willUpdateMeasurements = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (willUpdateMeasurements) {
|
if (willUpdateMeasurements) {
|
||||||
_updateMeasurements();
|
emit(
|
||||||
|
MeteringDataState(
|
||||||
|
ev100: state.ev100,
|
||||||
|
film: film,
|
||||||
|
iso: iso,
|
||||||
|
nd: nd,
|
||||||
|
isMetering: state.isMetering,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onFilmChanged(FilmChangedEvent event, Emitter emit) {
|
void _onFilmChanged(FilmChangedEvent event, Emitter emit) {
|
||||||
if (_film.name != event.data.name) {
|
if (state.film.name != event.film.name) {
|
||||||
_meteringInteractor.film = event.data;
|
_meteringInteractor.film = event.film;
|
||||||
_film = event.data;
|
|
||||||
|
/// 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
|
/// If user selects 'Other' film we preserve currently selected ISO
|
||||||
/// and therefore only discard reciprocity formula
|
/// and therefore only discard reciprocity formula
|
||||||
if (_iso.value != event.data.iso && event.data != const Film.other()) {
|
emit(
|
||||||
final newIso = IsoValue.values.firstWhere(
|
MeteringDataState(
|
||||||
(e) => e.value == event.data.iso,
|
ev100: state.ev100,
|
||||||
orElse: () => _iso,
|
film: event.film,
|
||||||
|
iso: iso,
|
||||||
|
nd: state.nd,
|
||||||
|
isMetering: state.isMetering,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
_meteringInteractor.iso = newIso;
|
|
||||||
_iso = newIso;
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateMeasurements();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,131 +147,77 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||||
/// because, for example, Fomapan 400 and any Ilford 400
|
/// because, for example, Fomapan 400 and any Ilford 400
|
||||||
/// have different reciprocity formulas
|
/// have different reciprocity formulas
|
||||||
_meteringInteractor.film = Film.values.first;
|
_meteringInteractor.film = Film.values.first;
|
||||||
_film = Film.values.first;
|
|
||||||
|
|
||||||
if (_iso != event.isoValue) {
|
if (state.iso != event.isoValue) {
|
||||||
_meteringInteractor.iso = event.isoValue;
|
_meteringInteractor.iso = event.isoValue;
|
||||||
_iso = event.isoValue;
|
emit(
|
||||||
_updateMeasurements();
|
MeteringDataState(
|
||||||
|
ev100: state.ev100,
|
||||||
|
film: Film.values.first,
|
||||||
|
iso: event.isoValue,
|
||||||
|
nd: state.nd,
|
||||||
|
isMetering: state.isMetering,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onNdChanged(NdChangedEvent event, Emitter emit) {
|
void _onNdChanged(NdChangedEvent event, Emitter emit) {
|
||||||
if (_nd != event.ndValue) {
|
if (state.nd != event.ndValue) {
|
||||||
_meteringInteractor.ndFilter = event.ndValue;
|
_meteringInteractor.ndFilter = event.ndValue;
|
||||||
_nd = event.ndValue;
|
emit(
|
||||||
_updateMeasurements();
|
MeteringDataState(
|
||||||
|
ev100: state.ev100,
|
||||||
|
film: state.film,
|
||||||
|
iso: state.iso,
|
||||||
|
nd: event.ndValue,
|
||||||
|
isMetering: state.isMetering,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onMeasure(MeasureEvent _, Emitter emit) {
|
void _onMeasure(MeasureEvent _, Emitter emit) {
|
||||||
_meteringInteractor.quickVibration();
|
_meteringInteractor.quickVibration();
|
||||||
_communicationBloc.add(const communication_events.MeasureEvent());
|
_communicationBloc.add(const communication_events.MeasureEvent());
|
||||||
_isMeteringInProgress = true;
|
|
||||||
emit(
|
emit(
|
||||||
LoadingState(
|
LoadingState(
|
||||||
film: _film,
|
film: state.film,
|
||||||
iso: _iso,
|
iso: state.iso,
|
||||||
nd: _nd,
|
nd: state.nd,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateMeasurements() => _handleEv100(_ev100);
|
void _handleEv100(double? ev100, {required bool isMetering}) {
|
||||||
|
|
||||||
void _handleEv100(double? ev100) {
|
|
||||||
if (ev100 == null || ev100.isNaN || ev100.isInfinite) {
|
if (ev100 == null || ev100.isNaN || ev100.isInfinite) {
|
||||||
add(const MeasureErrorEvent());
|
add(MeasureErrorEvent(isMetering: isMetering));
|
||||||
} else {
|
} else {
|
||||||
add(MeasuredEvent(ev100));
|
add(MeasuredEvent(ev100, isMetering: isMetering));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onMeasured(MeasuredEvent event, Emitter emit) {
|
void _onMeasured(MeasuredEvent event, Emitter emit) {
|
||||||
_meteringInteractor.responseVibration();
|
|
||||||
_ev100 = event.ev100;
|
|
||||||
final ev = event.ev100 + log2(_iso.value / 100) - _nd.stopReduction;
|
|
||||||
emit(
|
emit(
|
||||||
MeteringDataState(
|
MeteringDataState(
|
||||||
ev: ev,
|
ev100: event.ev100,
|
||||||
film: _film,
|
film: state.film,
|
||||||
iso: _iso,
|
iso: state.iso,
|
||||||
nd: _nd,
|
nd: state.nd,
|
||||||
exposurePairs: _buildExposureValues(ev),
|
isMetering: event.isMetering,
|
||||||
continuousMetering: _isMeteringInProgress,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onMeasureError(MeasureErrorEvent _, Emitter emit) {
|
void _onMeasureError(MeasureErrorEvent event, Emitter emit) {
|
||||||
_meteringInteractor.errorVibration();
|
|
||||||
_ev100 = null;
|
|
||||||
emit(
|
emit(
|
||||||
MeteringDataState(
|
MeteringDataState(
|
||||||
ev: null,
|
ev100: null,
|
||||||
film: _film,
|
film: state.film,
|
||||||
iso: _iso,
|
iso: state.iso,
|
||||||
nd: _nd,
|
nd: state.nd,
|
||||||
exposurePairs: const [],
|
isMetering: event.isMetering,
|
||||||
continuousMetering: _isMeteringInProgress,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
class MeteringInProgressEvent extends MeasuredEvent {
|
||||||
const MeteringInProgressEvent(super.ev100);
|
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 {
|
class MeteringEndedEvent extends MeasuredEvent {
|
||||||
const MeteringEndedEvent(super.ev100);
|
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();
|
const MeteringCommunicationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,11 +6,11 @@ class InitState extends MeteringCommunicationState {
|
||||||
const InitState();
|
const InitState();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class SourceState extends MeteringCommunicationState {
|
sealed class SourceState extends MeteringCommunicationState {
|
||||||
const SourceState();
|
const SourceState();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class ScreenState extends MeteringCommunicationState {
|
sealed class ScreenState extends MeteringCommunicationState {
|
||||||
const ScreenState();
|
const ScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ class MeasureState extends SourceState {
|
||||||
const MeasureState();
|
const MeasureState();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class MeasuredState extends ScreenState {
|
sealed class MeasuredState extends ScreenState {
|
||||||
final double? ev100;
|
final double? ev100;
|
||||||
|
|
||||||
const MeasuredState(this.ev100);
|
const MeasuredState(this.ev100);
|
||||||
|
@ -26,8 +26,28 @@ abstract class MeasuredState extends ScreenState {
|
||||||
|
|
||||||
class MeteringInProgressState extends MeasuredState {
|
class MeteringInProgressState extends MeasuredState {
|
||||||
const MeteringInProgressState(super.ev100);
|
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 {
|
class MeteringEndedState extends MeasuredState {
|
||||||
const MeteringEndedState(super.ev100);
|
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 {
|
class MeteringMeasureButton extends StatefulWidget {
|
||||||
final double? ev;
|
final double? ev;
|
||||||
final bool isMetering;
|
final bool isMetering;
|
||||||
final bool hasError;
|
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
const MeteringMeasureButton({
|
const MeteringMeasureButton({
|
||||||
required this.ev,
|
required this.ev,
|
||||||
required this.isMetering,
|
required this.isMetering,
|
||||||
required this.hasError,
|
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
@ -34,9 +32,7 @@ class _MeteringMeasureButtonState extends State<MeteringMeasureButton> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IgnorePointer(
|
return GestureDetector(
|
||||||
ignoring: widget.isMetering && widget.ev == null && !widget.hasError,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: widget.onTap,
|
onTap: widget.onTap,
|
||||||
onTapDown: (_) {
|
onTapDown: (_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -65,13 +61,7 @@ class _MeteringMeasureButtonState extends State<MeteringMeasureButton> {
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
size: Dimens.grid72 - Dimens.grid8,
|
size: Dimens.grid72 - Dimens.grid8,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: widget.hasError
|
child: widget.ev != null ? _EvValueText(ev: widget.ev!) : null,
|
||||||
? Icon(
|
|
||||||
Icons.error_outline,
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
size: Dimens.grid24,
|
|
||||||
)
|
|
||||||
: (widget.ev != null ? _EvValueText(ev: widget.ev!) : null),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -87,7 +77,6 @@ class _MeteringMeasureButtonState extends State<MeteringMeasureButton> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import 'package:lightmeter/screens/metering/components/bottom_controls/widget_bo
|
||||||
class MeteringBottomControlsProvider extends StatelessWidget {
|
class MeteringBottomControlsProvider extends StatelessWidget {
|
||||||
final double? ev;
|
final double? ev;
|
||||||
final bool isMetering;
|
final bool isMetering;
|
||||||
final bool hasError;
|
|
||||||
final VoidCallback? onSwitchEvSourceType;
|
final VoidCallback? onSwitchEvSourceType;
|
||||||
final VoidCallback onMeasure;
|
final VoidCallback onMeasure;
|
||||||
final VoidCallback onSettings;
|
final VoidCallback onSettings;
|
||||||
|
@ -14,7 +13,6 @@ class MeteringBottomControlsProvider extends StatelessWidget {
|
||||||
const MeteringBottomControlsProvider({
|
const MeteringBottomControlsProvider({
|
||||||
required this.ev,
|
required this.ev,
|
||||||
required this.isMetering,
|
required this.isMetering,
|
||||||
required this.hasError,
|
|
||||||
required this.onSwitchEvSourceType,
|
required this.onSwitchEvSourceType,
|
||||||
required this.onMeasure,
|
required this.onMeasure,
|
||||||
required this.onSettings,
|
required this.onSettings,
|
||||||
|
@ -38,7 +36,6 @@ class MeteringBottomControlsProvider extends StatelessWidget {
|
||||||
child: MeteringBottomControls(
|
child: MeteringBottomControls(
|
||||||
ev: ev,
|
ev: ev,
|
||||||
isMetering: isMetering,
|
isMetering: isMetering,
|
||||||
hasError: hasError,
|
|
||||||
onSwitchEvSourceType: onSwitchEvSourceType,
|
onSwitchEvSourceType: onSwitchEvSourceType,
|
||||||
onMeasure: onMeasure,
|
onMeasure: onMeasure,
|
||||||
onSettings: onSettings,
|
onSettings: onSettings,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import 'package:lightmeter/utils/inherited_generics.dart';
|
||||||
class MeteringBottomControls extends StatelessWidget {
|
class MeteringBottomControls extends StatelessWidget {
|
||||||
final double? ev;
|
final double? ev;
|
||||||
final bool isMetering;
|
final bool isMetering;
|
||||||
final bool hasError;
|
|
||||||
final VoidCallback? onSwitchEvSourceType;
|
final VoidCallback? onSwitchEvSourceType;
|
||||||
final VoidCallback onMeasure;
|
final VoidCallback onMeasure;
|
||||||
final VoidCallback onSettings;
|
final VoidCallback onSettings;
|
||||||
|
@ -15,7 +14,6 @@ class MeteringBottomControls extends StatelessWidget {
|
||||||
const MeteringBottomControls({
|
const MeteringBottomControls({
|
||||||
required this.ev,
|
required this.ev,
|
||||||
required this.isMetering,
|
required this.isMetering,
|
||||||
required this.hasError,
|
|
||||||
required this.onSwitchEvSourceType,
|
required this.onSwitchEvSourceType,
|
||||||
required this.onMeasure,
|
required this.onMeasure,
|
||||||
required this.onSettings,
|
required this.onSettings,
|
||||||
|
@ -56,7 +54,6 @@ class MeteringBottomControls extends StatelessWidget {
|
||||||
MeteringMeasureButton(
|
MeteringMeasureButton(
|
||||||
ev: ev,
|
ev: ev,
|
||||||
isMetering: isMetering,
|
isMetering: isMetering,
|
||||||
hasError: hasError,
|
|
||||||
onTap: onMeasure,
|
onTap: onMeasure,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
|
@ -53,8 +53,6 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
on<ZoomChangedEvent>(_onZoomChanged);
|
on<ZoomChangedEvent>(_onZoomChanged);
|
||||||
on<ExposureOffsetChangedEvent>(_onExposureOffsetChanged);
|
on<ExposureOffsetChangedEvent>(_onExposureOffsetChanged);
|
||||||
on<ExposureOffsetResetEvent>(_onExposureOffsetResetEvent);
|
on<ExposureOffsetResetEvent>(_onExposureOffsetResetEvent);
|
||||||
|
|
||||||
add(const RequestPermissionEvent());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -124,7 +122,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
_zoomRange = await Future.wait<double>([
|
_zoomRange = await Future.wait<double>([
|
||||||
_cameraController!.getMinZoomLevel(),
|
_cameraController!.getMinZoomLevel(),
|
||||||
_cameraController!.getMaxZoomLevel(),
|
_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;
|
_currentZoom = _zoomRange!.start;
|
||||||
|
|
||||||
_exposureOffsetRange = await Future.wait<double>([
|
_exposureOffsetRange = await Future.wait<double>([
|
||||||
|
@ -210,8 +208,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
final speed = speedValueRatio.numerator / speedValueRatio.denominator;
|
final speed = speedValueRatio.numerator / speedValueRatio.denominator;
|
||||||
|
|
||||||
return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100);
|
return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100);
|
||||||
} on CameraException catch (e) {
|
} catch (e) {
|
||||||
log('Error: ${e.code}\nError Message: ${e.description}');
|
log(e.toString());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,32 @@ class ZoomChangedEvent extends CameraContainerEvent {
|
||||||
final double value;
|
final double value;
|
||||||
|
|
||||||
const ZoomChangedEvent(this.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 {
|
class ExposureOffsetChangedEvent extends CameraContainerEvent {
|
||||||
final double value;
|
final double value;
|
||||||
|
|
||||||
const ExposureOffsetChangedEvent(this.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 {
|
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/interactors/metering_interactor.dart';
|
||||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.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/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/screens/metering/components/camera_container/widget_container_camera.dart';
|
||||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
@ -40,7 +41,7 @@ class CameraContainerProvider extends StatelessWidget {
|
||||||
create: (context) => CameraContainerBloc(
|
create: (context) => CameraContainerBloc(
|
||||||
context.get<MeteringInteractor>(),
|
context.get<MeteringInteractor>(),
|
||||||
context.read<MeteringCommunicationBloc>(),
|
context.read<MeteringCommunicationBloc>(),
|
||||||
),
|
)..add(const RequestPermissionEvent()),
|
||||||
child: CameraContainer(
|
child: CameraContainer(
|
||||||
fastest: fastest,
|
fastest: fastest,
|
||||||
slowest: slowest,
|
slowest: slowest,
|
||||||
|
|
|
@ -34,10 +34,42 @@ class CameraActiveState extends CameraContainerState {
|
||||||
required this.exposureOffsetStep,
|
required this.exposureOffsetStep,
|
||||||
required this.currentExposureOffset,
|
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 {
|
class CameraErrorState extends CameraContainerState {
|
||||||
final CameraErrorType error;
|
final CameraErrorType error;
|
||||||
|
|
||||||
const CameraErrorState(this.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 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.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/event_communication_metering.dart'
|
||||||
|
@ -16,35 +17,46 @@ class LightSensorContainerBloc
|
||||||
final MeteringInteractor _meteringInteractor;
|
final MeteringInteractor _meteringInteractor;
|
||||||
|
|
||||||
StreamSubscription<int>? _luxSubscriptions;
|
StreamSubscription<int>? _luxSubscriptions;
|
||||||
double _ev100 = 0.0;
|
|
||||||
|
|
||||||
LightSensorContainerBloc(
|
LightSensorContainerBloc(
|
||||||
this._meteringInteractor,
|
this._meteringInteractor,
|
||||||
MeteringCommunicationBloc communicationBloc,
|
MeteringCommunicationBloc communicationBloc,
|
||||||
) : super(
|
) : super(
|
||||||
communicationBloc,
|
communicationBloc,
|
||||||
const LightSensorInitState(),
|
const LightSensorContainerState(null),
|
||||||
);
|
) {
|
||||||
|
on<LuxMeteringEvent>(_onLuxMeteringEvent);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onCommunicationState(communication_states.SourceState communicationState) {
|
void onCommunicationState(communication_states.SourceState communicationState) {
|
||||||
if (communicationState is communication_states.MeasureState) {
|
if (communicationState is communication_states.MeasureState) {
|
||||||
if (_luxSubscriptions == null) {
|
if (_luxSubscriptions == null) {
|
||||||
_luxSubscriptions = _meteringInteractor.luxStream().listen((event) {
|
_startMetering();
|
||||||
_ev100 = log2(event.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration;
|
|
||||||
communicationBloc.add(communication_event.MeteringInProgressEvent(_ev100));
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
|
_cancelMetering();
|
||||||
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
|
_cancelMetering();
|
||||||
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
|
|
||||||
return super.close();
|
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 {
|
abstract class LightSensorContainerEvent {
|
||||||
const LightSensorContainerEvent();
|
const LightSensorContainerEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LuxMeteringEvent extends LightSensorContainerEvent {
|
||||||
|
final int lux;
|
||||||
|
|
||||||
|
const LuxMeteringEvent(this.lux);
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
abstract class LightSensorContainerState {
|
class LightSensorContainerState {
|
||||||
const LightSensorContainerState();
|
final double? ev100;
|
||||||
}
|
|
||||||
|
|
||||||
class LightSensorInitState extends LightSensorContainerState {
|
const LightSensorContainerState(this.ev100);
|
||||||
const LightSensorInitState();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/communication/state_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();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
void onCommunicationState(communication_states.SourceState communicationState);
|
void onCommunicationState(communication_states.SourceState communicationState);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
import 'package:lightmeter/data/models/film.dart';
|
import 'package:lightmeter/data/models/film.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
abstract class MeteringEvent {
|
sealed class MeteringEvent {
|
||||||
const MeteringEvent();
|
const MeteringEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class StopTypeChangedEvent extends MeteringEvent {
|
|
||||||
final StopType stopType;
|
|
||||||
|
|
||||||
const StopTypeChangedEvent(this.stopType);
|
|
||||||
}
|
|
||||||
|
|
||||||
class EquipmentProfileChangedEvent extends MeteringEvent {
|
class EquipmentProfileChangedEvent extends MeteringEvent {
|
||||||
final EquipmentProfileData equipmentProfileData;
|
final EquipmentProfileData equipmentProfileData;
|
||||||
|
|
||||||
|
@ -18,9 +12,9 @@ class EquipmentProfileChangedEvent extends MeteringEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
class FilmChangedEvent extends MeteringEvent {
|
class FilmChangedEvent extends MeteringEvent {
|
||||||
final Film data;
|
final Film film;
|
||||||
|
|
||||||
const FilmChangedEvent(this.data);
|
const FilmChangedEvent(this.film);
|
||||||
}
|
}
|
||||||
|
|
||||||
class IsoChangedEvent extends MeteringEvent {
|
class IsoChangedEvent extends MeteringEvent {
|
||||||
|
@ -41,10 +35,13 @@ class MeasureEvent extends MeteringEvent {
|
||||||
|
|
||||||
class MeasuredEvent extends MeteringEvent {
|
class MeasuredEvent extends MeteringEvent {
|
||||||
final double ev100;
|
final double ev100;
|
||||||
|
final bool isMetering;
|
||||||
|
|
||||||
const MeasuredEvent(this.ev100);
|
const MeasuredEvent(this.ev100, {required this.isMetering});
|
||||||
}
|
}
|
||||||
|
|
||||||
class MeasureErrorEvent extends MeteringEvent {
|
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/permissions_service.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
import 'package:lightmeter/interactors/metering_interactor.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/bloc_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/screen_metering.dart';
|
import 'package:lightmeter/screens/metering/screen_metering.dart';
|
||||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
|
||||||
|
|
||||||
class MeteringFlow extends StatefulWidget {
|
class MeteringFlow extends StatefulWidget {
|
||||||
const MeteringFlow({super.key});
|
const MeteringFlow({super.key});
|
||||||
|
@ -36,10 +34,8 @@ class _MeteringFlowState extends State<MeteringFlow> {
|
||||||
BlocProvider(create: (_) => MeteringCommunicationBloc()),
|
BlocProvider(create: (_) => MeteringCommunicationBloc()),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => MeteringBloc(
|
create: (context) => MeteringBloc(
|
||||||
context.read<MeteringCommunicationBloc>(),
|
|
||||||
context.get<MeteringInteractor>(),
|
context.get<MeteringInteractor>(),
|
||||||
context.get<EquipmentProfile>(),
|
context.read<MeteringCommunicationBloc>(),
|
||||||
context.get<StopType>(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||||
|
@ -29,9 +31,7 @@ class MeteringScreen extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: BlocBuilder<MeteringBloc, MeteringState>(
|
child: BlocBuilder<MeteringBloc, MeteringState>(
|
||||||
builder: (_, state) => _MeteringContainerBuidler(
|
builder: (_, state) => _MeteringContainerBuidler(
|
||||||
fastest: state is MeteringDataState ? state.fastest : null,
|
ev: state is MeteringDataState ? state.ev : null,
|
||||||
slowest: state is MeteringDataState ? state.slowest : null,
|
|
||||||
exposurePairs: state is MeteringDataState ? state.exposurePairs : [],
|
|
||||||
film: state.film,
|
film: state.film,
|
||||||
iso: state.iso,
|
iso: state.iso,
|
||||||
nd: state.nd,
|
nd: state.nd,
|
||||||
|
@ -45,9 +45,7 @@ class MeteringScreen extends StatelessWidget {
|
||||||
BlocBuilder<MeteringBloc, MeteringState>(
|
BlocBuilder<MeteringBloc, MeteringState>(
|
||||||
builder: (context, state) => MeteringBottomControlsProvider(
|
builder: (context, state) => MeteringBottomControlsProvider(
|
||||||
ev: state is MeteringDataState ? state.ev : null,
|
ev: state is MeteringDataState ? state.ev : null,
|
||||||
isMetering:
|
isMetering: state.isMetering,
|
||||||
state is LoadingState || state is MeteringDataState && state.continuousMetering,
|
|
||||||
hasError: state is MeteringDataState && state.hasError,
|
|
||||||
onSwitchEvSourceType: context.get<Environment>().hasLightSensor
|
onSwitchEvSourceType: context.get<Environment>().hasLightSensor
|
||||||
? EvSourceTypeProvider.of(context).toggleType
|
? EvSourceTypeProvider.of(context).toggleType
|
||||||
: null,
|
: null,
|
||||||
|
@ -73,10 +71,6 @@ class _InheritedListeners extends StatelessWidget {
|
||||||
onDidChangeDependencies: (value) {
|
onDidChangeDependencies: (value) {
|
||||||
context.read<MeteringBloc>().add(EquipmentProfileChangedEvent(value));
|
context.read<MeteringBloc>().add(EquipmentProfileChangedEvent(value));
|
||||||
},
|
},
|
||||||
child: InheritedWidgetListener<StopType>(
|
|
||||||
onDidChangeDependencies: (value) {
|
|
||||||
context.read<MeteringBloc>().add(StopTypeChangedEvent(value));
|
|
||||||
},
|
|
||||||
child: InheritedModelAspectListener<MeteringScreenLayoutFeature, bool>(
|
child: InheritedModelAspectListener<MeteringScreenLayoutFeature, bool>(
|
||||||
aspect: MeteringScreenLayoutFeature.filmPicker,
|
aspect: MeteringScreenLayoutFeature.filmPicker,
|
||||||
onDidChangeDependencies: (value) {
|
onDidChangeDependencies: (value) {
|
||||||
|
@ -84,36 +78,34 @@ class _InheritedListeners extends StatelessWidget {
|
||||||
},
|
},
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MeteringContainerBuidler extends StatelessWidget {
|
class _MeteringContainerBuidler extends StatelessWidget {
|
||||||
final ExposurePair? fastest;
|
final double? ev;
|
||||||
final ExposurePair? slowest;
|
|
||||||
final Film film;
|
final Film film;
|
||||||
final IsoValue iso;
|
final IsoValue iso;
|
||||||
final NdValue nd;
|
final NdValue nd;
|
||||||
final ValueChanged<Film> onFilmChanged;
|
final ValueChanged<Film> onFilmChanged;
|
||||||
final ValueChanged<IsoValue> onIsoChanged;
|
final ValueChanged<IsoValue> onIsoChanged;
|
||||||
final ValueChanged<NdValue> onNdChanged;
|
final ValueChanged<NdValue> onNdChanged;
|
||||||
final List<ExposurePair> exposurePairs;
|
|
||||||
|
|
||||||
const _MeteringContainerBuidler({
|
const _MeteringContainerBuidler({
|
||||||
required this.fastest,
|
required this.ev,
|
||||||
required this.slowest,
|
|
||||||
required this.film,
|
required this.film,
|
||||||
required this.iso,
|
required this.iso,
|
||||||
required this.nd,
|
required this.nd,
|
||||||
required this.onFilmChanged,
|
required this.onFilmChanged,
|
||||||
required this.onIsoChanged,
|
required this.onIsoChanged,
|
||||||
required this.onNdChanged,
|
required this.onNdChanged,
|
||||||
required this.exposurePairs,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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
|
return context.listen<EvSourceType>() == EvSourceType.camera
|
||||||
? CameraContainerProvider(
|
? CameraContainerProvider(
|
||||||
fastest: fastest,
|
fastest: fastest,
|
||||||
|
@ -138,4 +130,68 @@ class _MeteringContainerBuidler extends StatelessWidget {
|
||||||
exposurePairs: exposurePairs,
|
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:flutter/material.dart';
|
||||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
|
||||||
import 'package:lightmeter/data/models/film.dart';
|
import 'package:lightmeter/data/models/film.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
abstract class MeteringState {
|
abstract class MeteringState {
|
||||||
|
final double? ev100;
|
||||||
final Film film;
|
final Film film;
|
||||||
final IsoValue iso;
|
final IsoValue iso;
|
||||||
final NdValue nd;
|
final NdValue nd;
|
||||||
|
final bool isMetering;
|
||||||
|
|
||||||
const MeteringState({
|
const MeteringState({
|
||||||
|
this.ev100,
|
||||||
required this.film,
|
required this.film,
|
||||||
required this.iso,
|
required this.iso,
|
||||||
required this.nd,
|
required this.nd,
|
||||||
|
required this.isMetering,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,24 +24,18 @@ class LoadingState extends MeteringState {
|
||||||
required super.film,
|
required super.film,
|
||||||
required super.iso,
|
required super.iso,
|
||||||
required super.nd,
|
required super.nd,
|
||||||
});
|
}) : super(isMetering: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MeteringDataState extends MeteringState {
|
class MeteringDataState extends MeteringState {
|
||||||
final double? ev;
|
|
||||||
final List<ExposurePair> exposurePairs;
|
|
||||||
final bool continuousMetering;
|
|
||||||
|
|
||||||
const MeteringDataState({
|
const MeteringDataState({
|
||||||
required this.ev,
|
required super.ev100,
|
||||||
required super.film,
|
required super.film,
|
||||||
required super.iso,
|
required super.iso,
|
||||||
required super.nd,
|
required super.nd,
|
||||||
required this.exposurePairs,
|
required super.isMetering,
|
||||||
required this.continuousMetering,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ExposurePair? get fastest => exposurePairs.isEmpty ? null : exposurePairs.first;
|
double? get ev => ev100 != null ? ev100! + log2(iso.value / 100) - nd.stopReduction : null;
|
||||||
ExposurePair? get slowest => exposurePairs.isEmpty ? null : exposurePairs.last;
|
|
||||||
bool get hasError => ev == null;
|
bool get hasError => ev == null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ environment:
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
app_settings: 4.2.0
|
app_settings: 4.2.0
|
||||||
|
bloc_concurrency: 0.2.2
|
||||||
camera: 0.10.5
|
camera: 0.10.5
|
||||||
clipboard: 0.1.3
|
clipboard: 0.1.3
|
||||||
dynamic_color: 1.6.5
|
dynamic_color: 1.6.5
|
||||||
|
@ -16,7 +17,7 @@ dependencies:
|
||||||
firebase_crashlytics: 3.3.1
|
firebase_crashlytics: 3.3.1
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_bloc: 8.1.2
|
flutter_bloc: 8.1.3
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
intl: 0.18.0
|
intl: 0.18.0
|
||||||
|
@ -35,10 +36,15 @@ dependencies:
|
||||||
vibration: 1.7.7
|
vibration: 1.7.7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
bloc_test: 9.1.3
|
||||||
|
build_runner: ^2.1.7
|
||||||
flutter_launcher_icons: 0.11.0
|
flutter_launcher_icons: 0.11.0
|
||||||
flutter_native_splash: 2.2.16
|
flutter_native_splash: 2.2.16
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
google_fonts: 3.0.1
|
google_fonts: 3.0.1
|
||||||
lint: 2.1.2
|
lint: 2.1.2
|
||||||
|
mocktail: 0.3.0
|
||||||
test: 1.24.1
|
test: 1.24.1
|
||||||
|
|
||||||
flutter:
|
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