diff --git a/designer_v2/lib/features/design/shared/questionnaire/question/question_form_data.dart b/designer_v2/lib/features/design/shared/questionnaire/question/question_form_data.dart index 065a8b5c2..bf4001d9c 100644 --- a/designer_v2/lib/features/design/shared/questionnaire/question/question_form_data.dart +++ b/designer_v2/lib/features/design/shared/questionnaire/question/question_form_data.dart @@ -94,7 +94,7 @@ abstract class QuestionFormData implements IFormData { final QuestionConditional? conditional; /// Mapping from response option => qualifying/disqualifying - late final Map responseOptionsValidity; + Map responseOptionsValidity = {}; List get responseOptions; // subclass responsibility diff --git a/designer_v2/lib/features/forms/form_view_model_collection_actions.dart b/designer_v2/lib/features/forms/form_view_model_collection_actions.dart index 60824fd4c..ad2ec9189 100644 --- a/designer_v2/lib/features/forms/form_view_model_collection_actions.dart +++ b/designer_v2/lib/features/forms/form_view_model_collection_actions.dart @@ -35,7 +35,7 @@ extension FormViewModelCollectionActions< final duplicateFormViewModel = formViewModel.createDuplicate() as T; add(duplicateFormViewModel); - formViewModel.save(); + duplicateFormViewModel.save(); }, isAvailable: !isReadOnly, ), diff --git a/designer_v2/lib/localization/app_translation.dart b/designer_v2/lib/localization/app_translation.dart index 83f3b6f72..c9a51a8c3 100644 --- a/designer_v2/lib/localization/app_translation.dart +++ b/designer_v2/lib/localization/app_translation.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:studyu_designer_v2/localization/app_localizations.dart'; import 'package:studyu_designer_v2/localization/locale_providers.dart'; @@ -11,6 +12,11 @@ class AppTranslation { // Loads the currently selected locale and sets the localization _tr = lookupAppLocalizations(ref.watch(localeProvider)); } + + @visibleForTesting + static void setForTesting(AppLocalizations localizations) { + _tr = localizations; + } } typedef LocalizedStringResolver = String Function(); diff --git a/designer_v2/test/features/forms/form_view_model_collection_actions_test.dart b/designer_v2/test/features/forms/form_view_model_collection_actions_test.dart new file mode 100644 index 000000000..3fae1fa15 --- /dev/null +++ b/designer_v2/test/features/forms/form_view_model_collection_actions_test.dart @@ -0,0 +1,107 @@ +import 'package:reactive_forms/reactive_forms.dart'; +import 'package:studyu_designer_v2/features/design/shared/questionnaire/question/question_form_data.dart'; +import 'package:studyu_designer_v2/features/design/shared/questionnaire/question/types/question_type.dart'; +import 'package:studyu_designer_v2/features/forms/form_data.dart'; +import 'package:studyu_designer_v2/features/forms/form_view_model.dart'; +import 'package:studyu_designer_v2/features/forms/form_view_model_collection.dart'; +import 'package:studyu_designer_v2/features/forms/form_view_model_collection_actions.dart'; +import 'package:studyu_designer_v2/localization/app_localizations_en.dart'; +import 'package:studyu_designer_v2/localization/app_translation.dart'; +import 'package:studyu_designer_v2/utils/model_action.dart'; +import 'package:test/test.dart'; + +class _TestFormData implements IFormData { + _TestFormData({required this.id, required this.value}); + + @override + final String id; + final String value; + + @override + _TestFormData copy() => _TestFormData(id: '${id}_copy', value: value); +} + +class _TestFormViewModel extends ManagedFormViewModel<_TestFormData> { + _TestFormViewModel({super.formData, this.duplicate}); + + final _TestFormViewModel? duplicate; + final FormControl valueControl = FormControl(value: ''); + int saveCount = 0; + + @override + late final FormGroup form = FormGroup({'value': valueControl}); + + @override + Map get titles => {}; + + @override + void setControlsFrom(_TestFormData data) { + valueControl.value = data.value; + } + + @override + _TestFormData buildFormData() { + return _TestFormData(id: formData?.id ?? 'new', value: valueControl.value!); + } + + @override + _TestFormViewModel createDuplicate() { + return duplicate ?? _TestFormViewModel(formData: formData?.copy()); + } + + @override + Future save() { + saveCount += 1; + return super.save(); + } +} + +void main() { + setUpAll(() { + AppTranslation.setForTesting(AppLocalizationsEn()); + }); + + test('duplicate action saves the duplicated view model', () async { + final formArray = FormArray([]); + final duplicate = _TestFormViewModel( + formData: _TestFormData(id: 'duplicate', value: 'Question copy'), + ); + final original = _TestFormViewModel( + formData: _TestFormData(id: 'original', value: 'Question'), + duplicate: duplicate, + ); + final collection = + FormViewModelCollection<_TestFormViewModel, _TestFormData>([ + original, + ], formArray); + formArray.add(original.form); + + final duplicateAction = collection + .availableActions(original) + .singleWhere((action) => action.type == ModelActionType.duplicate); + + duplicateAction.onExecute(); + await Future.delayed(Duration.zero); + + expect(collection.formViewModels, [original, duplicate]); + expect(original.saveCount, 0); + expect(duplicate.saveCount, 1); + }); + + test( + 'question data can be copied before response validity is initialized', + () { + final data = BoolQuestionFormData( + questionId: 'question-1', + questionText: 'Temporary duplicate test', + questionType: SurveyQuestionType.bool, + ); + + final duplicate = data.copy(); + + expect(duplicate.questionId, isNot(data.questionId)); + expect(duplicate.questionText, 'Temporary duplicate test (Copy)'); + expect(duplicate.responseOptionsValidity, isEmpty); + }, + ); +}