Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/models/workouts/log.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ class Log {

weight = setConfig.weight;
weightTarget = setConfig.weight;
weightUnitId = setConfig.weightUnitId ?? WEIGHT_UNIT_KG;
// Fall back to the resolved unit object (set during routine hydration from
// the user's metric preference) when the config carries no explicit unit.
weightUnitId = setConfig.weightUnitId ?? setConfig.weightUnit?.id ?? WEIGHT_UNIT_KG;
weightUnitObj = setConfig.weightUnit;

repetitions = setConfig.repetitions;
Expand Down
2 changes: 1 addition & 1 deletion lib/models/workouts/set_config_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class SetConfigData {
this.maxNrOfSets,
this.weight,
this.maxWeight,
this.weightUnitId = WEIGHT_UNIT_KG,
this.weightUnitId,
this.weightRounding,
this.repetitions,
this.maxRepetitions,
Expand Down
2 changes: 1 addition & 1 deletion lib/models/workouts/set_config_data.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 15 additions & 3 deletions lib/providers/routines_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/models/workouts/day.dart';
import 'package:wger/models/workouts/day_data.dart';
import 'package:wger/models/workouts/repetition_unit.dart';
Expand All @@ -30,6 +31,7 @@ import 'package:wger/models/workouts/weight_unit.dart';
import 'package:wger/providers/exercises_notifier.dart';
import 'package:wger/providers/helpers.dart';
import 'package:wger/providers/routines_repository.dart';
import 'package:wger/providers/user_profile_notifier.dart';
import 'package:wger/providers/workout_session_notifier.dart';

part 'routines_notifier.g.dart';
Expand Down Expand Up @@ -111,6 +113,9 @@ class RoutinesRiverpod extends _$RoutinesRiverpod {
ref.listen(exercisesProvider, (_, _) => _rehydrate());
ref.listen(routineRepetitionUnitProvider, (_, _) => _rehydrate());
ref.listen(routineWeightUnitProvider, (_, _) => _rehydrate());
// The default weight unit for set configs without an explicit one depends
// on the user's metric preference, so re-hydrate when the profile arrives.
ref.listen(userProfileProvider, (_, _) => _rehydrate());

return repo.watchAllDrift().map((freshRoutines) {
final existing = state.value?.routines ?? const <Routine>[];
Expand Down Expand Up @@ -140,6 +145,13 @@ class RoutinesRiverpod extends _$RoutinesRiverpod {
final repetitionUnits =
ref.read(routineRepetitionUnitProvider).value ?? const <RepetitionUnit>[];
final weightUnits = ref.read(routineWeightUnitProvider).value ?? const <WeightUnit>[];
final profile = ref.read(userProfileProvider).value;

// Used for set configs that don't carry an explicit weight unit: kg for
// metric users, lb otherwise. Defaults to kg while the profile is still
// syncing (the profile listener re-hydrates once it arrives).
final defaultWeightUnitId = (profile?.isMetric ?? true) ? WEIGHT_UNIT_KG : WEIGHT_UNIT_LB;
final defaultWeightUnit = weightUnits.firstWhereOrNull((u) => u.id == defaultWeightUnitId);

routine.sessions = sessions.where((s) => s.routineId == routine.id).toList();

Expand Down Expand Up @@ -195,9 +207,9 @@ class RoutinesRiverpod extends _$RoutinesRiverpod {
setConfig.repetitionsUnit = repetitionUnits.firstWhereOrNull(
(u) => u.id == setConfig.repetitionsUnitId,
);
setConfig.weightUnit = weightUnits.firstWhereOrNull(
(u) => u.id == setConfig.weightUnitId,
);
setConfig.weightUnit = setConfig.weightUnitId != null
? weightUnits.firstWhereOrNull((u) => u.id == setConfig.weightUnitId)
: defaultWeightUnit;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/providers/routines_notifier.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 21 additions & 3 deletions test/routine/helpers/routine_form_test_overrides.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ import 'package:wger/models/workouts/weight_unit.dart';
import 'package:wger/providers/exercise_repository.dart';
import 'package:wger/providers/exercises_notifier.dart';
import 'package:wger/providers/routines_notifier.dart';
import 'package:wger/providers/user_profile_repository.dart';
import 'package:wger/providers/workout_session_repository.dart';

import 'routine_form_test_overrides.mocks.dart';

export 'routine_form_test_overrides.mocks.dart'
show MockExerciseRepository, MockWorkoutSessionRepository;
show MockExerciseRepository, MockWorkoutSessionRepository, MockUserProfileRepository;

@GenerateMocks([ExerciseRepository, WorkoutSessionRepository])
@GenerateMocks([ExerciseRepository, WorkoutSessionRepository, UserProfileRepository])
/// Repository overrides for the two reference-data notifiers that
/// `RoutinesRiverpod.fetchAndSetRoutineFull` `awaitFirstValue`s on:
/// `exercisesProvider` and `workoutSessionProvider`. Both notifiers read
Expand All @@ -46,15 +47,27 @@ export 'routine_form_test_overrides.mocks.dart'
List<Override> exerciseAndSessionRepoOverrides({
MockExerciseRepository? exercise,
MockWorkoutSessionRepository? session,
MockUserProfileRepository? userProfile,
}) {
final exerciseRepo = exercise ?? _emptyExerciseRepoMock();
final sessionRepo = session ?? _emptySessionRepoMock();
return [
exerciseRepositoryProvider.overrideWithValue(exerciseRepo),
workoutSessionRepositoryProvider.overrideWithValue(sessionRepo),
// RoutinesRiverpod.build listens to the user profile to pick the default
// weight unit; stub it so the real Drift-backed repo isn't pulled in.
// Defaults to a null (metric) profile; pass [userProfile] to exercise the
// imperial default-unit path.
userProfileRepositoryProvider.overrideWithValue(userProfile ?? _emptyUserProfileRepoMock()),
];
}

MockUserProfileRepository _emptyUserProfileRepoMock() {
final mock = MockUserProfileRepository();
when(mock.watchDrift()).thenAnswer((_) => Stream.value(null));
return mock;
}

MockExerciseRepository _emptyExerciseRepoMock() {
final mock = MockExerciseRepository();
when(
Expand All @@ -80,10 +93,15 @@ MockWorkoutSessionRepository _emptySessionRepoMock() {
List<Override> routineFormAmbientOverrides({
MockExerciseRepository? exercise,
MockWorkoutSessionRepository? session,
MockUserProfileRepository? userProfile,
List<RepetitionUnit> repetitionUnits = const [],
List<WeightUnit> weightUnits = const [],
}) => [
...exerciseAndSessionRepoOverrides(exercise: exercise, session: session),
...exerciseAndSessionRepoOverrides(
exercise: exercise,
session: session,
userProfile: userProfile,
),
routineRepetitionUnitProvider.overrideWith(
(ref) => Stream<List<RepetitionUnit>>.value(repetitionUnits),
),
Expand Down
28 changes: 28 additions & 0 deletions test/routine/helpers/routine_form_test_overrides.mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import 'package:wger/models/exercises/category.dart' as _i6;
import 'package:wger/models/exercises/equipment.dart' as _i8;
import 'package:wger/models/exercises/exercise_filters.dart' as _i5;
import 'package:wger/models/exercises/muscle.dart' as _i9;
import 'package:wger/models/user/user_profile.dart' as _i14;
import 'package:wger/models/workouts/session.dart' as _i12;
import 'package:wger/providers/exercise_repository.dart' as _i2;
import 'package:wger/providers/exercises_notifier.dart' as _i7;
import 'package:wger/providers/user_profile_repository.dart' as _i13;
import 'package:wger/providers/workout_session_repository.dart' as _i11;

// ignore_for_file: type=lint
Expand Down Expand Up @@ -163,3 +165,29 @@ class MockWorkoutSessionRepository extends _i1.Mock implements _i11.WorkoutSessi
)
as _i3.Future<void>);
}

/// A class which mocks [UserProfileRepository].
///
/// See the documentation for Mockito's code generation for more information.
class MockUserProfileRepository extends _i1.Mock implements _i13.UserProfileRepository {
MockUserProfileRepository() {
_i1.throwOnMissingStub(this);
}

@override
_i3.Stream<_i14.UserProfile?> watchDrift() =>
(super.noSuchMethod(
Invocation.method(#watchDrift, []),
returnValue: _i3.Stream<_i14.UserProfile?>.empty(),
)
as _i3.Stream<_i14.UserProfile?>);

@override
_i3.Future<void> editLocalDrift(_i14.UserProfile? profile) =>
(super.noSuchMethod(
Invocation.method(#editLocalDrift, [profile]),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
)
as _i3.Future<void>);
}
21 changes: 20 additions & 1 deletion test/routine/models/log_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2020, 2025 wger Team
* Copyright (c) 2020 - 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
Expand All @@ -22,6 +22,7 @@ import 'package:wger/models/exercises/category.dart';
import 'package:wger/models/exercises/exercise.dart';
import 'package:wger/models/workouts/log.dart';
import 'package:wger/models/workouts/set_config_data.dart';
import 'package:wger/models/workouts/weight_unit.dart';

void main() {
group('Log.volume', () {
Expand Down Expand Up @@ -178,6 +179,24 @@ void main() {
expect(log.repetitionsUnitId, REP_UNIT_TILL_FAILURE_ID);
});

test('derives the unit ID from the resolved unit object when no explicit ID is set', () {
// Imperial users: routine hydration leaves weightUnitId null and only
// resolves the weightUnit object (lb) from the profile. The log must pick
// up lb for *both* the ID and the object
const lb = WeightUnit(id: WEIGHT_UNIT_LB, name: 'lb');
final setConfig = SetConfigData(
exerciseId: 1,
slotEntryId: 1,
exercise: exercise,
weightUnit: lb,
);

final log = Log.fromSetConfigData(setConfig);

expect(log.weightUnitId, WEIGHT_UNIT_LB);
expect(log.weightUnitObj, lb);
});

test('copies weight and repetitions values', () {
final setConfig = SetConfigData(
exerciseId: 1,
Expand Down
14 changes: 13 additions & 1 deletion test/routine/routine_form_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import 'package:wger/providers/exercises_notifier.dart';
import 'package:wger/providers/network_provider.dart';
import 'package:wger/providers/routines_notifier.dart';
import 'package:wger/providers/routines_repository.dart';
import 'package:wger/providers/user_profile_repository.dart';
import 'package:wger/providers/workout_session_repository.dart';
import 'package:wger/screens/routine_edit_screen.dart';
import 'package:wger/screens/routine_screen.dart';
Expand All @@ -46,13 +47,19 @@ import '../../test_data/routines.dart';
import '../fake_connectivity.dart';
import './routine_form_test.mocks.dart';

@GenerateMocks([RoutinesRepository, WorkoutSessionRepository, ExerciseRepository])
@GenerateMocks([
RoutinesRepository,
WorkoutSessionRepository,
ExerciseRepository,
UserProfileRepository,
])
void main() {
installFakeConnectivity();

late MockRoutinesRepository mockRoutinesRepository;
late MockWorkoutSessionRepository mockSessionRepo;
late MockExerciseRepository mockExerciseRepo;
late MockUserProfileRepository mockUserProfileRepo;
late StreamController<List<Routine>> routineStream;
late Routine existingRoutine;
late Routine newRoutine;
Expand Down Expand Up @@ -92,6 +99,10 @@ void main() {
when(
mockExerciseRepo.watchAllDrift(),
).thenAnswer((_) => Stream.value(const ExerciseState(<Exercise>[])));
// RoutinesRiverpod.build() also listens to the user profile for the default
// weight unit; stub it so the real PowerSync DB isn't pulled in.
mockUserProfileRepo = MockUserProfileRepository();
when(mockUserProfileRepo.watchDrift()).thenAnswer((_) => Stream.value(null));
});

tearDown(() {
Expand All @@ -106,6 +117,7 @@ void main() {
routinesRepositoryProvider.overrideWithValue(mockRoutinesRepository),
workoutSessionRepositoryProvider.overrideWithValue(mockSessionRepo),
exerciseRepositoryProvider.overrideWithValue(mockExerciseRepo),
userProfileRepositoryProvider.overrideWithValue(mockUserProfileRepo),
networkStatusProvider.overrideWithValue(isOnline),
routineRepetitionUnitProvider.overrideWith(
(ref) => Stream<List<RepetitionUnit>>.value(testRepetitionUnits),
Expand Down
28 changes: 28 additions & 0 deletions test/routine/routine_form_test.mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:wger/models/exercises/category.dart' as _i16;
import 'package:wger/models/exercises/equipment.dart' as _i18;
import 'package:wger/models/exercises/exercise_filters.dart' as _i15;
import 'package:wger/models/exercises/muscle.dart' as _i19;
import 'package:wger/models/user/user_profile.dart' as _i22;
import 'package:wger/models/workouts/base_config.dart' as _i6;
import 'package:wger/models/workouts/day.dart' as _i3;
import 'package:wger/models/workouts/repetition_unit.dart' as _i10;
Expand All @@ -23,6 +24,7 @@ import 'package:wger/models/workouts/weight_unit.dart' as _i9;
import 'package:wger/providers/exercise_repository.dart' as _i13;
import 'package:wger/providers/exercises_notifier.dart' as _i17;
import 'package:wger/providers/routines_repository.dart' as _i7;
import 'package:wger/providers/user_profile_repository.dart' as _i21;
import 'package:wger/providers/workout_session_repository.dart' as _i11;

// ignore_for_file: type=lint
Expand Down Expand Up @@ -409,3 +411,29 @@ class MockExerciseRepository extends _i1.Mock implements _i13.ExerciseRepository
)
as _i8.Stream<List<_i20.Language>>);
}

/// A class which mocks [UserProfileRepository].
///
/// See the documentation for Mockito's code generation for more information.
class MockUserProfileRepository extends _i1.Mock implements _i21.UserProfileRepository {
MockUserProfileRepository() {
_i1.throwOnMissingStub(this);
}

@override
_i8.Stream<_i22.UserProfile?> watchDrift() =>
(super.noSuchMethod(
Invocation.method(#watchDrift, []),
returnValue: _i8.Stream<_i22.UserProfile?>.empty(),
)
as _i8.Stream<_i22.UserProfile?>);

@override
_i8.Future<void> editLocalDrift(_i22.UserProfile? profile) =>
(super.noSuchMethod(
Invocation.method(#editLocalDrift, [profile]),
returnValue: _i8.Future<void>.value(),
returnValueForMissingStub: _i8.Future<void>.value(),
)
as _i8.Future<void>);
}
Loading