diff --git a/app/lib/grades/grades_service/grades_service.dart b/app/lib/grades/grades_service/grades_service.dart index 9638f430d..023409151 100644 --- a/app/lib/grades/grades_service/grades_service.dart +++ b/app/lib/grades/grades_service/grades_service.dart @@ -180,6 +180,11 @@ class TermRef { void removeGradeTypeWeight(GradeTypeId gradeType) { _service.removeGradeTypeWeightForTerm(termId: id, gradeType: gradeType); } + + void changeWeightDisplayType(WeightDisplayType weightDisplayType) { + _service.changeWeightDisplayTypeForTerm( + termId: id, weightDisplayType: weightDisplayType); + } } class TermSubjectRef { @@ -552,6 +557,7 @@ class TermResult extends Equatable { final String name; final GradeType finalGradeType; final IMap gradeTypeWeightings; + final WeightDisplayType weightDisplayType; SubjectResult subject(SubjectId id) { final subject = subjects.firstWhere((element) => element.id == id); @@ -567,6 +573,7 @@ class TermResult extends Equatable { required this.isActiveTerm, required this.finalGradeType, required this.gradeTypeWeightings, + required this.weightDisplayType, }); @override @@ -579,6 +586,7 @@ class TermResult extends Equatable { name, finalGradeType, gradeTypeWeightings, + weightDisplayType, ]; } @@ -663,6 +671,8 @@ extension on GradeInput { } } +enum WeightDisplayType { percent, factor } + enum WeightType { perGrade, perGradeType, diff --git a/app/lib/grades/grades_service/src/grades_repository.dart b/app/lib/grades/grades_service/src/grades_repository.dart index 72c7ffed6..7f809dc94 100644 --- a/app/lib/grades/grades_service/src/grades_repository.dart +++ b/app/lib/grades/grades_service/src/grades_repository.dart @@ -287,6 +287,7 @@ class FirestoreGradesStateRepository extends GradesStateRepository { createdOn: dto.createdOn?.toDate(), gradingSystem: dto.gradingSystem.toGradingSystemModel(), isActiveTerm: data['currentTerm'] == dto.id, + weightDisplayType: dto.weightDisplayType, subjects: termSubjects .where((subject) => subject.termId.value == dto.id) .toIList(), @@ -436,6 +437,7 @@ class TermDto { final Timestamp? createdOn; final GradingSystem gradingSystem; final Map<_SubjectId, WeightDto> subjectWeights; + final WeightDisplayType weightDisplayType; final Map<_GradeTypeId, WeightDto> gradeTypeWeights; final List subjects; final _GradeTypeId finalGradeTypeId; @@ -446,6 +448,7 @@ class TermDto { required this.createdOn, required this.gradingSystem, required this.subjectWeights, + required this.weightDisplayType, required this.gradeTypeWeights, required this.finalGradeTypeId, required this.subjects, @@ -459,6 +462,7 @@ class TermDto { finalGradeTypeId: term.finalGradeType.value, gradingSystem: term.gradingSystem.spec.gradingSystem, subjects: term.subjects.map(TermSubjectDto.fromSubject).toList(), + weightDisplayType: term.weightDisplayType, subjectWeights: Map.fromEntries(term.subjects.map((subject) => MapEntry(subject.id.value, subject.weightingForTermGrade.toDto()))), gradeTypeWeights: term.gradeTypeWeightings @@ -475,6 +479,9 @@ class TermDto { gradingSystem: GradingSystem.values.byName(data['gradingSystem'] as String), subjectWeights: data['subjectWeights'].toWeightsDtoMap(), + weightDisplayType: data['weightDisplayType'] is String + ? WeightDisplayType.values.byName(data['weightDisplayType'] as String) + : WeightDisplayType.factor, gradeTypeWeights: data['gradeTypeWeights'].toWeightsDtoMap(), subjects: (data['subjects'] as Map) .mapTo((_, sub) => TermSubjectDto.fromData(sub)) @@ -491,6 +498,7 @@ class TermDto { if (createdOn == null) 'createdOn': FieldValue.serverTimestamp(), 'gradingSystem': gradingSystem.name, 'subjectWeights': subjectWeights.toWeightDataMap(), + 'weightDisplayType': weightDisplayType.name, 'gradeTypeWeights': gradeTypeWeights.toWeightDataMap(), 'subjects': subjects .map((subject) => MapEntry(subject.id, subject.toData())) diff --git a/app/lib/grades/grades_service/src/grades_service_internal.dart b/app/lib/grades/grades_service/src/grades_service_internal.dart index d8f160889..1c842f58a 100644 --- a/app/lib/grades/grades_service/src/grades_service_internal.dart +++ b/app/lib/grades/grades_service/src/grades_service_internal.dart @@ -64,6 +64,7 @@ class _GradesServiceInternal { ? term.gradingSystem.toGradeResult(term.tryGetTermGrade()!) : null, gradeTypeWeightings: term.gradeTypeWeightings, + weightDisplayType: term.weightDisplayType, subjects: term.subjects .map( (subject) => SubjectResult( @@ -330,6 +331,12 @@ class _GradesServiceInternal { _updateTerm(newTerm); } + void changeWeightDisplayTypeForTerm( + {required TermId termId, required WeightDisplayType weightDisplayType}) { + final newTerm = _term(termId).changeWeightDisplayType(weightDisplayType); + _updateTerm(newTerm); + } + void changeGradeTypeWeightForTerm( {required TermId termId, required GradeTypeId gradeType, diff --git a/app/lib/grades/grades_service/src/term.dart b/app/lib/grades/grades_service/src/term.dart index 3d49a2b08..52a4614ea 100644 --- a/app/lib/grades/grades_service/src/term.dart +++ b/app/lib/grades/grades_service/src/term.dart @@ -20,6 +20,7 @@ class TermModel extends Equatable { final GradeTypeId finalGradeType; final bool isActiveTerm; final String name; + final WeightDisplayType weightDisplayType; @override List get props => [ @@ -27,6 +28,7 @@ class TermModel extends Equatable { createdOn, subjects, gradeTypeWeightings, + weightDisplayType, gradingSystem, finalGradeType, isActiveTerm, @@ -42,12 +44,14 @@ class TermModel extends Equatable { this.createdOn, this.subjects = const IListConst([]), this.gradeTypeWeightings = const IMapConst({}), + this.weightDisplayType = WeightDisplayType.factor, }); const TermModel.internal( this.id, this.subjects, this.gradeTypeWeightings, + this.weightDisplayType, this.finalGradeType, this.isActiveTerm, this.name, @@ -93,6 +97,7 @@ class TermModel extends Equatable { TermId? id, IList? subjects, IMap? gradeTypeWeightings, + WeightDisplayType? weightDisplayType, GradeTypeId? finalGradeType, bool? isActiveTerm, String? name, @@ -103,6 +108,7 @@ class TermModel extends Equatable { id ?? this.id, subjects ?? this.subjects, gradeTypeWeightings ?? this.gradeTypeWeightings, + weightDisplayType ?? this.weightDisplayType, finalGradeType ?? this.finalGradeType, isActiveTerm ?? this.isActiveTerm, name ?? this.name, @@ -322,6 +328,10 @@ class TermModel extends Equatable { bool containsGrade(GradeId id) { return subjects.any((s) => s.hasGrade(id)); } + + TermModel changeWeightDisplayType(WeightDisplayType weightDisplayType) { + return _copyWith(weightDisplayType: weightDisplayType); + } } class SubjectModel extends Equatable { diff --git a/app/lib/grades/pages/term_settings_page/term_settings_page.dart b/app/lib/grades/pages/term_settings_page/term_settings_page.dart index 643d3367e..53dd2c6fc 100644 --- a/app/lib/grades/pages/term_settings_page/term_settings_page.dart +++ b/app/lib/grades/pages/term_settings_page/term_settings_page.dart @@ -8,6 +8,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:sharezone/grades/grades_service/grades_service.dart'; import 'package:sharezone/grades/pages/create_term_page/create_term_page.dart'; @@ -129,7 +130,10 @@ class _Loaded extends StatelessWidget { _IsActiveTerm(isActiveTerm: view.isActiveTerm), const Divider(), const SizedBox(height: 8), - _SubjectWeights(subjects: view.subjects), + _SubjectWeights( + subjects: view.subjects, + weightDisplayType: view.weightDisplayType, + ), const Divider(), const SizedBox(height: 8), _GradingTypeWeights( @@ -282,8 +286,10 @@ class _IsActiveTerm extends StatelessWidget { class _SubjectWeights extends StatelessWidget { const _SubjectWeights({ required this.subjects, + required this.weightDisplayType, }); + final WeightDisplayType weightDisplayType; final IList subjects; @override @@ -296,6 +302,14 @@ class _SubjectWeights extends StatelessWidget { style: TextStyle(fontSize: 16), ), const SizedBox(height: 8), + _WeightDisplaySetting( + weightDisplayType: weightDisplayType, + onWeightDisplayTypeChanged: (newDisplayType) { + final controller = context.read(); + controller.setWeightDisplayType(newDisplayType); + }, + ), + const SizedBox(height: 8), Text( 'Solltest du Kurse haben, die doppelt gewichtet werden, kannst du bei diesen eine 2.0 eintragen.', style: TextStyle( @@ -309,20 +323,76 @@ class _SubjectWeights extends StatelessWidget { color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5), ), ), - for (final subject in subjects) _SubjectTile(subject), + for (final subject in subjects) + _SubjectTile(subject, weightDisplayType), ], ); } } +class _WeightDisplaySetting extends StatelessWidget { + const _WeightDisplaySetting({ + required this.weightDisplayType, + required this.onWeightDisplayTypeChanged, + }); + + final WeightDisplayType weightDisplayType; + final void Function(WeightDisplayType) onWeightDisplayTypeChanged; + + String get weightDisplayTypeString => switch (weightDisplayType) { + WeightDisplayType.factor => 'Faktor', + WeightDisplayType.percent => 'Prozent', + }; + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(Symbols.weight, fill: 1), + title: const Text('Gewichtungssystem'), + subtitle: Text(weightDisplayTypeString), + onTap: () async { + final result = await showDialog( + context: context, + builder: (context) => SimpleDialog( + title: const Text('Gewichtungssystem'), + children: [ + ListTile( + title: const Text('Faktor'), + onTap: () { + Navigator.pop(context, WeightDisplayType.factor); + }, + ), + ListTile( + title: const Text('Prozent'), + onTap: () { + Navigator.pop(context, WeightDisplayType.percent); + }, + ), + ], + ), + ); + if (result != null) { + onWeightDisplayTypeChanged(result); + } + }, + ); + } +} + class _SubjectTile extends StatelessWidget { - const _SubjectTile(this.subject); + const _SubjectTile(this.subject, this.weightDisplayType); final SubjectView subject; + final WeightDisplayType weightDisplayType; @override Widget build(BuildContext context) { - final factor = subject.weight.asFactor.toStringAsPrecision(2); + final factor = switch (weightDisplayType) { + WeightDisplayType.factor => + subject.weight.asFactor.toStringAsPrecision(2), + WeightDisplayType.percent => + '${subject.weight.asPercentage.toStringAsPrecision(3)}%', + }; return ListTile( leading: SubjectAvatar( abbreviation: subject.abbreviation, @@ -330,16 +400,17 @@ class _SubjectTile extends StatelessWidget { ), title: Text(subject.displayName), onTap: () async { - final weight = await showDialog( + final weight = await showDialog( context: context, builder: (context) => _FactorDialog( - initialValue: subject.weight.asFactor.toDouble(), + weight: subject.weight, + weightDisplayType: weightDisplayType, ), ); if (weight != null && context.mounted) { final controller = context.read(); - controller.setSubjectWeight(subject.id, Weight.factor(weight)); + controller.setSubjectWeight(subject.id, weight); } }, trailing: Text( @@ -355,10 +426,12 @@ class _SubjectTile extends StatelessWidget { class _FactorDialog extends StatefulWidget { const _FactorDialog({ - required this.initialValue, + required this.weight, + required this.weightDisplayType, }); - final double initialValue; + final Weight weight; + final WeightDisplayType weightDisplayType; @override State<_FactorDialog> createState() => _FactorDialogState(); @@ -366,16 +439,39 @@ class _FactorDialog extends StatefulWidget { class _FactorDialogState extends State<_FactorDialog> { String? errorText; - double? value; + num? value; + + num get initialValue => switch (widget.weightDisplayType) { + WeightDisplayType.factor => widget.weight.asFactor, + WeightDisplayType.percent => widget.weight.asPercentage, + }; + + String get formatted => switch (widget.weightDisplayType) { + WeightDisplayType.factor => value == null + ? '' + // If there are no decimals, we want to show the "1" as "1.0", so + // that the user knows that he can write a decimal number and what + // the seperator is ("." instead of ","). + : value!.hasDecimals + ? value.toString() + : value!.toStringAsPrecision(2), + WeightDisplayType.percent => value?.toString() ?? '', + }; + + Weight? get valueAsWeight => switch (widget.weightDisplayType) { + WeightDisplayType.factor => + value != null ? Weight.factor(value!) : null, + WeightDisplayType.percent => + value != null ? Weight.percent(value!) : null, + }; @override void initState() { super.initState(); - value = widget.initialValue; + value = initialValue; } - bool isValid() => - errorText == null && value != null && widget.initialValue != value; + bool isValid() => errorText == null && value != null && initialValue != value; @override Widget build(BuildContext context) { @@ -396,21 +492,25 @@ class _FactorDialogState extends State<_FactorDialog> { ), const SizedBox(height: 20), PrefilledTextField( - prefilledText: value?.toStringAsPrecision(2), + prefilledText: formatted, autofocus: true, decoration: InputDecoration( labelText: 'Gewichtung', hintText: 'z.B. 1.0', errorText: errorText, + suffixText: switch (widget.weightDisplayType) { + WeightDisplayType.factor => null, + WeightDisplayType.percent => '%', + }, ), onEditingComplete: () { if (isValid()) { - Navigator.of(context).pop(value); + Navigator.of(context).pop(valueAsWeight); } }, onChanged: (value) { setState(() { - this.value = double.tryParse(value); + this.value = num.tryParse(value); final isValid = this.value != null; errorText = isValid ? null : 'Bitte gib eine Zahl ein.'; @@ -428,8 +528,9 @@ class _FactorDialogState extends State<_FactorDialog> { AnimatedSwitcher( duration: const Duration(milliseconds: 250), child: FilledButton( - onPressed: - isValid() ? () => Navigator.of(context).pop(value) : null, + onPressed: isValid() + ? () => Navigator.of(context).pop(valueAsWeight) + : null, child: const Text('Speichern'), ), ), diff --git a/app/lib/grades/pages/term_settings_page/term_settings_page_controller.dart b/app/lib/grades/pages/term_settings_page/term_settings_page_controller.dart index d4cf7e511..6ee9801b5 100644 --- a/app/lib/grades/pages/term_settings_page/term_settings_page_controller.dart +++ b/app/lib/grades/pages/term_settings_page/term_settings_page_controller.dart @@ -27,6 +27,7 @@ class TermSettingsPageController extends ChangeNotifier { late bool isActiveTerm; late GradingSystem gradingSystem; late GradeType finalGradeType; + late WeightDisplayType weightDisplayType; late IMap _weights; IList courses = IList(); late StreamSubscription> _courseSubscription; @@ -41,6 +42,7 @@ class TermSettingsPageController extends ChangeNotifier { selectableGradingTypes: gradesService.getPossibleGradeTypes(), weights: _weights, subjects: _mergeCoursesAndSubjects(), + weightDisplayType: weightDisplayType, ); TermSettingsPageController({ @@ -59,6 +61,7 @@ class TermSettingsPageController extends ChangeNotifier { gradingSystem = term.gradingSystem; finalGradeType = term.finalGradeType; _weights = term.gradeTypeWeightings; + weightDisplayType = term.weightDisplayType; state = TermSettingsLoaded(view); @@ -136,6 +139,13 @@ class TermSettingsPageController extends ChangeNotifier { notifyListeners(); } + void setWeightDisplayType(WeightDisplayType newDisplayType) { + termRef.changeWeightDisplayType(newDisplayType); + weightDisplayType = newDisplayType; + state = TermSettingsLoaded(view); + notifyListeners(); + } + Future setSubjectWeight(SubjectId subjectId, Weight weight) async { final subRef = termRef.subject(subjectId); final isNewSubject = gradesService.getSubject(subjectId) == null; diff --git a/app/lib/grades/pages/term_settings_page/term_settings_page_view.dart b/app/lib/grades/pages/term_settings_page/term_settings_page_view.dart index bc1650465..9d9329f52 100644 --- a/app/lib/grades/pages/term_settings_page/term_settings_page_view.dart +++ b/app/lib/grades/pages/term_settings_page/term_settings_page_view.dart @@ -19,6 +19,7 @@ class TermSettingsPageView extends Equatable { final IList selectableGradingTypes; final IMap weights; final IList subjects; + final WeightDisplayType weightDisplayType; const TermSettingsPageView({ required this.name, @@ -28,6 +29,7 @@ class TermSettingsPageView extends Equatable { required this.selectableGradingTypes, required this.weights, required this.subjects, + required this.weightDisplayType, }); @override @@ -39,6 +41,7 @@ class TermSettingsPageView extends Equatable { selectableGradingTypes, weights, subjects, + weightDisplayType, ]; } diff --git a/app/test/grades/grades_repository_test.dart b/app/test/grades/grades_repository_test.dart index e1039f4ec..b85ca6a4b 100644 --- a/app/test/grades/grades_repository_test.dart +++ b/app/test/grades/grades_repository_test.dart @@ -115,6 +115,25 @@ void main() { expect(repository.data.containsKey('currentTerm'), isTrue); expect(repository.data['currentTerm'], isNull); }); + test( + 'if `weightDisplayType` is not in data the default ${WeightDisplayType.factor} is used', + () { + final repository = TestFirestoreGradesStateRepository(); + final controller = GradesTestController( + gradesService: GradesService(repository: repository)); + + final term = termWith(id: TermId('term1')); + controller.createTerm(term); + + final terms = repository.data['terms'] as Map; + final term1 = terms['term1'] as Map; + // Make sure that the attribute actually exists + expect(term1.remove('weightDisplayType'), isNotNull); + repository.refreshStateFromUpdatedData(); + + expect(repository.state.value.terms.first.weightDisplayType, + WeightDisplayType.factor); + }); test('serializes expected data map for empty state', () { final res = FirestoreGradesStateRepository.toDto(( customGradeTypes: const IListConst([]), @@ -255,6 +274,7 @@ void main() { term0110 .subject(const SubjectId('englisch')) .changeFinalGradeType(GradeType.oralParticipation.id); + term0110.changeWeightDisplayType(WeightDisplayType.percent); term0210.changeGradeTypeWeight( GradeType.vocabularyTest.id, const Weight.factor(1.5)); @@ -276,6 +296,7 @@ void main() { 'displayName': '02/10', 'createdOn': FieldValue.serverTimestamp(), 'gradingSystem': 'zeroToFifteenPoints', + 'weightDisplayType': 'factor', 'subjectWeights': { 'mathe': { 'value': 2.5, @@ -316,6 +337,7 @@ void main() { 'displayName': '01/10', 'createdOn': FieldValue.serverTimestamp(), 'gradingSystem': 'oneToSixWithPlusAndMinus', + 'weightDisplayType': 'percent', 'subjectWeights': { 'englisch': {'value': 1.0, 'type': 'factor'}, }, @@ -935,4 +957,9 @@ class TestFirestoreGradesStateRepository extends GradesStateRepository { final newState = FirestoreGradesStateRepository.fromData(data); this.state.add(newState); } + + void refreshStateFromUpdatedData() { + final newState = FirestoreGradesStateRepository.fromData(data); + state.add(newState); + } } diff --git a/app/test/grades/grades_test.dart b/app/test/grades/grades_test.dart index 0bdb45d62..3f102d23a 100644 --- a/app/test/grades/grades_test.dart +++ b/app/test/grades/grades_test.dart @@ -480,6 +480,98 @@ void main() { .keys, [exam.id]); }); + test( + 'A term has ${WeightDisplayType.factor} as a default weight display type', + () { + final controller = GradesTestController(); + + controller.createTerm( + termWith( + id: const TermId('term1'), + subjects: [ + subjectWith( + id: const SubjectId('Deutsch'), + abbreviation: 'D', + grades: [gradeWith(value: 2)], + ), + ], + ), + ); + + expect(controller.term(const TermId('term1')).weightDisplayType, + WeightDisplayType.factor); + }); + test('The weight display type of a term can be changed', () { + final controller = GradesTestController(); + + controller.createTerms([ + termWith( + id: const TermId('term1'), + weightDisplayType: WeightDisplayType.factor, + subjects: [ + subjectWith( + id: const SubjectId('Deutsch'), + abbreviation: 'D', + grades: [gradeWith(value: 2)], + ), + ], + ), + ]); + + controller.changeWeightDisplayTypeForTerm( + termId: const TermId('term1'), + weightDisplayType: WeightDisplayType.percent, + ); + + expect(controller.term(const TermId('term1')).weightDisplayType, + WeightDisplayType.percent); + + controller.changeWeightDisplayTypeForTerm( + termId: const TermId('term1'), + weightDisplayType: WeightDisplayType.factor, + ); + + expect(controller.term(const TermId('term1')).weightDisplayType, + WeightDisplayType.factor); + }); + test( + 'When changing weight display type for one term then it doesnt affect the type of the other terms', + () { + final controller = GradesTestController(); + + controller.createTerms([ + termWith( + id: const TermId('term1'), + weightDisplayType: WeightDisplayType.factor, + subjects: [ + subjectWith( + id: const SubjectId('Deutsch'), + abbreviation: 'D', + grades: [gradeWith(value: 2)], + ), + ], + ), + termWith( + id: const TermId('term2'), + weightDisplayType: WeightDisplayType.factor, + subjects: [ + subjectWith( + id: const SubjectId('Englisch'), + abbreviation: 'E', + grades: [gradeWith(value: 2)], + ), + ], + ), + ]); + + controller.changeWeightDisplayTypeForTerm( + termId: const TermId('term1'), + weightDisplayType: WeightDisplayType.percent, + ); + + expect(controller.term(const TermId('term2')).weightDisplayType, + WeightDisplayType.factor); + }); test('a grade type weight can be remove from term', () { final controller = GradesTestController(); diff --git a/app/test/grades/grades_test_common.dart b/app/test/grades/grades_test_common.dart index c9b319e45..6b22334fc 100644 --- a/app/test/grades/grades_test_common.dart +++ b/app/test/grades/grades_test_common.dart @@ -55,6 +55,10 @@ class GradesTestController { gradingSystem: testTerm.gradingSystem, ); + if (testTerm.weightDisplayType != null) { + service.term(termId).changeWeightDisplayType(testTerm.weightDisplayType!); + } + if (testTerm.gradeTypeWeights != null) { for (var e in testTerm.gradeTypeWeights!.entries) { service.term(termId).changeGradeTypeWeight(e.key, e.value); @@ -310,6 +314,17 @@ class GradesTestController { void editCustomGradeType(GradeTypeId id, {required String displayName}) { service.editCustomGradeType(id: id, displayName: displayName); } + + void createTerms(List list, {bool createMissingGradeTypes = true}) { + for (var term in list) { + createTerm(term, createMissingGradeTypes: createMissingGradeTypes); + } + } + + void changeWeightDisplayTypeForTerm( + {required TermId termId, required WeightDisplayType weightDisplayType}) { + service.term(termId).changeWeightDisplayType(weightDisplayType); + } } TestTerm termWith({ @@ -320,6 +335,7 @@ TestTerm termWith({ GradeTypeId finalGradeType = const GradeTypeId('Endnote'), bool isActiveTerm = true, GradingSystem? gradingSystem, + WeightDisplayType? weightDisplayType, }) { final rdm = randomAlpha(5); final idd = id ?? TermId(rdm); @@ -328,6 +344,7 @@ TestTerm termWith({ name: name ?? '$idd', subjects: IMap.fromEntries(subjects.map((s) => MapEntry(s.id, s))), gradingSystem: gradingSystem ?? GradingSystem.zeroToFifteenPoints, + weightDisplayType: weightDisplayType, gradeTypeWeights: gradeTypeWeights, finalGradeType: finalGradeType, isActiveTerm: isActiveTerm, @@ -339,6 +356,7 @@ class TestTerm { final String name; final IMap subjects; final GradingSystem gradingSystem; + final WeightDisplayType? weightDisplayType; final Map? gradeTypeWeights; final GradeTypeId finalGradeType; final bool isActiveTerm; @@ -351,6 +369,7 @@ class TestTerm { this.gradeTypeWeights, required this.isActiveTerm, required this.gradingSystem, + required this.weightDisplayType, }); } diff --git a/app/test/grades/pages/term_settings_page/term_settings_page_test.dart b/app/test/grades/pages/term_settings_page/term_settings_page_test.dart new file mode 100644 index 000000000..d5774640e --- /dev/null +++ b/app/test/grades/pages/term_settings_page/term_settings_page_test.dart @@ -0,0 +1,103 @@ +// Copyright (c) 2024 Sharezone UG (haftungsbeschränkt) +// Licensed under the EUPL-1.2-or-later. +// +// You may obtain a copy of the Licence at: +// https://joinup.ec.europa.eu/software/page/eupl +// +// SPDX-License-Identifier: EUPL-1.2 + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; +import 'package:sharezone/grades/grades_service/grades_service.dart'; +import 'package:sharezone/grades/pages/term_settings_page/term_settings_page.dart'; +import 'package:sharezone/grades/pages/term_settings_page/term_settings_page_controller.dart'; +import 'package:sharezone/grades/pages/term_settings_page/term_settings_page_controller_factory.dart'; + +import '../../../../test_goldens/grades/pages/term_settings_page/term_settings_page_test.dart'; +import '../../../../test_goldens/grades/pages/term_settings_page/term_settings_page_test.mocks.dart'; + +void main() { + group('$TermSettingsPage', () { + const termId = TermId('term-1'); + + late TermSettingsPageController controller; + late TermSettingsPageControllerFactory controllerFactory; + + setUp(() { + controller = MockTermSettingsPageController(); + controllerFactory = MockTermSettingsPageControllerFactory(); + when(controllerFactory.create(termId)).thenReturn(controller); + }); + + void setState(TermSettingsState state) { + // Mockito does not support mocking sealed classes yet, so we have to + // provide a dummy implementation of the state. + // + // Ticket: https://github.com/dart-lang/mockito/issues/675 + provideDummy(state); + when(controller.state).thenReturn(state); + } + + Future pumpTermSettingsPage(WidgetTester tester) async { + await tester.pumpWidgetBuilder( + MultiProvider( + providers: [ + Provider( + create: (_) => GradesService(), + ), + Provider.value( + value: controllerFactory, + ), + ], + child: const TermSettingsPage(termId: termId), + ), + wrapper: materialAppWrapper()); + } + + testWidgets( + 'if $WeightDisplayType is ${WeightDisplayType.factor} the factor dialog will be shown when tapping subject weight', + (tester) async { + expect(loadedState2.view.weightDisplayType, WeightDisplayType.factor); + setState(loadedState2); + await pumpTermSettingsPage(tester); + + final germanSubjectFinder = find.text('Deutsch'); + tester.ensureVisible(germanSubjectFinder); + await tester.tap(find.text("Deutsch")); + await tester.pumpAndSettle(); + + Finder findInDialog(Finder finder) => find.descendant( + of: find.byWidgetPredicate((widget) => widget is Dialog), + matching: finder, + ); + // We want to show "1.0" instead of "1" so that the user knows what the + // decimal separator is ("." instead of ","). + expect(findInDialog(find.text('1.0')), findsOneWidget); + expect(findInDialog(find.text('%')), findsNothing); + }); + testWidgets( + 'if $WeightDisplayType is ${WeightDisplayType.percent} the percent dialog will be shown when tapping subject weight', + (tester) async { + expect(loadedState1.view.weightDisplayType, WeightDisplayType.percent); + setState(loadedState1); + + await pumpTermSettingsPage(tester); + + final germanSubjectFinder = find.text('Deutsch'); + tester.ensureVisible(germanSubjectFinder); + await tester.tap(find.text("Deutsch")); + await tester.pumpAndSettle(); + + Finder findInDialog(Finder finder) => find.descendant( + of: find.byWidgetPredicate((widget) => widget is Dialog), + matching: finder, + ); + expect(findInDialog(find.text('100')), findsOneWidget); + // We add "%" as a suffix to the text field + expect(findInDialog(find.text('%')), findsOneWidget); + }); + }); +} diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.iphone11.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.iphone11.png index e1ed00068..31c21ba0a 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.iphone11.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.iphone11.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.phone.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.phone.png index 594bba968..6f82ee3ad 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.phone.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.phone.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.tablet_landscape.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.tablet_landscape.png index 36a39a3b7..51ceeb07d 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.tablet_landscape.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.tablet_landscape.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.tablet_portrait.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.tablet_portrait.png index efe2cb0ba..ea28cd940 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.tablet_portrait.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_dark.tablet_portrait.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.iphone11.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.iphone11.png index a2cf45996..06db9614c 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.iphone11.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.iphone11.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.phone.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.phone.png index 07d8dfcbe..bb0dec197 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.phone.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.phone.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.tablet_landscape.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.tablet_landscape.png index 33fab6d9b..68c638cdd 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.tablet_landscape.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.tablet_landscape.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.tablet_portrait.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.tablet_portrait.png index 26adbb809..37eb3733b 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.tablet_portrait.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_page_with_data_2_light.tablet_portrait.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.iphone11.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.iphone11.png index 28eb4f692..1f7f31e70 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.iphone11.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.iphone11.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.phone.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.phone.png index dd5d16f76..b17414772 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.phone.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.phone.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.tablet_landscape.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.tablet_landscape.png index 6852860b2..ac75e5581 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.tablet_landscape.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.tablet_landscape.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.tablet_portrait.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.tablet_portrait.png index e4b48749e..2fd72d888 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.tablet_portrait.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_dark.tablet_portrait.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.iphone11.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.iphone11.png index cee76f601..52a942b02 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.iphone11.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.iphone11.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.phone.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.phone.png index 18cde1392..77e7a158c 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.phone.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.phone.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.tablet_landscape.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.tablet_landscape.png index 145d5ef64..9be921e72 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.tablet_landscape.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.tablet_landscape.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.tablet_portrait.png b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.tablet_portrait.png index 734ad70b3..b95c762d6 100644 Binary files a/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.tablet_portrait.png and b/app/test_goldens/grades/pages/term_settings_page/goldens/terms_settings_with_data_1_light.tablet_portrait.png differ diff --git a/app/test_goldens/grades/pages/term_settings_page/term_settings_page_test.dart b/app/test_goldens/grades/pages/term_settings_page/term_settings_page_test.dart index c92fd484f..a0466c23f 100644 --- a/app/test_goldens/grades/pages/term_settings_page/term_settings_page_test.dart +++ b/app/test_goldens/grades/pages/term_settings_page/term_settings_page_test.dart @@ -50,76 +50,12 @@ void main() { when(controller.state).thenReturn(state); } - void setLoaded2() { - final random = Random(35); - setState( - TermSettingsLoaded( - TermSettingsPageView( - name: '11/23', - isActiveTerm: true, - gradingSystem: GradingSystem.zeroToFifteenPoints, - finalGradeType: GradeType.writtenExam, - selectableGradingTypes: const IListConst([]), - weights: IMapConst({ - GradeType.writtenExam.id: const Weight.factor(2), - GradeType.oralParticipation.id: const Weight.factor(.5), - GradeType.presentation.id: const Weight.factor(1), - }), - subjects: IListConst([ - ( - displayName: 'Deutsch', - abbreviation: 'DE', - design: Design.random(random), - id: const SubjectId('d'), - weight: const Weight.factor(1), - ), - ( - displayName: 'Englisch', - abbreviation: 'E', - design: Design.random(random), - id: const SubjectId('e'), - weight: const Weight.factor(2), - ), - ]), - ), - ), - ); + void setLoaded1() { + setState(loadedState1); } - void setLoaded1() { - final random = Random(42); - setState( - TermSettingsLoaded( - TermSettingsPageView( - name: '10/22', - isActiveTerm: true, - gradingSystem: GradingSystem.oneToSixWithPlusAndMinus, - finalGradeType: GradeType.schoolReportGrade, - selectableGradingTypes: const IListConst([]), - weights: IMapConst({ - GradeType.writtenExam.id: const Weight.factor(2), - GradeType.oralParticipation.id: const Weight.factor(.5), - GradeType.presentation.id: const Weight.factor(1), - }), - subjects: IListConst([ - ( - displayName: 'Deutsch', - abbreviation: 'DE', - design: Design.random(random), - id: const SubjectId('d'), - weight: const Weight.factor(1), - ), - ( - displayName: 'Englisch', - abbreviation: 'E', - design: Design.random(random), - id: const SubjectId('e'), - weight: const Weight.factor(2), - ), - ]), - ), - ), - ); + void setLoaded2() { + setState(loadedState2); } void setError() { @@ -133,17 +69,20 @@ void main() { Future pushTermSettingsPage( WidgetTester tester, ThemeData theme) async { await tester.pumpWidgetBuilder( - MultiProvider(providers: [ - Provider( - create: (_) => GradesService(), - ), - Provider.value( - value: controllerFactory, - ), - ChangeNotifierProvider.value( - value: controller, - ), - ], child: const TermSettingsPage(termId: termId)), + MultiProvider( + providers: [ + Provider( + create: (_) => GradesService(), + ), + Provider.value( + value: controllerFactory, + ), + ChangeNotifierProvider.value( + value: controller, + ), + ], + child: const TermSettingsPage(termId: termId), + ), wrapper: materialAppWrapper(theme: theme), ); } @@ -210,3 +149,67 @@ void main() { }); }); } + +final loadedState1 = TermSettingsLoaded( + TermSettingsPageView( + name: '10/22', + isActiveTerm: true, + gradingSystem: GradingSystem.oneToSixWithPlusAndMinus, + finalGradeType: GradeType.schoolReportGrade, + selectableGradingTypes: const IListConst([]), + weightDisplayType: WeightDisplayType.percent, + weights: IMapConst({ + GradeType.writtenExam.id: const Weight.percent(200), + GradeType.oralParticipation.id: const Weight.percent(50), + GradeType.presentation.id: const Weight.percent(100), + }), + subjects: IListConst([ + ( + displayName: 'Deutsch', + abbreviation: 'DE', + design: Design.random(Random(42)), + id: const SubjectId('d'), + weight: const Weight.factor(1), + ), + ( + displayName: 'Englisch', + abbreviation: 'E', + design: Design.random(Random(42)), + id: const SubjectId('e'), + weight: const Weight.factor(2), + ), + ]), + ), +); + +final loadedState2 = TermSettingsLoaded( + TermSettingsPageView( + name: '11/23', + isActiveTerm: true, + gradingSystem: GradingSystem.zeroToFifteenPoints, + finalGradeType: GradeType.writtenExam, + selectableGradingTypes: const IListConst([]), + weightDisplayType: WeightDisplayType.factor, + weights: IMapConst({ + GradeType.writtenExam.id: const Weight.factor(2), + GradeType.oralParticipation.id: const Weight.factor(.5), + GradeType.presentation.id: const Weight.factor(1), + }), + subjects: IListConst([ + ( + displayName: 'Deutsch', + abbreviation: 'DE', + design: Design.random(Random(35)), + id: const SubjectId('d'), + weight: const Weight.factor(1), + ), + ( + displayName: 'Englisch', + abbreviation: 'E', + design: Design.random(Random(35)), + id: const SubjectId('e'), + weight: const Weight.factor(2), + ), + ]), + ), +);