Merge branch 'master' into feature/trophies

# Conflicts:
#	lib/providers/base_provider.dart
#	lib/providers/gym_state.dart
#	lib/providers/gym_state.g.dart
#	lib/widgets/routines/gym_mode/log_page.dart
#	test/core/settings_test.mocks.dart
#	test/exercises/contribute_exercise_image_test.mocks.dart
#	test/gallery/gallery_form_test.mocks.dart
#	test/gallery/gallery_screen_test.mocks.dart
#	test/measurements/measurement_provider_test.mocks.dart
#	test/nutrition/nutritional_plan_screen_test.mocks.dart
#	test/nutrition/nutritional_plans_screen_test.mocks.dart
#	test/routine/gym_mode/gym_mode_test.mocks.dart
#	test/routine/routine_screen_test.mocks.dart
#	test/routine/routines_provider_test.mocks.dart
#	test/routine/routines_screen_test.mocks.dart
#	test/user/provider_test.mocks.dart
#	test/weight/weight_provider_test.mocks.dart
#	test/widgets/routines/gym_mode/log_page_test.dart
This commit is contained in:
Roland Geider
2026-01-16 16:54:08 +01:00
32 changed files with 1559 additions and 575 deletions

View File

@@ -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
@@ -25,20 +25,24 @@ import 'package:wger/core/exceptions/http_exception.dart';
void main() {
group('WgerHttpException', () {
test('parses valid JSON response', () {
// Arrange
final resp = http.Response(
'{"foo":"bar"}',
400,
headers: {HttpHeaders.contentTypeHeader: 'application/json'},
);
// Act
final ex = WgerHttpException(resp);
// Assert
expect(ex.type, ErrorType.json);
expect(ex.errors['foo'], 'bar');
expect(ex.toString(), contains('WgerHttpException'));
});
test('falls back on malformed JSON', () {
// Arrange
const body = '{"foo":';
final resp = http.Response(
body,
@@ -46,13 +50,16 @@ void main() {
headers: {HttpHeaders.contentTypeHeader: 'application/json'},
);
// Act
final ex = WgerHttpException(resp);
// Assert
expect(ex.type, ErrorType.json);
expect(ex.errors['unknown_error'], body);
});
test('detects HTML response', () {
test('detects HTML response from headers', () {
// Arrange
const body = '<html lang="en"><body>Error</body></html>';
final resp = http.Response(
body,
@@ -60,16 +67,39 @@ void main() {
headers: {HttpHeaders.contentTypeHeader: 'text/html; charset=utf-8'},
);
// Act
final ex = WgerHttpException(resp);
// Assert
expect(ex.type, ErrorType.html);
expect(ex.htmlError, body);
});
test('detects HTML response from content', () {
// Arrange
const body = '<html lang="en"><body>Error</body></html>';
final resp = http.Response(
body,
500,
headers: {HttpHeaders.contentTypeHeader: 'text/foo; charset=utf-8'},
);
// Act
final ex = WgerHttpException(resp);
// Assert
expect(ex.type, ErrorType.html);
expect(ex.htmlError, body);
});
test('fromMap sets errors and type', () {
// Arrange
final map = <String, dynamic>{'field': 'value'};
// Act
final ex = WgerHttpException.fromMap(map);
// Assert
expect(ex.type, ErrorType.json);
expect(ex.errors, map);
});

View File

@@ -1,3 +1,21 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:convert';
import 'package:drift/native.dart';
@@ -100,19 +118,25 @@ void main() {
SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty();
// Mock categories
when(mockBaseProvider.makeUrl(categoryUrl)).thenReturn(tCategoryEntriesUri);
when(
mockBaseProvider.makeUrl(categoryUrl, query: anyNamed('query')),
).thenReturn(tCategoryEntriesUri);
when(
mockBaseProvider.fetchPaginated(tCategoryEntriesUri),
).thenAnswer((_) => Future.value(tCategoryMap['results']));
// Mock muscles
when(mockBaseProvider.makeUrl(muscleUrl)).thenReturn(tMuscleEntriesUri);
when(
mockBaseProvider.makeUrl(muscleUrl, query: anyNamed('query')),
).thenReturn(tMuscleEntriesUri);
when(
mockBaseProvider.fetchPaginated(tMuscleEntriesUri),
).thenAnswer((_) => Future.value(tMuscleMap['results']));
// Mock equipment
when(mockBaseProvider.makeUrl(equipmentUrl)).thenReturn(tEquipmentEntriesUri);
when(
mockBaseProvider.makeUrl(equipmentUrl, query: anyNamed('query')),
).thenReturn(tEquipmentEntriesUri);
when(
mockBaseProvider.fetchPaginated(tEquipmentEntriesUri),
).thenAnswer((_) => Future.value(tEquipmentMap['results']));

View File

@@ -1,3 +1,21 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:convert';
import 'package:drift/drift.dart';
@@ -105,19 +123,25 @@ void main() {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
// Mock categories
when(mockBaseProvider.makeUrl(categoryUrl)).thenReturn(tCategoryEntriesUri);
when(
mockBaseProvider.makeUrl(categoryUrl, query: anyNamed('query')),
).thenReturn(tCategoryEntriesUri);
when(
mockBaseProvider.fetchPaginated(tCategoryEntriesUri),
).thenAnswer((_) => Future.value(tCategoryMap['results']));
// Mock muscles
when(mockBaseProvider.makeUrl(muscleUrl)).thenReturn(tMuscleEntriesUri);
when(
mockBaseProvider.makeUrl(muscleUrl, query: anyNamed('query')),
).thenReturn(tMuscleEntriesUri);
when(
mockBaseProvider.fetchPaginated(tMuscleEntriesUri),
).thenAnswer((_) => Future.value(tMuscleMap['results']));
// Mock equipment
when(mockBaseProvider.makeUrl(equipmentUrl)).thenReturn(tEquipmentEntriesUri);
when(
mockBaseProvider.makeUrl(equipmentUrl, query: anyNamed('query')),
).thenReturn(tEquipmentEntriesUri);
when(
mockBaseProvider.fetchPaginated(tEquipmentEntriesUri),
).thenAnswer((_) => Future.value(tEquipmentMap['results']));

View File

@@ -0,0 +1,135 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:wger/core/exceptions/http_exception.dart';
import 'package:wger/providers/base_provider.dart';
import '../utils.dart';
import 'base_provider.mocks.dart';
@GenerateMocks([Client])
void main() {
final Uri testUri = Uri(scheme: 'https', host: 'localhost', path: 'api/v2/test/');
test('Retry on SocketException then succeeds', () async {
// Arrange
final mockClient = MockClient();
var callCount = 0;
when(mockClient.get(testUri, headers: anyNamed('headers'))).thenAnswer((_) {
if (callCount == 0) {
callCount++;
return Future.error(const SocketException('conn fail'));
}
return Future.value(Response('{"ok": true}', 200));
});
// Act
final provider = WgerBaseProvider(testAuthProvider, mockClient);
final result = await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1));
// Assert
expect(result, isA<Map>());
expect(result['ok'], isTrue);
verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(2);
});
test('Retry on 5xx then succeeds', () async {
// Arrange
final mockClient = MockClient();
var callCount = 0;
when(mockClient.get(testUri, headers: anyNamed('headers'))).thenAnswer((_) {
if (callCount == 0) {
callCount++;
return Future.value(Response('{"msg":"error"}', 502));
}
return Future.value(Response('{"ok": true}', 200));
});
// Act
final provider = WgerBaseProvider(testAuthProvider, mockClient);
final result = await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1));
// Assert
expect(result, isA<Map>());
expect(result['ok'], isTrue);
verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(2);
});
test('Do not retry on 4xx client error', () async {
// Arrange
final mockClient = MockClient();
when(
mockClient.get(testUri, headers: anyNamed('headers')),
).thenAnswer((_) => Future.value(Response('{"error":"bad"}', 400)));
// Act
final provider = WgerBaseProvider(testAuthProvider, mockClient);
// Assert
await expectLater(
provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)),
throwsA(isA<WgerHttpException>()),
);
verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(1);
});
test('Exceed max retries and rethrow after retries', () async {
// Arrange
final mockClient = MockClient();
when(
mockClient.get(testUri, headers: anyNamed('headers')),
).thenAnswer((_) => Future.error(ClientException('conn fail')));
// Act
final provider = WgerBaseProvider(testAuthProvider, mockClient);
dynamic caught;
try {
await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1));
} catch (e) {
caught = e;
}
// Assert
expect(caught, isA<ClientException>());
// initial try + 3 retries = 4 calls
verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(4);
});
test('Request succeeds without retries', () async {
// Arrange
final mockClient = MockClient();
when(
mockClient.get(testUri, headers: anyNamed('headers')),
).thenAnswer((_) => Future.value(Response('{"ok": true}', 200)));
// Act
final provider = WgerBaseProvider(testAuthProvider, mockClient);
final result = await provider.fetch(testUri);
// Assert
expect(result, isA<Map>());
expect(result['ok'], isTrue);
verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(1);
});
}

View File

@@ -0,0 +1,218 @@
// Mocks generated by Mockito 5.4.6 from annotations
// in wger/test/providers/base_provider.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i3;
import 'dart:convert' as _i4;
import 'dart:typed_data' as _i6;
import 'package:http/http.dart' as _i2;
import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i5;
// 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
// ignore_for_file: invalid_use_of_internal_member
class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response {
_FakeResponse_0(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
}
class _FakeStreamedResponse_1 extends _i1.SmartFake implements _i2.StreamedResponse {
_FakeStreamedResponse_1(Object parent, Invocation parentInvocation)
: super(parent, parentInvocation);
}
/// A class which mocks [Client].
///
/// See the documentation for Mockito's code generation for more information.
class MockClient extends _i1.Mock implements _i2.Client {
MockClient() {
_i1.throwOnMissingStub(this);
}
@override
_i3.Future<_i2.Response> head(Uri? url, {Map<String, String>? headers}) =>
(super.noSuchMethod(
Invocation.method(#head, [url], {#headers: headers}),
returnValue: _i3.Future<_i2.Response>.value(
_FakeResponse_0(
this,
Invocation.method(#head, [url], {#headers: headers}),
),
),
)
as _i3.Future<_i2.Response>);
@override
_i3.Future<_i2.Response> get(Uri? url, {Map<String, String>? headers}) =>
(super.noSuchMethod(
Invocation.method(#get, [url], {#headers: headers}),
returnValue: _i3.Future<_i2.Response>.value(
_FakeResponse_0(
this,
Invocation.method(#get, [url], {#headers: headers}),
),
),
)
as _i3.Future<_i2.Response>);
@override
_i3.Future<_i2.Response> post(
Uri? url, {
Map<String, String>? headers,
Object? body,
_i4.Encoding? encoding,
}) =>
(super.noSuchMethod(
Invocation.method(
#post,
[url],
{#headers: headers, #body: body, #encoding: encoding},
),
returnValue: _i3.Future<_i2.Response>.value(
_FakeResponse_0(
this,
Invocation.method(
#post,
[url],
{#headers: headers, #body: body, #encoding: encoding},
),
),
),
)
as _i3.Future<_i2.Response>);
@override
_i3.Future<_i2.Response> put(
Uri? url, {
Map<String, String>? headers,
Object? body,
_i4.Encoding? encoding,
}) =>
(super.noSuchMethod(
Invocation.method(
#put,
[url],
{#headers: headers, #body: body, #encoding: encoding},
),
returnValue: _i3.Future<_i2.Response>.value(
_FakeResponse_0(
this,
Invocation.method(
#put,
[url],
{#headers: headers, #body: body, #encoding: encoding},
),
),
),
)
as _i3.Future<_i2.Response>);
@override
_i3.Future<_i2.Response> patch(
Uri? url, {
Map<String, String>? headers,
Object? body,
_i4.Encoding? encoding,
}) =>
(super.noSuchMethod(
Invocation.method(
#patch,
[url],
{#headers: headers, #body: body, #encoding: encoding},
),
returnValue: _i3.Future<_i2.Response>.value(
_FakeResponse_0(
this,
Invocation.method(
#patch,
[url],
{#headers: headers, #body: body, #encoding: encoding},
),
),
),
)
as _i3.Future<_i2.Response>);
@override
_i3.Future<_i2.Response> delete(
Uri? url, {
Map<String, String>? headers,
Object? body,
_i4.Encoding? encoding,
}) =>
(super.noSuchMethod(
Invocation.method(
#delete,
[url],
{#headers: headers, #body: body, #encoding: encoding},
),
returnValue: _i3.Future<_i2.Response>.value(
_FakeResponse_0(
this,
Invocation.method(
#delete,
[url],
{#headers: headers, #body: body, #encoding: encoding},
),
),
),
)
as _i3.Future<_i2.Response>);
@override
_i3.Future<String> read(Uri? url, {Map<String, String>? headers}) =>
(super.noSuchMethod(
Invocation.method(#read, [url], {#headers: headers}),
returnValue: _i3.Future<String>.value(
_i5.dummyValue<String>(
this,
Invocation.method(#read, [url], {#headers: headers}),
),
),
)
as _i3.Future<String>);
@override
_i3.Future<_i6.Uint8List> readBytes(
Uri? url, {
Map<String, String>? headers,
}) =>
(super.noSuchMethod(
Invocation.method(#readBytes, [url], {#headers: headers}),
returnValue: _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)),
)
as _i3.Future<_i6.Uint8List>);
@override
_i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) =>
(super.noSuchMethod(
Invocation.method(#send, [request]),
returnValue: _i3.Future<_i2.StreamedResponse>.value(
_FakeStreamedResponse_1(
this,
Invocation.method(#send, [request]),
),
),
)
as _i3.Future<_i2.StreamedResponse>);
@override
void close() => super.noSuchMethod(
Invocation.method(#close, []),
returnValueForMissingStub: null,
);
}

View File

@@ -1,21 +1,3 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2025 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Mocks generated by Mockito 5.4.6 from annotations
// in wger/test/providers/plate_calculator_test.dart.
// Do not manually edit this file.

View File

@@ -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
@@ -109,12 +109,10 @@ void main() {
await withClock(Clock.fixed(DateTime(2025, 3, 29, 14, 33)), () async {
await tester.pumpWidget(renderGymMode());
await tester.pumpAndSettle();
await tester.tap(find.byType(TextButton));
await tester.pumpAndSettle();
//await tester.ensureVisible(find.byKey(Key(key as String)));
//
// Start page
//
@@ -306,6 +304,7 @@ void main() {
expect(find.byIcon(Icons.chevron_right), findsNothing);
});
},
tags: ['golden'],
semanticsEnabled: false,
);
}

View File

@@ -128,6 +128,7 @@ void main() {
// Act
notifier.calculatePages();
notifier.setCurrentPage(2);
// Assert
expect(notifier.state.getSlotEntryPageByIndex()!.type, SlotPageType.log);
@@ -159,6 +160,7 @@ void main() {
iteration: 1,
);
notifier.calculatePages();
notifier.setCurrentPage(2);
// Act
// Log page is at index 2
@@ -189,15 +191,16 @@ void main() {
testWidgets('save button calls addLog on RoutinesProvider', (tester) async {
// Arrange
final gymNotifier = container.read(gymStateProvider.notifier);
final notifier = container.read(gymStateProvider.notifier);
final routine = testdata.getTestRoutine();
gymNotifier.state = gymNotifier.state.copyWith(
notifier.state = notifier.state.copyWith(
dayId: routine.days.first.id,
routine: routine,
iteration: 1,
);
gymNotifier.calculatePages();
gymNotifier.state = gymNotifier.state.copyWith(currentPage: 2);
notifier.calculatePages();
notifier.setCurrentPage(2);
notifier.state = notifier.state.copyWith(currentPage: 2);
final mockRoutines = MockRoutinesProvider();
// Act
@@ -206,8 +209,8 @@ void main() {
final editableFields = find.byType(EditableText);
expect(editableFields, findsWidgets);
await tester.enterText(editableFields.at(0), '7');
await tester.enterText(editableFields.at(1), '77');
await tester.enterText(editableFields.at(0), '12'); // Reps
await tester.enterText(editableFields.at(1), '34'); // Weight
await tester.pumpAndSettle();
Log? capturedLog;
@@ -226,13 +229,13 @@ void main() {
// Assert
verify(mockRoutines.addLog(any)).called(1);
expect(capturedLog, isNotNull);
expect(capturedLog!.repetitions, equals(7));
expect(capturedLog!.weight, equals(77));
expect(capturedLog!.repetitions, equals(12));
expect(capturedLog!.weight, equals(34));
final currentSlotPage = gymNotifier.state.getSlotEntryPageByIndex()!;
final currentSlotPage = notifier.state.getSlotEntryPageByIndex()!;
expect(capturedLog!.slotEntryId, equals(currentSlotPage.setConfigData!.slotEntryId));
expect(capturedLog!.routineId, equals(gymNotifier.state.routine.id));
expect(capturedLog!.iteration, equals(gymNotifier.state.iteration));
expect(capturedLog!.routineId, equals(notifier.state.routine.id));
expect(capturedLog!.iteration, equals(notifier.state.iteration));
});
});
}