From 40d9261e34be50d525d08999513d71578a280488 Mon Sep 17 00:00:00 2001 From: Richard Glod Date: Thu, 16 Oct 2025 11:06:28 +0200 Subject: [PATCH 1/6] Pridanie novej funkcionality alebo opravy --- ios/Flutter/ephemeral/flutter_lldb_helper.py | 32 ++++++++++++++++++++ ios/Flutter/ephemeral/flutter_lldbinit | 5 +++ lib/screens/add_exercise_screen.dart | 7 +++-- 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 ios/Flutter/ephemeral/flutter_lldb_helper.py create mode 100644 ios/Flutter/ephemeral/flutter_lldbinit diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 000000000..a88caf99d --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 000000000..e3ba6fbed --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/lib/screens/add_exercise_screen.dart b/lib/screens/add_exercise_screen.dart index 83dd447ed..ff8e72857 100644 --- a/lib/screens/add_exercise_screen.dart +++ b/lib/screens/add_exercise_screen.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; @@ -29,7 +29,10 @@ class AddExerciseScreen extends StatelessWidget { Widget build(BuildContext context) { final profile = context.read().profile; - return profile!.isTrustworthy ? const AddExerciseStepper() : const EmailNotVerified(); + + // return profile!.isTrustworthy ? const AddExerciseStepper() : const EmailNotVerified(); + return const AddExerciseStepper(); + } } From 8b602fe6dd193dfdbb249203f7fe16e44d10ffb7 Mon Sep 17 00:00:00 2001 From: Richard Glod Date: Thu, 16 Oct 2025 13:43:29 +0200 Subject: [PATCH 2/6] Added server side validation to exercise comments --- lib/screens/add_exercise_screen.dart | 254 ++++++++++++------ .../steps/step_3_description.dart | 6 +- .../steps/step_4_translations.dart | 10 +- .../add_exercise_screen_test.dart | 104 +++++++ 4 files changed, 281 insertions(+), 93 deletions(-) create mode 100644 test/widgets/add_exercise/add_exercise_screen_test.dart diff --git a/lib/screens/add_exercise_screen.dart b/lib/screens/add_exercise_screen.dart index ff8e72857..800accc97 100644 --- a/lib/screens/add_exercise_screen.dart +++ b/lib/screens/add_exercise_screen.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; @@ -11,8 +11,8 @@ import 'package:wger/providers/user.dart'; import 'package:wger/screens/exercise_screen.dart'; import 'package:wger/widgets/add_exercise/steps/step_1_basics.dart'; import 'package:wger/widgets/add_exercise/steps/step_2_variations.dart'; -import 'package:wger/widgets/add_exercise/steps/step_3_description.dart'; -import 'package:wger/widgets/add_exercise/steps/step_4_translations.dart'; +import 'package:wger/widgets/add_exercise/steps/step_3_description.dart' as step3; +import 'package:wger/widgets/add_exercise/steps/step_4_translations.dart' as step4; import 'package:wger/widgets/add_exercise/steps/step_5_images.dart'; import 'package:wger/widgets/add_exercise/steps/step_6_overview.dart'; import 'package:wger/widgets/core/app_bar.dart'; @@ -29,10 +29,8 @@ class AddExerciseScreen extends StatelessWidget { Widget build(BuildContext context) { final profile = context.read().profile; - - // return profile!.isTrustworthy ? const AddExerciseStepper() : const EmailNotVerified(); + // return profile!.isTrustworthy ? const AddExerciseStepper() : const EmailNotVerified(); return const AddExerciseStepper(); - } } @@ -49,7 +47,9 @@ class _AddExerciseStepperState extends State { int _currentStep = 0; int lastStepIndex = AddExerciseStepper.STEPS_IN_FORM - 1; bool _isLoading = false; + bool _isValidating = false; Widget errorWidget = const SizedBox.shrink(); + String? _validationError; final List> _keys = [ GlobalKey(), @@ -60,16 +60,67 @@ class _AddExerciseStepperState extends State { GlobalKey(), ]; + Future _validateLanguageOnServer(BuildContext context) async { + final addExerciseProvider = context.read(); + + try { + if (_currentStep == 2) { + await addExerciseProvider.validateLanguage( + addExerciseProvider.descriptionEn ?? '', + 'en', + ); + } + + if (_currentStep == 3 && addExerciseProvider.descriptionTrans != null) { + final languageCode = addExerciseProvider.languageTranslation?.shortName ?? ''; + if (languageCode.isNotEmpty) { + await addExerciseProvider.validateLanguage( + addExerciseProvider.descriptionTrans ?? '', + languageCode, + ); + } + } + + return true; + } on WgerHttpException catch (error) { + if (mounted) { + setState(() { + _validationError = error.toString(); + }); + } + return false; + } catch (error) { + if (mounted) { + setState(() { + _validationError = error.toString(); + }); + } + return false; + } + } + Widget _controlsBuilder(BuildContext context, ControlsDetails details) { return Column( children: [ const SizedBox(height: 10), + + if (_validationError != null && _currentStep != lastStepIndex) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + _validationError!, + style: TextStyle(color: Theme.of(context).colorScheme.error), + textAlign: TextAlign.center, + ), + ), + if (_currentStep == lastStepIndex) errorWidget, + Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ OutlinedButton( - onPressed: details.onStepCancel, + onPressed: _isValidating ? null : details.onStepCancel, child: Text(AppLocalizations.of(context).previous), ), @@ -79,70 +130,112 @@ class _AddExerciseStepperState extends State { onPressed: _isLoading ? null : () async { - setState(() { - _isLoading = true; - errorWidget = const SizedBox.shrink(); - }); - final addExerciseProvider = context.read(); - final exerciseProvider = context.read(); - - Exercise? exercise; - try { - final exerciseId = await addExerciseProvider.postExerciseToServer(); - exercise = await exerciseProvider.fetchAndSetExercise(exerciseId); - } on WgerHttpException catch (error) { - if (context.mounted) { - setState(() { - errorWidget = FormHttpErrorsWidget(error); - }); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - - if (exercise == null || !context.mounted) { - return; - } - - final name = exercise - .getTranslation(Localizations.localeOf(context).languageCode) - .name; - - return showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text(AppLocalizations.of(context).success), - content: Text(AppLocalizations.of(context).cacheWarning), - actions: [ - TextButton( - child: Text(name), - onPressed: () { - Navigator.of(context).pop(); - Navigator.pushReplacementNamed( - context, - ExerciseDetailScreen.routeName, - arguments: exercise, - ); - }, - ), - ], - ); - }, - ); - }, + setState(() { + _isLoading = true; + errorWidget = const SizedBox.shrink(); + }); + final addExerciseProvider = context.read(); + final exerciseProvider = context.read(); + + Exercise? exercise; + try { + final exerciseId = await addExerciseProvider.postExerciseToServer(); + exercise = await exerciseProvider.fetchAndSetExercise(exerciseId); + } on WgerHttpException catch (error) { + if (context.mounted) { + setState(() { + errorWidget = FormHttpErrorsWidget(error); + }); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + + if (exercise == null || !context.mounted) { + return; + } + + final name = exercise + .getTranslation(Localizations.localeOf(context).languageCode) + .name; + + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(AppLocalizations.of(context).success), + content: Text(AppLocalizations.of(context).cacheWarning), + actions: [ + TextButton( + child: Text(name), + onPressed: () { + Navigator.of(context).pop(); + Navigator.pushReplacementNamed( + context, + ExerciseDetailScreen.routeName, + arguments: exercise, + ); + }, + ), + ], + ); + }, + ); + }, child: _isLoading ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator()) : Text(AppLocalizations.of(context).save), ) else ElevatedButton( - onPressed: details.onStepContinue, - child: Text(AppLocalizations.of(context).next), + onPressed: _isValidating + ? null + : () async { + setState(() { + _validationError = null; + }); + + if (!(_keys[_currentStep].currentState?.validate() ?? false)) { + return; + } + + _keys[_currentStep].currentState?.save(); + + if (_currentStep == 2 || _currentStep == 3) { + setState(() { + _isValidating = true; + }); + + final isValid = await _validateLanguageOnServer(context); + + if (mounted) { + setState(() { + _isValidating = false; + }); + } + + if (!isValid) { + return; + } + } + + if (_currentStep != lastStepIndex) { + setState(() { + _currentStep += 1; + }); + } + }, + child: _isValidating + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(AppLocalizations.of(context).next), ), ], ), @@ -167,42 +260,29 @@ class _AddExerciseStepperState extends State { ), Step( title: Text(AppLocalizations.of(context).description), - content: Step3Description(formkey: _keys[2]), + content: step3.Step3Description(formkey: _keys[2]), ), Step( title: Text(AppLocalizations.of(context).translation), - content: Step4Translation(formkey: _keys[3]), + content: step4.Step4Translation(formkey: _keys[3]), ), Step( title: Text(AppLocalizations.of(context).images), content: Step5Images(formkey: _keys[4]), ), - Step(title: Text(AppLocalizations.of(context).overview), content: Step6Overview()), + Step( + title: Text(AppLocalizations.of(context).overview), + content: Step6Overview(), + ), ], currentStep: _currentStep, - onStepContinue: () { - if (_keys[_currentStep].currentState?.validate() ?? false) { - _keys[_currentStep].currentState?.save(); - - if (_currentStep != lastStepIndex) { - setState(() { - _currentStep += 1; - }); - } - } - }, + onStepContinue: null, // Použijeme vlastnú logiku v _controlsBuilder onStepCancel: () => setState(() { if (_currentStep != 0) { _currentStep -= 1; + _validationError = null; // Resetovať chybu pri návrate } }), - /* - onStepTapped: (int index) { - setState(() { - _currentStep = index; - }); - }, - */ ), ); } @@ -259,4 +339,4 @@ class EmailNotVerified extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/widgets/add_exercise/steps/step_3_description.dart b/lib/widgets/add_exercise/steps/step_3_description.dart index 2905df173..6d2f2b2f2 100644 --- a/lib/widgets/add_exercise/steps/step_3_description.dart +++ b/lib/widgets/add_exercise/steps/step_3_description.dart @@ -20,7 +20,9 @@ class Step3Description extends StatelessWidget { child: Column( children: [ AddExerciseTextArea( - onChange: (value) => {}, + onChange: (value) => { + addExerciseProvider.descriptionEn = value + }, title: '${i18n.description}*', helperText: i18n.enterTextInLanguage, isMultiline: true, @@ -31,4 +33,4 @@ class Step3Description extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/widgets/add_exercise/steps/step_4_translations.dart b/lib/widgets/add_exercise/steps/step_4_translations.dart index d8f4a778f..01d8b30c8 100644 --- a/lib/widgets/add_exercise/steps/step_4_translations.dart +++ b/lib/widgets/add_exercise/steps/step_4_translations.dart @@ -83,17 +83,19 @@ class _Step4TranslationState extends State { return null; }, onSaved: (String? alternateName) => - addExerciseProvider.alternateNamesTrans = alternateName!.split('\n'), + addExerciseProvider.alternateNamesTrans = alternateName!.split('\n'), ), Consumer( builder: (ctx, provider, __) => AddExerciseTextArea( - onChange: (value) => {}, + onChange: (value) { + addExerciseProvider.descriptionTrans = value; + }, title: '${i18n.description}*', helperText: i18n.enterTextInLanguage, isMultiline: true, validator: (name) => validateExerciseDescription(name, context), onSaved: (String? description) => - addExerciseProvider.descriptionTrans = description!, + addExerciseProvider.descriptionTrans = description!, ), ), ], @@ -102,4 +104,4 @@ class _Step4TranslationState extends State { ), ); } -} +} \ No newline at end of file diff --git a/test/widgets/add_exercise/add_exercise_screen_test.dart b/test/widgets/add_exercise/add_exercise_screen_test.dart new file mode 100644 index 000000000..6831ef826 --- /dev/null +++ b/test/widgets/add_exercise/add_exercise_screen_test.dart @@ -0,0 +1,104 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; +import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/providers/add_exercise.dart'; + +@GenerateMocks([AddExerciseProvider]) +import 'add_exercise_translation_test.mocks.dart'; + +void main() { + late MockAddExerciseProvider mockAddExerciseProvider; + + setUp(() { + mockAddExerciseProvider = MockAddExerciseProvider(); + }); + + group('Language validation - correct language', () { + test('English text validated as English', () async { + const text = + 'Push-ups are a basic exercise to strengthen the upper body, especially the chest, shoulders, and triceps.'; + when(mockAddExerciseProvider.validateLanguage(text, 'en')) + .thenAnswer((_) async => true); + + final result = await mockAddExerciseProvider.validateLanguage(text, 'en'); + + expect(result, isTrue); + verify(mockAddExerciseProvider.validateLanguage(text, 'en')).called(1); + }); + + test('German text validated as German', () async { + const text = + 'Liegestütze sind eine grundlegende Übung zur Stärkung der oberen Körpermuskulatur.'; + when(mockAddExerciseProvider.validateLanguage(text, 'de')) + .thenAnswer((_) async => true); + + final result = await mockAddExerciseProvider.validateLanguage(text, 'de'); + + expect(result, isTrue); + verify(mockAddExerciseProvider.validateLanguage(text, 'de')).called(1); + }); + + test('Slovak text validated as Slovak', () async { + const text = + 'Kliky sú základným cvikom na posilnenie hornej časti tela a tricepsov.'; + when(mockAddExerciseProvider.validateLanguage(text, 'sk')) + .thenAnswer((_) async => true); + + final result = await mockAddExerciseProvider.validateLanguage(text, 'sk'); + + expect(result, isTrue); + verify(mockAddExerciseProvider.validateLanguage(text, 'sk')).called(1); + }); + }); + + group('Language validation - wrong language', () { + test('Slovak text validated as English - should throw exception', () async { + const text = + 'Kliky sú základným cvikom na posilnenie hornej časti tela.'; + when(mockAddExerciseProvider.validateLanguage(text, 'en')) + .thenThrow(WgerHttpException({ + 'text': ['The detected language does not match the expected language.'] + })); + + expect( + () => mockAddExerciseProvider.validateLanguage(text, 'en'), + throwsA(isA()), + ); + + verify(mockAddExerciseProvider.validateLanguage(text, 'en')).called(1); + }); + + test('English text validated as German - should throw exception', () async { + const text = + 'Push-ups are a common exercise for strengthening the upper body.'; + when(mockAddExerciseProvider.validateLanguage(text, 'de')) + .thenThrow(WgerHttpException({ + 'text': ['The detected language does not match the expected language.'] + })); + + expect( + () => mockAddExerciseProvider.validateLanguage(text, 'de'), + throwsA(isA()), + ); + + verify(mockAddExerciseProvider.validateLanguage(text, 'de')).called(1); + }); + + test('German text validated as Slovak - should throw exception', () async { + const text = + 'Liegestütze sind eine klassische Übung zur Verbesserung der Kraft.'; + when(mockAddExerciseProvider.validateLanguage(text, 'sk')) + .thenThrow(WgerHttpException({ + 'text': ['The detected language does not match the expected language.'] + })); + + expect( + () => mockAddExerciseProvider.validateLanguage(text, 'sk'), + throwsA(isA()), + ); + + verify(mockAddExerciseProvider.validateLanguage(text, 'sk')).called(1); + }); + }); +} From 087c123e0d045dd627ab76cf6faaa88a8c721f65 Mon Sep 17 00:00:00 2001 From: Richard Glod Date: Thu, 16 Oct 2025 13:59:57 +0200 Subject: [PATCH 3/6] Added server side validation to exercise comments --- .../add_exercise_translation_test.mocks.dart | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 test/widgets/add_exercise/add_exercise_translation_test.mocks.dart diff --git a/test/widgets/add_exercise/add_exercise_translation_test.mocks.dart b/test/widgets/add_exercise/add_exercise_translation_test.mocks.dart new file mode 100644 index 000000000..9bb98461b --- /dev/null +++ b/test/widgets/add_exercise/add_exercise_translation_test.mocks.dart @@ -0,0 +1,320 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in wger/test/widgets/add_exercise/add_exercise_screen_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i12; +import 'dart:ui' as _i13; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i6; +import 'package:wger/models/exercises/category.dart' as _i11; +import 'package:wger/models/exercises/equipment.dart' as _i7; +import 'package:wger/models/exercises/exercise_submission.dart' as _i9; +import 'package:wger/models/exercises/exercise_submission_images.dart' as _i5; +import 'package:wger/models/exercises/language.dart' as _i10; +import 'package:wger/models/exercises/muscle.dart' as _i8; +import 'package:wger/models/exercises/variation.dart' as _i3; +import 'package:wger/providers/add_exercise.dart' as _i4; +import 'package:wger/providers/base_provider.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 +// ignore_for_file: invalid_use_of_internal_member + +class _FakeWgerBaseProvider_0 extends _i1.SmartFake + implements _i2.WgerBaseProvider { + _FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeVariation_1 extends _i1.SmartFake implements _i3.Variation { + _FakeVariation_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [AddExerciseProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAddExerciseProvider extends _i1.Mock + implements _i4.AddExerciseProvider { + MockAddExerciseProvider() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WgerBaseProvider get baseProvider => + (super.noSuchMethod( + Invocation.getter(#baseProvider), + returnValue: _FakeWgerBaseProvider_0( + this, + Invocation.getter(#baseProvider), + ), + ) + as _i2.WgerBaseProvider); + + @override + List<_i5.ExerciseSubmissionImage> get exerciseImages => + (super.noSuchMethod( + Invocation.getter(#exerciseImages), + returnValue: <_i5.ExerciseSubmissionImage>[], + ) + as List<_i5.ExerciseSubmissionImage>); + + @override + String get author => + (super.noSuchMethod( + Invocation.getter(#author), + returnValue: _i6.dummyValue( + this, + Invocation.getter(#author), + ), + ) + as String); + + @override + List get alternateNamesEn => + (super.noSuchMethod( + Invocation.getter(#alternateNamesEn), + returnValue: [], + ) + as List); + + @override + List get alternateNamesTrans => + (super.noSuchMethod( + Invocation.getter(#alternateNamesTrans), + returnValue: [], + ) + as List); + + @override + List<_i7.Equipment> get equipment => + (super.noSuchMethod( + Invocation.getter(#equipment), + returnValue: <_i7.Equipment>[], + ) + as List<_i7.Equipment>); + + @override + bool get newVariation => + (super.noSuchMethod(Invocation.getter(#newVariation), returnValue: false) + as bool); + + @override + _i3.Variation get variation => + (super.noSuchMethod( + Invocation.getter(#variation), + returnValue: _FakeVariation_1(this, Invocation.getter(#variation)), + ) + as _i3.Variation); + + @override + List<_i8.Muscle> get primaryMuscles => + (super.noSuchMethod( + Invocation.getter(#primaryMuscles), + returnValue: <_i8.Muscle>[], + ) + as List<_i8.Muscle>); + + @override + List<_i8.Muscle> get secondaryMuscles => + (super.noSuchMethod( + Invocation.getter(#secondaryMuscles), + returnValue: <_i8.Muscle>[], + ) + as List<_i8.Muscle>); + + @override + _i9.ExerciseSubmissionApi get exerciseApiObject => + (super.noSuchMethod( + Invocation.getter(#exerciseApiObject), + returnValue: _i6.dummyValue<_i9.ExerciseSubmissionApi>( + this, + Invocation.getter(#exerciseApiObject), + ), + ) + as _i9.ExerciseSubmissionApi); + + @override + set author(String? value) => super.noSuchMethod( + Invocation.setter(#author, value), + returnValueForMissingStub: null, + ); + + @override + set exerciseNameEn(String? value) => super.noSuchMethod( + Invocation.setter(#exerciseNameEn, value), + returnValueForMissingStub: null, + ); + + @override + set exerciseNameTrans(String? value) => super.noSuchMethod( + Invocation.setter(#exerciseNameTrans, value), + returnValueForMissingStub: null, + ); + + @override + set descriptionEn(String? value) => super.noSuchMethod( + Invocation.setter(#descriptionEn, value), + returnValueForMissingStub: null, + ); + + @override + set descriptionTrans(String? value) => super.noSuchMethod( + Invocation.setter(#descriptionTrans, value), + returnValueForMissingStub: null, + ); + + @override + set languageEn(_i10.Language? value) => super.noSuchMethod( + Invocation.setter(#languageEn, value), + returnValueForMissingStub: null, + ); + + @override + set languageTranslation(_i10.Language? value) => super.noSuchMethod( + Invocation.setter(#languageTranslation, value), + returnValueForMissingStub: null, + ); + + @override + set alternateNamesEn(List? value) => super.noSuchMethod( + Invocation.setter(#alternateNamesEn, value), + returnValueForMissingStub: null, + ); + + @override + set alternateNamesTrans(List? value) => super.noSuchMethod( + Invocation.setter(#alternateNamesTrans, value), + returnValueForMissingStub: null, + ); + + @override + set category(_i11.ExerciseCategory? value) => super.noSuchMethod( + Invocation.setter(#category, value), + returnValueForMissingStub: null, + ); + + @override + set equipment(List<_i7.Equipment>? equipment) => super.noSuchMethod( + Invocation.setter(#equipment, equipment), + returnValueForMissingStub: null, + ); + + @override + set variationConnectToExercise(int? value) => super.noSuchMethod( + Invocation.setter(#variationConnectToExercise, value), + returnValueForMissingStub: null, + ); + + @override + set variationId(int? variation) => super.noSuchMethod( + Invocation.setter(#variationId, variation), + returnValueForMissingStub: null, + ); + + @override + set primaryMuscles(List<_i8.Muscle>? muscles) => super.noSuchMethod( + Invocation.setter(#primaryMuscles, muscles), + returnValueForMissingStub: null, + ); + + @override + set secondaryMuscles(List<_i8.Muscle>? muscles) => super.noSuchMethod( + Invocation.setter(#secondaryMuscles, muscles), + returnValueForMissingStub: null, + ); + + @override + bool get hasListeners => + (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) + as bool); + + @override + void clear() => super.noSuchMethod( + Invocation.method(#clear, []), + returnValueForMissingStub: null, + ); + + @override + void addExerciseImages(List<_i5.ExerciseSubmissionImage>? images) => + super.noSuchMethod( + Invocation.method(#addExerciseImages, [images]), + returnValueForMissingStub: null, + ); + + @override + void removeImage(String? path) => super.noSuchMethod( + Invocation.method(#removeImage, [path]), + returnValueForMissingStub: null, + ); + + @override + _i12.Future postExerciseToServer() => + (super.noSuchMethod( + Invocation.method(#postExerciseToServer, []), + returnValue: _i12.Future.value(0), + ) + as _i12.Future); + + @override + _i12.Future addExerciseSubmission() => + (super.noSuchMethod( + Invocation.method(#addExerciseSubmission, []), + returnValue: _i12.Future.value(0), + ) + as _i12.Future); + + @override + _i12.Future addImages(int? exerciseId) => + (super.noSuchMethod( + Invocation.method(#addImages, [exerciseId]), + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) + as _i12.Future); + + @override + _i12.Future validateLanguage(String? input, String? languageCode) => + (super.noSuchMethod( + Invocation.method(#validateLanguage, [input, languageCode]), + returnValue: _i12.Future.value(false), + ) + as _i12.Future); + + @override + void addListener(_i13.VoidCallback? listener) => super.noSuchMethod( + Invocation.method(#addListener, [listener]), + returnValueForMissingStub: null, + ); + + @override + void removeListener(_i13.VoidCallback? listener) => super.noSuchMethod( + Invocation.method(#removeListener, [listener]), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method(#dispose, []), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method(#notifyListeners, []), + returnValueForMissingStub: null, + ); +} From cc1c6cb898592e3e0a0df7578f08d192d3aaa7fd Mon Sep 17 00:00:00 2001 From: Richard Glod Date: Thu, 16 Oct 2025 14:08:24 +0200 Subject: [PATCH 4/6] Added server side validation to exercise comments --- lib/screens/add_exercise_screen.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/screens/add_exercise_screen.dart b/lib/screens/add_exercise_screen.dart index 800accc97..b91e30002 100644 --- a/lib/screens/add_exercise_screen.dart +++ b/lib/screens/add_exercise_screen.dart @@ -29,8 +29,7 @@ class AddExerciseScreen extends StatelessWidget { Widget build(BuildContext context) { final profile = context.read().profile; - // return profile!.isTrustworthy ? const AddExerciseStepper() : const EmailNotVerified(); - return const AddExerciseStepper(); + return profile!.isTrustworthy ? const AddExerciseStepper() : const EmailNotVerified(); } } From 1e563f31d8f22e07b6094164c89bb92051fd666c Mon Sep 17 00:00:00 2001 From: Richard Glod Date: Fri, 17 Oct 2025 18:31:30 +0200 Subject: [PATCH 5/6] New test for validateLanguage --- .../add_exercise_screen_test.dart | 195 ++++++++++++------ ...rt => add_exercise_screen_test.mocks.dart} | 0 2 files changed, 137 insertions(+), 58 deletions(-) rename test/widgets/add_exercise/{add_exercise_translation_test.mocks.dart => add_exercise_screen_test.mocks.dart} (100%) diff --git a/test/widgets/add_exercise/add_exercise_screen_test.dart b/test/widgets/add_exercise/add_exercise_screen_test.dart index 6831ef826..a2d1f14b3 100644 --- a/test/widgets/add_exercise/add_exercise_screen_test.dart +++ b/test/widgets/add_exercise/add_exercise_screen_test.dart @@ -5,100 +5,179 @@ import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/providers/add_exercise.dart'; @GenerateMocks([AddExerciseProvider]) -import 'add_exercise_translation_test.mocks.dart'; +import 'add_exercise_screen_test.mocks.dart'; void main() { - late MockAddExerciseProvider mockAddExerciseProvider; + late MockAddExerciseProvider mockProvider; setUp(() { - mockAddExerciseProvider = MockAddExerciseProvider(); + mockProvider = MockAddExerciseProvider(); }); - group('Language validation - correct language', () { - test('English text validated as English', () async { - const text = - 'Push-ups are a basic exercise to strengthen the upper body, especially the chest, shoulders, and triceps.'; - when(mockAddExerciseProvider.validateLanguage(text, 'en')) + group('validateLanguage - English description (step 2)', () { + test('calls validateLanguage with English text and "en" code', () async { + + const descriptionText = 'Push-ups are a basic exercise for upper body strength.'; + when(mockProvider.validateLanguage(descriptionText, 'en')) .thenAnswer((_) async => true); - final result = await mockAddExerciseProvider.validateLanguage(text, 'en'); + await mockProvider.validateLanguage(descriptionText, 'en'); - expect(result, isTrue); - verify(mockAddExerciseProvider.validateLanguage(text, 'en')).called(1); + verify(mockProvider.validateLanguage(descriptionText, 'en')).called(1); }); - test('German text validated as German', () async { - const text = - 'Liegestütze sind eine grundlegende Übung zur Stärkung der oberen Körpermuskulatur.'; - when(mockAddExerciseProvider.validateLanguage(text, 'de')) + test('throws WgerHttpException when validation fails', () async { + const descriptionText = 'Deutscher Text in English field'; + final exception = WgerHttpException({ + 'text': ['The detected language does not match the expected language.'] + }); + + when(mockProvider.validateLanguage(descriptionText, 'en')) + .thenThrow(exception); + + expect( + () => mockProvider.validateLanguage(descriptionText, 'en'), + throwsA(isA()), + ); + }); + + test('validates with correct language code "en"', () async { + const text = 'This is an English description.'; + when(mockProvider.validateLanguage(text, 'en')) .thenAnswer((_) async => true); - final result = await mockAddExerciseProvider.validateLanguage(text, 'de'); + final result = await mockProvider.validateLanguage(text, 'en'); expect(result, isTrue); - verify(mockAddExerciseProvider.validateLanguage(text, 'de')).called(1); + verify(mockProvider.validateLanguage(text, 'en')).called(1); }); + }); - test('Slovak text validated as Slovak', () async { - const text = - 'Kliky sú základným cvikom na posilnenie hornej časti tela a tricepsov.'; - when(mockAddExerciseProvider.validateLanguage(text, 'sk')) + group('validateLanguage - Translation (step 3)', () { + test('calls validateLanguage with German text and "de" code', () async { + const translationText = 'Liegestütze sind eine Grundübung für den Oberkörper.'; + when(mockProvider.validateLanguage(translationText, 'de')) .thenAnswer((_) async => true); - final result = await mockAddExerciseProvider.validateLanguage(text, 'sk'); + await mockProvider.validateLanguage(translationText, 'de'); - expect(result, isTrue); - verify(mockAddExerciseProvider.validateLanguage(text, 'sk')).called(1); + verify(mockProvider.validateLanguage(translationText, 'de')).called(1); + }); + + test('calls validateLanguage with Slovak text and "sk" code', () async { + const translationText = 'Kliky sú základné cvičenie pre hornú časť tela.'; + when(mockProvider.validateLanguage(translationText, 'sk')) + .thenAnswer((_) async => true); + + await mockProvider.validateLanguage(translationText, 'sk'); + + verify(mockProvider.validateLanguage(translationText, 'sk')).called(1); + }); + + test('uses correct language code from Language model', () async { + const text = 'Deutscher Text'; + const languageCode = 'de'; + + when(mockProvider.validateLanguage(text, languageCode)) + .thenAnswer((_) async => true); + + await mockProvider.validateLanguage(text, languageCode); + + verify(mockProvider.validateLanguage(text, languageCode)).called(1); + verifyNever(mockProvider.validateLanguage(text, 'en')); }); }); - group('Language validation - wrong language', () { - test('Slovak text validated as English - should throw exception', () async { - const text = - 'Kliky sú základným cvikom na posilnenie hornej časti tela.'; - when(mockAddExerciseProvider.validateLanguage(text, 'en')) - .thenThrow(WgerHttpException({ - 'text': ['The detected language does not match the expected language.'] - })); + group('validateLanguage - Edge cases', () { + test('handles empty text', () async { + when(mockProvider.validateLanguage('', 'en')) + .thenAnswer((_) async => true); - expect( - () => mockAddExerciseProvider.validateLanguage(text, 'en'), - throwsA(isA()), - ); + await mockProvider.validateLanguage('', 'en'); - verify(mockAddExerciseProvider.validateLanguage(text, 'en')).called(1); + verify(mockProvider.validateLanguage('', 'en')).called(1); }); - test('English text validated as German - should throw exception', () async { - const text = - 'Push-ups are a common exercise for strengthening the upper body.'; - when(mockAddExerciseProvider.validateLanguage(text, 'de')) - .thenThrow(WgerHttpException({ - 'text': ['The detected language does not match the expected language.'] - })); + test('handles text with special characters', () async { + const text = 'Übung für Körper & Geist (100% Erfolg)'; + when(mockProvider.validateLanguage(text, 'de')) + .thenAnswer((_) async => true); - expect( - () => mockAddExerciseProvider.validateLanguage(text, 'de'), - throwsA(isA()), - ); + await mockProvider.validateLanguage(text, 'de'); - verify(mockAddExerciseProvider.validateLanguage(text, 'de')).called(1); + verify(mockProvider.validateLanguage(text, 'de')).called(1); }); - test('German text validated as Slovak - should throw exception', () async { - const text = - 'Liegestütze sind eine klassische Übung zur Verbesserung der Kraft.'; - when(mockAddExerciseProvider.validateLanguage(text, 'sk')) - .thenThrow(WgerHttpException({ + test('handles long text', () async { + const longText = ''' +Push-ups are a fundamental bodyweight exercise that strengthens the upper body, +especially the chest, shoulders, and triceps. The movement involves supporting +the body on hands and feet while keeping it straight. It improves muscular +strength, core stability, endurance, and overall physical fitness. +'''; + when(mockProvider.validateLanguage(longText, 'en')) + .thenAnswer((_) async => true); + + await mockProvider.validateLanguage(longText, 'en'); + + verify(mockProvider.validateLanguage(longText, 'en')).called(1); + }); + }); + + group('validateLanguage - Error responses', () { + test('WgerHttpException is thrown for invalid language', () { + final exception = WgerHttpException({ 'text': ['The detected language does not match the expected language.'] - })); + }); + + expect(exception, isA()); + }); + + test('throws WgerHttpException on validation failure', () async { + const text = 'Wrong language text'; + final exception = WgerHttpException({ + 'text': ['Language mismatch error'] + }); + + when(mockProvider.validateLanguage(text, 'en')).thenThrow(exception); expect( - () => mockAddExerciseProvider.validateLanguage(text, 'sk'), + () => mockProvider.validateLanguage(text, 'en'), throwsA(isA()), ); + }); + }); + + group('validateLanguage - Multiple language codes', () { + test('validates different language codes correctly', () async { + when(mockProvider.validateLanguage('English text', 'en')) + .thenAnswer((_) async => true); + await mockProvider.validateLanguage('English text', 'en'); + verify(mockProvider.validateLanguage('English text', 'en')).called(1); + + when(mockProvider.validateLanguage('Deutscher Text', 'de')) + .thenAnswer((_) async => true); + await mockProvider.validateLanguage('Deutscher Text', 'de'); + verify(mockProvider.validateLanguage('Deutscher Text', 'de')).called(1); + + when(mockProvider.validateLanguage('Slovenský text', 'sk')) + .thenAnswer((_) async => true); + await mockProvider.validateLanguage('Slovenský text', 'sk'); + verify(mockProvider.validateLanguage('Slovenský text', 'sk')).called(1); + }); + + test('does not cross-validate wrong language combinations', () async { + when(mockProvider.validateLanguage('Deutscher Text', 'de')) + .thenAnswer((_) async => true); + + await mockProvider.validateLanguage('Deutscher Text', 'de'); - verify(mockAddExerciseProvider.validateLanguage(text, 'sk')).called(1); + verify(mockProvider.validateLanguage('Deutscher Text', 'de')).called(1); + + verifyNever(mockProvider.validateLanguage('Deutscher Text', 'en')); + verifyNever(mockProvider.validateLanguage('Deutscher Text', 'sk')); }); }); -} + + +} \ No newline at end of file diff --git a/test/widgets/add_exercise/add_exercise_translation_test.mocks.dart b/test/widgets/add_exercise/add_exercise_screen_test.mocks.dart similarity index 100% rename from test/widgets/add_exercise/add_exercise_translation_test.mocks.dart rename to test/widgets/add_exercise/add_exercise_screen_test.mocks.dart From b3a5e15c6bff441b34186a5cde962799705265e9 Mon Sep 17 00:00:00 2001 From: Richard Glod Date: Fri, 17 Oct 2025 18:32:19 +0200 Subject: [PATCH 6/6] how validation errors using FormHttpErrorsWidget --- lib/screens/add_exercise_screen.dart | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/screens/add_exercise_screen.dart b/lib/screens/add_exercise_screen.dart index b91e30002..52b9382a8 100644 --- a/lib/screens/add_exercise_screen.dart +++ b/lib/screens/add_exercise_screen.dart @@ -48,7 +48,7 @@ class _AddExerciseStepperState extends State { bool _isLoading = false; bool _isValidating = false; Widget errorWidget = const SizedBox.shrink(); - String? _validationError; + WgerHttpException? _validationError; final List> _keys = [ GlobalKey(), @@ -84,14 +84,14 @@ class _AddExerciseStepperState extends State { } on WgerHttpException catch (error) { if (mounted) { setState(() { - _validationError = error.toString(); + _validationError = error; }); } return false; } catch (error) { if (mounted) { setState(() { - _validationError = error.toString(); + _validationError = WgerHttpException({'error': [error.toString()]}); }); } return false; @@ -106,11 +106,7 @@ class _AddExerciseStepperState extends State { if (_validationError != null && _currentStep != lastStepIndex) Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - _validationError!, - style: TextStyle(color: Theme.of(context).colorScheme.error), - textAlign: TextAlign.center, - ), + child: FormHttpErrorsWidget(_validationError!), ), if (_currentStep == lastStepIndex) errorWidget,