diff --git a/app/lib/settings/src/subpages/timetable/timetable_settings_page.dart b/app/lib/settings/src/subpages/timetable/timetable_settings_page.dart index e3fa912a2..985e86e4a 100644 --- a/app/lib/settings/src/subpages/timetable/timetable_settings_page.dart +++ b/app/lib/settings/src/subpages/timetable/timetable_settings_page.dart @@ -23,11 +23,9 @@ import 'package:sharezone/settings/src/subpages/timetable/periods/periods_edit_p import 'package:sharezone/settings/src/subpages/timetable/weekdays/weekdays_edit_page.dart'; import 'package:sharezone/sharezone_plus/page/sharezone_plus_page.dart'; import 'package:sharezone/sharezone_plus/subscription_service/subscription_service.dart'; -import 'package:sharezone/timetable/src/edit_time.dart'; import 'package:sharezone/timetable/src/edit_weektype.dart'; import 'package:sharezone/timetable/src/models/lesson_length/lesson_length.dart'; import 'package:sharezone_widgets/sharezone_widgets.dart'; -import 'package:time/time.dart'; import 'package:user/user.dart'; class TimetableSettingsPage extends StatelessWidget { @@ -48,20 +46,14 @@ class TimetableSettingsPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - LessonsLengthField( - streamLessonLength: bloc.lessonLengthStream, - onChanged: (lessonLength) => - bloc.saveLessonLengthInCache(lessonLength.minutes), - ), + _TimetablePeriodsField(), const Divider(), _ABWeekField(), const Divider(), - _TimetablePreferencesField(), + _AbbreviationInTimetable(), const Divider(), _TimetableEnabledWeekDaysField(), const Divider(), - _TimetablePeriodsField(), - const Divider(), const _ICalLinks(), // We only show the time picker settings on iOS because on // other platforms we use the different time picker where we @@ -153,11 +145,34 @@ class _ABWeekField extends StatelessWidget { } } +class _AbbreviationInTimetable extends StatelessWidget { + @override + Widget build(BuildContext context) { + final bloc = BlocProvider.of(context); + return StreamBuilder( + stream: bloc.streamUserSettings(), + builder: (context, snapshot) { + if (!snapshot.hasData) return Container(); + final userSettings = snapshot.data!; + return SwitchListTile.adaptive( + title: const Text("Kürzel im Stundenplan anzeigen"), + value: userSettings.showAbbreviation, + onChanged: (newValue) { + bloc.updateSettings( + userSettings.copyWith(showAbbreviation: newValue)); + }, + ); + }, + ); + } +} + class _TimetablePeriodsField extends StatelessWidget { @override Widget build(BuildContext context) { return ListTile( title: const Text("Stundenzeiten"), + subtitle: const Text("Stundenplanbeginn, Stundenlänge, etc."), onTap: () => openPeriodsEditPage(context), ); } @@ -204,45 +219,6 @@ class _TimetableEnabledWeekDaysField extends StatelessWidget { } } -class _TimetablePreferencesField extends StatelessWidget { - @override - Widget build(BuildContext context) { - final bloc = BlocProvider.of(context); - return StreamBuilder( - stream: bloc.streamUserSettings(), - builder: (context, snapshot) { - if (!snapshot.hasData) return Container(); - final userSettings = snapshot.data; - final tbStart = userSettings!.timetableStartTime; - return Column( - children: [ - ListTile( - title: const Text("Stundenplanbeginn"), - subtitle: Text(tbStart.time), - onTap: () async { - final newTime = await selectTime(context, - initialTime: Time(hour: 8, minute: 0)); - if (newTime != null) { - bloc.updateTimetableStartTime(newTime); - } - }, - ), - const Divider(), - SwitchListTile.adaptive( - title: const Text("Kürzel im Stundenplan anzeigen"), - value: userSettings.showAbbreviation, - onChanged: (newValue) { - bloc.updateSettings( - userSettings.copyWith(showAbbreviation: newValue)); - }, - ), - ], - ); - }, - ); - } -} - class LessonsLengthField extends StatelessWidget { const LessonsLengthField({ super.key, diff --git a/app/test_goldens/timetable/goldens/timetable_settings_page_dark.iphone11.png b/app/test_goldens/timetable/goldens/timetable_settings_page_dark.iphone11.png new file mode 100644 index 000000000..a87aa74c3 Binary files /dev/null and b/app/test_goldens/timetable/goldens/timetable_settings_page_dark.iphone11.png differ diff --git a/app/test_goldens/timetable/goldens/timetable_settings_page_dark.phone.png b/app/test_goldens/timetable/goldens/timetable_settings_page_dark.phone.png new file mode 100644 index 000000000..8a2135259 Binary files /dev/null and b/app/test_goldens/timetable/goldens/timetable_settings_page_dark.phone.png differ diff --git a/app/test_goldens/timetable/goldens/timetable_settings_page_dark.phone_landscape.png b/app/test_goldens/timetable/goldens/timetable_settings_page_dark.phone_landscape.png new file mode 100644 index 000000000..f081a6253 Binary files /dev/null and b/app/test_goldens/timetable/goldens/timetable_settings_page_dark.phone_landscape.png differ diff --git a/app/test_goldens/timetable/goldens/timetable_settings_page_dark.tablet_landscape.png b/app/test_goldens/timetable/goldens/timetable_settings_page_dark.tablet_landscape.png new file mode 100644 index 000000000..41cf2a1f3 Binary files /dev/null and b/app/test_goldens/timetable/goldens/timetable_settings_page_dark.tablet_landscape.png differ diff --git a/app/test_goldens/timetable/goldens/timetable_settings_page_dark.tablet_portrait.png b/app/test_goldens/timetable/goldens/timetable_settings_page_dark.tablet_portrait.png new file mode 100644 index 000000000..a4c629180 Binary files /dev/null and b/app/test_goldens/timetable/goldens/timetable_settings_page_dark.tablet_portrait.png differ diff --git a/app/test_goldens/timetable/goldens/timetable_settings_page_light.iphone11.png b/app/test_goldens/timetable/goldens/timetable_settings_page_light.iphone11.png new file mode 100644 index 000000000..7e1aee964 Binary files /dev/null and b/app/test_goldens/timetable/goldens/timetable_settings_page_light.iphone11.png differ diff --git a/app/test_goldens/timetable/goldens/timetable_settings_page_light.phone.png b/app/test_goldens/timetable/goldens/timetable_settings_page_light.phone.png new file mode 100644 index 000000000..00ce9deee Binary files /dev/null and b/app/test_goldens/timetable/goldens/timetable_settings_page_light.phone.png differ diff --git a/app/test_goldens/timetable/goldens/timetable_settings_page_light.phone_landscape.png b/app/test_goldens/timetable/goldens/timetable_settings_page_light.phone_landscape.png new file mode 100644 index 000000000..ed41054c3 Binary files /dev/null and b/app/test_goldens/timetable/goldens/timetable_settings_page_light.phone_landscape.png differ diff --git a/app/test_goldens/timetable/goldens/timetable_settings_page_light.tablet_landscape.png b/app/test_goldens/timetable/goldens/timetable_settings_page_light.tablet_landscape.png new file mode 100644 index 000000000..6fe8f2b7e Binary files /dev/null and b/app/test_goldens/timetable/goldens/timetable_settings_page_light.tablet_landscape.png differ diff --git a/app/test_goldens/timetable/goldens/timetable_settings_page_light.tablet_portrait.png b/app/test_goldens/timetable/goldens/timetable_settings_page_light.tablet_portrait.png new file mode 100644 index 000000000..c603cc1c7 Binary files /dev/null and b/app/test_goldens/timetable/goldens/timetable_settings_page_light.tablet_portrait.png differ diff --git a/app/test_goldens/timetable/timetable_settings_page_test.dart b/app/test_goldens/timetable/timetable_settings_page_test.dart new file mode 100644 index 000000000..673450f2a --- /dev/null +++ b/app/test_goldens/timetable/timetable_settings_page_test.dart @@ -0,0 +1,85 @@ +// Copyright (c) 2022 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:bloc_provider/bloc_provider.dart'; +import 'package:bloc_provider/multi_bloc_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; +import 'package:sharezone/settings/src/bloc/user_settings_bloc.dart'; +import 'package:sharezone/settings/src/subpages/timetable/bloc/timetable_settings_bloc_factory.dart'; +import 'package:sharezone/settings/src/subpages/timetable/time_picker_settings_cache.dart'; +import 'package:sharezone/settings/src/subpages/timetable/timetable_settings_page.dart'; +import 'package:sharezone/sharezone_plus/subscription_service/subscription_service.dart'; +import 'package:sharezone/timetable/src/models/lesson_length/lesson_length_cache.dart'; +import 'package:sharezone_widgets/sharezone_widgets.dart'; +import 'package:user/user.dart'; + +import 'timetable_settings_page_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + group(TimetableSettingsPage, () { + late UserSettingsBloc userSettingsBloc; + + setUp(() { + userSettingsBloc = MockUserSettingsBloc(); + + when(userSettingsBloc.streamUserSettings()) + .thenAnswer((_) => Stream.value(UserSettings.defaultSettings())); + }); + + Future pumpTimetableSettingsPage( + WidgetTester tester, { + ThemeData? themeData, + }) async { + await tester.pumpWidgetBuilder( + Provider( + create: (context) => MockSubscriptionService(), + child: MultiBlocProvider( + blocProviders: [ + BlocProvider( + bloc: TimetableSettingsBlocFactory( + MockLessonLengthCache(), + MockTimePickerSettingsCache(), + ), + ), + BlocProvider(bloc: userSettingsBloc) + ], + child: (_) => const TimetableSettingsPage(), + ), + ), + wrapper: materialAppWrapper(theme: themeData), + ); + } + + testGoldens('should render as expected (light mode)', (tester) async { + await pumpTimetableSettingsPage( + tester, + themeData: getLightTheme(), + ); + await multiScreenGolden(tester, 'timetable_settings_page_light'); + }); + + testGoldens('should render as expected (dark mode)', (tester) async { + await pumpTimetableSettingsPage( + tester, + themeData: getDarkTheme(), + ); + await multiScreenGolden(tester, 'timetable_settings_page_dark'); + }); + }); +} diff --git a/app/test_goldens/timetable/timetable_settings_page_test.mocks.dart b/app/test_goldens/timetable/timetable_settings_page_test.mocks.dart new file mode 100644 index 000000000..f96d699f8 --- /dev/null +++ b/app/test_goldens/timetable/timetable_settings_page_test.mocks.dart @@ -0,0 +1,349 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in sharezone/test_goldens/timetable/timetable_settings_page_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i6; + +import 'package:cloud_functions/cloud_functions.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; +import 'package:sharezone/settings/src/bloc/user_settings_bloc.dart' as _i10; +import 'package:sharezone/settings/src/subpages/timetable/time_picker_settings_cache.dart' + as _i7; +import 'package:sharezone/sharezone_plus/subscription_service/subscription_service.dart' + as _i8; +import 'package:sharezone/timetable/src/models/lesson_length/lesson_length.dart' + as _i5; +import 'package:sharezone/timetable/src/models/lesson_length/lesson_length_cache.dart' + as _i4; +import 'package:sharezone/util/cache/streaming_key_value_store.dart' as _i2; +import 'package:time/time.dart' as _i11; +import 'package:user/user.dart' as _i9; + +// 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: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeStreamingKeyValueStore_0 extends _i1.SmartFake + implements _i2.StreamingKeyValueStore { + _FakeStreamingKeyValueStore_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFirebaseFunctions_1 extends _i1.SmartFake + implements _i3.FirebaseFunctions { + _FakeFirebaseFunctions_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [LessonLengthCache]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLessonLengthCache extends _i1.Mock implements _i4.LessonLengthCache { + @override + _i2.StreamingKeyValueStore get streamingCache => (super.noSuchMethod( + Invocation.getter(#streamingCache), + returnValue: _FakeStreamingKeyValueStore_0( + this, + Invocation.getter(#streamingCache), + ), + returnValueForMissingStub: _FakeStreamingKeyValueStore_0( + this, + Invocation.getter(#streamingCache), + ), + ) as _i2.StreamingKeyValueStore); + + @override + void setLessonLength(_i5.LessonLength? lessonLength) => super.noSuchMethod( + Invocation.method( + #setLessonLength, + [lessonLength], + ), + returnValueForMissingStub: null, + ); + + @override + _i6.Stream<_i5.LessonLength> streamLessonLength() => (super.noSuchMethod( + Invocation.method( + #streamLessonLength, + [], + ), + returnValue: _i6.Stream<_i5.LessonLength>.empty(), + returnValueForMissingStub: _i6.Stream<_i5.LessonLength>.empty(), + ) as _i6.Stream<_i5.LessonLength>); + + @override + _i6.Future hasUserSavedLessonLengthInCache() => (super.noSuchMethod( + Invocation.method( + #hasUserSavedLessonLengthInCache, + [], + ), + returnValue: _i6.Future.value(false), + returnValueForMissingStub: _i6.Future.value(false), + ) as _i6.Future); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [TimePickerSettingsCache]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTimePickerSettingsCache extends _i1.Mock + implements _i7.TimePickerSettingsCache { + @override + _i2.StreamingKeyValueStore get streamingCache => (super.noSuchMethod( + Invocation.getter(#streamingCache), + returnValue: _FakeStreamingKeyValueStore_0( + this, + Invocation.getter(#streamingCache), + ), + returnValueForMissingStub: _FakeStreamingKeyValueStore_0( + this, + Invocation.getter(#streamingCache), + ), + ) as _i2.StreamingKeyValueStore); + + @override + void setTimePickerWithFifeMinutesInterval(bool? newValue) => + super.noSuchMethod( + Invocation.method( + #setTimePickerWithFifeMinutesInterval, + [newValue], + ), + returnValueForMissingStub: null, + ); + + @override + _i6.Stream isTimePickerWithFifeMinutesIntervalActiveStream() => + (super.noSuchMethod( + Invocation.method( + #isTimePickerWithFifeMinutesIntervalActiveStream, + [], + ), + returnValue: _i6.Stream.empty(), + returnValueForMissingStub: _i6.Stream.empty(), + ) as _i6.Stream); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [SubscriptionService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSubscriptionService extends _i1.Mock + implements _i8.SubscriptionService { + @override + _i6.Stream<_i9.AppUser?> get user => (super.noSuchMethod( + Invocation.getter(#user), + returnValue: _i6.Stream<_i9.AppUser?>.empty(), + returnValueForMissingStub: _i6.Stream<_i9.AppUser?>.empty(), + ) as _i6.Stream<_i9.AppUser?>); + + @override + _i3.FirebaseFunctions get functions => (super.noSuchMethod( + Invocation.getter(#functions), + returnValue: _FakeFirebaseFunctions_1( + this, + Invocation.getter(#functions), + ), + returnValueForMissingStub: _FakeFirebaseFunctions_1( + this, + Invocation.getter(#functions), + ), + ) as _i3.FirebaseFunctions); + + @override + _i6.Stream<_i9.SharezonePlusStatus?> get sharezonePlusStatusStream => + (super.noSuchMethod( + Invocation.getter(#sharezonePlusStatusStream), + returnValue: _i6.Stream<_i9.SharezonePlusStatus?>.empty(), + returnValueForMissingStub: _i6.Stream<_i9.SharezonePlusStatus?>.empty(), + ) as _i6.Stream<_i9.SharezonePlusStatus?>); + + @override + set sharezonePlusStatusStream( + _i6.Stream<_i9.SharezonePlusStatus?>? _sharezonePlusStatusStream) => + super.noSuchMethod( + Invocation.setter( + #sharezonePlusStatusStream, + _sharezonePlusStatusStream, + ), + returnValueForMissingStub: null, + ); + + @override + bool isSubscriptionActive([_i9.AppUser? appUser]) => (super.noSuchMethod( + Invocation.method( + #isSubscriptionActive, + [appUser], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i6.Stream isSubscriptionActiveStream() => (super.noSuchMethod( + Invocation.method( + #isSubscriptionActiveStream, + [], + ), + returnValue: _i6.Stream.empty(), + returnValueForMissingStub: _i6.Stream.empty(), + ) as _i6.Stream); + + @override + bool hasFeatureUnlocked(_i8.SharezonePlusFeature? feature) => + (super.noSuchMethod( + Invocation.method( + #hasFeatureUnlocked, + [feature], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i6.Stream hasFeatureUnlockedStream( + _i8.SharezonePlusFeature? feature) => + (super.noSuchMethod( + Invocation.method( + #hasFeatureUnlockedStream, + [feature], + ), + returnValue: _i6.Stream.empty(), + returnValueForMissingStub: _i6.Stream.empty(), + ) as _i6.Stream); + + @override + _i6.Future cancelStripeSubscription() => (super.noSuchMethod( + Invocation.method( + #cancelStripeSubscription, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future showLetParentsBuyButton() => (super.noSuchMethod( + Invocation.method( + #showLetParentsBuyButton, + [], + ), + returnValue: _i6.Future.value(false), + returnValueForMissingStub: _i6.Future.value(false), + ) as _i6.Future); + + @override + _i6.Future getPlusWebsiteBuyToken() => (super.noSuchMethod( + Invocation.method( + #getPlusWebsiteBuyToken, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [UserSettingsBloc]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUserSettingsBloc extends _i1.Mock implements _i10.UserSettingsBloc { + @override + _i6.Stream<_i9.UserSettings> streamUserSettings() => (super.noSuchMethod( + Invocation.method( + #streamUserSettings, + [], + ), + returnValue: _i6.Stream<_i9.UserSettings>.empty(), + returnValueForMissingStub: _i6.Stream<_i9.UserSettings>.empty(), + ) as _i6.Stream<_i9.UserSettings>); + + @override + void updateSettings(_i9.UserSettings? newUserSettings) => super.noSuchMethod( + Invocation.method( + #updateSettings, + [newUserSettings], + ), + returnValueForMissingStub: null, + ); + + @override + void updatePeriods(_i9.Periods? periods) => super.noSuchMethod( + Invocation.method( + #updatePeriods, + [periods], + ), + returnValueForMissingStub: null, + ); + + @override + void updateEnabledWeekDays(_i9.EnabledWeekDays? enabledWeekDays) => + super.noSuchMethod( + Invocation.method( + #updateEnabledWeekDays, + [enabledWeekDays], + ), + returnValueForMissingStub: null, + ); + + @override + void updateTimetableStartTime(_i11.Time? time) => super.noSuchMethod( + Invocation.method( + #updateTimetableStartTime, + [time], + ), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); +}