diff --git a/lib/providers/plate_weights.dart b/lib/providers/plate_weights.dart index 26f56472..65806c66 100644 --- a/lib/providers/plate_weights.dart +++ b/lib/providers/plate_weights.dart @@ -13,11 +13,12 @@ const DEFAULT_LB_PLATES = [2.5, 5, 10, 25, 35, 45]; const PREFS_KEY_PLATES = 'selectedPlates'; -final plateWeightsProvider = StateNotifierProvider((ref) { - return PlateWeightsNotifier(); +final plateCalculatorProvider = + StateNotifierProvider((ref) { + return PlateCalculatorNotifier(); }); -class PlateWeightsState { +class PlateCalculatorState { final _logger = Logger('PlateWeightsState'); final barWeightKg = 20; @@ -53,7 +54,7 @@ class PlateWeightsState { final List availablePlatesKg = const [0.5, 1, 1.25, 2, 2.5, 5, 10, 15, 20, 25]; final List availablePlatesLb = const [2.5, 5, 10, 25, 35, 45]; - PlateWeightsState({ + PlateCalculatorState({ this.isMetric = true, this.totalWeight = 0, List? selectedPlates, @@ -90,12 +91,12 @@ class PlateWeightsState { return {'isMetric': isMetric, 'selectedPlates': selectedPlates}; } - PlateWeightsState copyWith({ + PlateCalculatorState copyWith({ bool? isMetric, num? totalWeight, List? selectedPlates, }) { - return PlateWeightsState( + return PlateCalculatorState( isMetric: isMetric ?? this.isMetric, totalWeight: totalWeight ?? this.totalWeight, selectedPlates: selectedPlates ?? this.selectedPlates, @@ -103,21 +104,23 @@ class PlateWeightsState { } } -class PlateWeightsNotifier extends StateNotifier { - final _logger = Logger('PlateWeightsNotifier'); +class PlateCalculatorNotifier extends StateNotifier { + final _logger = Logger('PlateCalculatorNotifier'); late SharedPreferencesAsync prefs; - PlateWeightsNotifier({SharedPreferencesAsync? prefs}) : super(PlateWeightsState()) { + PlateCalculatorNotifier({SharedPreferencesAsync? prefs}) : super(PlateCalculatorState()) { this.prefs = prefs ?? PreferenceHelper.asyncPref; _readDataFromSharedPrefs(); } Future saveToSharedPrefs() async { + _logger.fine('Saving plate data to SharedPreferences'); await prefs.setString(PREFS_KEY_PLATES, jsonEncode(state.toJson())); } Future _readDataFromSharedPrefs() async { + _logger.fine('Reading plate data from SharedPreferences'); final prefsData = await prefs.getString(PREFS_KEY_PLATES); if (prefsData != null) { diff --git a/lib/screens/add_plate_weights.dart b/lib/screens/add_plate_weights.dart index b9bdb16a..8c5b56d7 100644 --- a/lib/screens/add_plate_weights.dart +++ b/lib/screens/add_plate_weights.dart @@ -22,7 +22,7 @@ class _AddPlateWeightsState extends ConsumerState void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - ref.read(plateWeightsProvider.notifier); + ref.read(plateCalculatorProvider.notifier); }); } @@ -36,8 +36,8 @@ class _AddPlateWeightsState extends ConsumerState Widget build(BuildContext context) { final i18n = AppLocalizations.of(context); - final plateWeightsState = ref.watch(plateWeightsProvider); - final plateWeightsNotifier = ref.read(plateWeightsProvider.notifier); + final plateWeightsState = ref.watch(plateCalculatorProvider); + final plateWeightsNotifier = ref.read(plateCalculatorProvider.notifier); final userProvider = provider.Provider.of(context); return Scaffold( diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index c621b7c9..0bcd399e 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -166,7 +166,7 @@ class _LogPageState extends ConsumerState { setState(() { widget._log.weight = newValue; _weightController.text = newValue.toString(); - ref.read(plateWeightsProvider.notifier).setWeight( + ref.read(plateCalculatorProvider.notifier).setWeight( _weightController.text == '' ? 0 : double.parse(_weightController.text), ); }); @@ -189,7 +189,7 @@ class _LogPageState extends ConsumerState { num.parse(value); setState(() { widget._log.weight = num.parse(value); - ref.read(plateWeightsProvider.notifier).setWeight( + ref.read(plateCalculatorProvider.notifier).setWeight( _weightController.text == '' ? 0 : double.parse(_weightController.text), ); }); @@ -218,7 +218,7 @@ class _LogPageState extends ConsumerState { setState(() { widget._log.weight = newValue; _weightController.text = newValue.toString(); - ref.read(plateWeightsProvider.notifier).setWeight( + ref.read(plateCalculatorProvider.notifier).setWeight( _weightController.text == '' ? 0 : double.parse(_weightController.text), ); }); @@ -379,7 +379,7 @@ class _LogPageState extends ConsumerState { } Widget getPlates() { - final plateWeightsState = ref.watch(plateWeightsProvider); + final plateWeightsState = ref.watch(plateCalculatorProvider); return Column( children: [ @@ -414,7 +414,7 @@ class _LogPageState extends ConsumerState { size: 37, padding: 2, margin: 0, - color: ref.read(plateWeightsProvider).getColor(entry.key), + color: ref.read(plateCalculatorProvider).getColor(entry.key), ), const SizedBox(width: 10), ], diff --git a/test/providers/plate_calculator_test.dart b/test/providers/plate_calculator_test.dart new file mode 100644 index 00000000..70298308 --- /dev/null +++ b/test/providers/plate_calculator_test.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; // Added for annotations +import 'package:mockito/mockito.dart'; // Added for mockito +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:wger/helpers/consts.dart'; +import 'package:wger/providers/plate_weights.dart'; + +import 'plate_calculator_test.mocks.dart'; + +@GenerateMocks([SharedPreferencesAsync]) +void main() { + group('PlateWeightsNotifier', () { + late PlateCalculatorNotifier notifier; + late MockSharedPreferencesAsync mockPrefs; + + setUp(() { + mockPrefs = MockSharedPreferencesAsync(); + when(mockPrefs.getString(PREFS_KEY_PLATES)).thenAnswer((_) async => null); + when(mockPrefs.setString(any, any)).thenAnswer((_) async => true); + + notifier = PlateCalculatorNotifier(prefs: mockPrefs); + }); + + test('toggleSelection adds and removes plates', () async { + // Test adding a plate + await notifier.toggleSelection(0.5); + expect(notifier.state.selectedPlates.contains(0.5), true); + + // Test removing a plate + await notifier.toggleSelection(0.5); + expect(notifier.state.selectedPlates.contains(0.5), false); + }); + + test('unitChange updates state correctly', () { + // Change to imperial + notifier.setWeight(123); + notifier.unitChange(unit: WeightUnitEnum.lb); + expect(notifier.state.isMetric, false); + expect(notifier.state.totalWeight, 0); + expect(notifier.state.selectedPlates, DEFAULT_LB_PLATES); + expect(notifier.state.barWeight, notifier.state.barWeightLb); + expect(notifier.state.availablePlates, notifier.state.availablePlatesLb); + + // Change back to metric + notifier.setWeight(123); + notifier.unitChange(unit: WeightUnitEnum.kg); + expect(notifier.state.isMetric, true); + expect(notifier.state.totalWeight, 0); + expect(notifier.state.selectedPlates, DEFAULT_KG_PLATES); + expect(notifier.state.barWeight, notifier.state.barWeightKg); + expect(notifier.state.availablePlates, notifier.state.availablePlatesKg); + }); + + test('setWeight updates totalWeight', () { + notifier.setWeight(100); + expect(notifier.state.totalWeight, 100); + }); + }); + + group('PlateWeightsState', () { + test('copyWith creates a new instance with updated values', () { + final initialState = PlateCalculatorState(); + final updatedState = initialState.copyWith( + isMetric: false, + totalWeight: 100, + selectedPlates: [1, 2, 3], + ); + + expect(updatedState.isMetric, false); + expect(updatedState.totalWeight, 100); + expect(updatedState.selectedPlates, [1, 2, 3]); + }); + + test('toJson returns correct map', () { + final state = PlateCalculatorState(isMetric: false, selectedPlates: [10, 20]); + final json = state.toJson(); + expect(json['isMetric'], false); + expect(json['selectedPlates'], [10, 20]); + }); + + test('barWeight returns correct value based on isMetric', () { + final metricState = PlateCalculatorState(isMetric: true); + expect(metricState.barWeight, metricState.barWeightKg); + + final imperialState = PlateCalculatorState(isMetric: false); + expect(imperialState.barWeight, imperialState.barWeightLb); + }); + + test('availablePlates returns correct list based on isMetric', () { + final metricState = PlateCalculatorState(isMetric: true); + expect(metricState.availablePlates, metricState.availablePlatesKg); + + final imperialState = PlateCalculatorState(isMetric: false); + expect(imperialState.availablePlates, metricState.availablePlatesLb); + }); + + test('getColor returns correct color', () { + final metricState = PlateCalculatorState(isMetric: true); + expect(metricState.getColor(20), Colors.blue); + expect(metricState.getColor(0.1), Colors.grey, reason: 'Fallback color'); + + final imperialState = PlateCalculatorState(isMetric: false); + expect(imperialState.getColor(45), Colors.blue); + expect(imperialState.getColor(0.1), Colors.grey, reason: 'Fallback color'); + }); + }); +} diff --git a/test/providers/plate_calculator_test.mocks.dart b/test/providers/plate_calculator_test.mocks.dart new file mode 100644 index 00000000..df2e0a09 --- /dev/null +++ b/test/providers/plate_calculator_test.mocks.dart @@ -0,0 +1,213 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in wger/test/providers/plate_weights_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:shared_preferences/src/shared_preferences_async.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [SharedPreferencesAsync]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockSharedPreferencesAsync extends _i1.Mock implements _i2.SharedPreferencesAsync { + MockSharedPreferencesAsync() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future> getKeys({Set? allowList}) => (super.noSuchMethod( + Invocation.method( + #getKeys, + [], + {#allowList: allowList}, + ), + returnValue: _i3.Future>.value({}), + ) as _i3.Future>); + + @override + _i3.Future> getAll({Set? allowList}) => (super.noSuchMethod( + Invocation.method( + #getAll, + [], + {#allowList: allowList}, + ), + returnValue: _i3.Future>.value({}), + ) as _i3.Future>); + + @override + _i3.Future getBool(String? key) => (super.noSuchMethod( + Invocation.method( + #getBool, + [key], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future getInt(String? key) => (super.noSuchMethod( + Invocation.method( + #getInt, + [key], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future getDouble(String? key) => (super.noSuchMethod( + Invocation.method( + #getDouble, + [key], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future getString(String? key) => (super.noSuchMethod( + Invocation.method( + #getString, + [key], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future?> getStringList(String? key) => (super.noSuchMethod( + Invocation.method( + #getStringList, + [key], + ), + returnValue: _i3.Future?>.value(), + ) as _i3.Future?>); + + @override + _i3.Future containsKey(String? key) => (super.noSuchMethod( + Invocation.method( + #containsKey, + [key], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); + + @override + _i3.Future setBool( + String? key, + bool? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setBool, + [ + key, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future setInt( + String? key, + int? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setInt, + [ + key, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future setDouble( + String? key, + double? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setDouble, + [ + key, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future setString( + String? key, + String? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setString, + [ + key, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future setStringList( + String? key, + List? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setStringList, + [ + key, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future remove(String? key) => (super.noSuchMethod( + Invocation.method( + #remove, + [key], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future clear({Set? allowList}) => (super.noSuchMethod( + Invocation.method( + #clear, + [], + {#allowList: allowList}, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); +}