From e5a8dd91c24330fd79e9b69785c075c8326fb61f Mon Sep 17 00:00:00 2001 From: HugoMigner <48542098+HugoMigner@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:54:11 -0400 Subject: [PATCH] Add a customizable card in the dashboard (#853) * Add broadcast card logic (WIP) * WIP broadcast card UI & logic w/ remote config * Fix card order in dashboard+ config default Fix card order in dashboard and prevent duplicates from appearing Add default values for fr & en remote configs * Set card busy when model loading And minor code improvements * WIP add title & toggle remote configs * Add remote config based color * Use color from remote config, or default * Fix color variable and text color * Add reorder broadcast * Add custom icon, onClick event and reorderable/hide functions * Fix cards not appearing * Tests * Cleanup * [BOT] Applying version. * Fix tests * [BOT] Applying version. * Check for broadcast change while card is dismissed * Fix tests * Fix tests again * Fix tests pdv * Fix tests real --------- Co-authored-by: Emma Co-authored-by: HugoMigner --- lib/core/constants/preferences_flags.dart | 2 + lib/core/managers/settings_manager.dart | 12 +- lib/core/services/remote_config_service.dart | 60 ++++++- lib/core/viewmodels/dashboard_viewmodel.dart | 65 ++++++- lib/ui/views/dashboard_view.dart | 82 +++++++++ pubspec.yaml | 2 +- test/managers/settings_manager_test.dart | 36 ++-- .../services/remote_config_service_mock.dart | 40 +++++ test/ui/views/dashboard_view_test.dart | 169 +++++++++++++----- test/viewmodels/dashboard_viewmodel_test.dart | 79 ++++++-- 10 files changed, 463 insertions(+), 84 deletions(-) diff --git a/lib/core/constants/preferences_flags.dart b/lib/core/constants/preferences_flags.dart index 1344f8667..3b578a17c 100644 --- a/lib/core/constants/preferences_flags.dart +++ b/lib/core/constants/preferences_flags.dart @@ -34,11 +34,13 @@ enum PreferencesFlag { discoveryMore, // Dashboard flags + broadcastCard, aboutUsCard, scheduleCard, progressBarCard, gradesCard, progressBarText, + broadcastChange, // Rating flag ratingTimer, diff --git a/lib/core/managers/settings_manager.dart b/lib/core/managers/settings_manager.dart index fba0d85c8..58655fd93 100644 --- a/lib/core/managers/settings_manager.dart +++ b/lib/core/managers/settings_manager.dart @@ -97,6 +97,13 @@ class SettingsManager with ChangeNotifier { Future> getDashboard() async { final Map dashboard = {}; + final broadcastCardIndex = + await _preferencesService.getInt(PreferencesFlag.broadcastCard) ?? + getDefaultCardIndex(PreferencesFlag.broadcastCard); + + dashboard.putIfAbsent( + PreferencesFlag.broadcastCard, () => broadcastCardIndex); + final aboutUsIndex = await _preferencesService.getInt(PreferencesFlag.aboutUsCard) ?? getDefaultCardIndex(PreferencesFlag.aboutUsCard); @@ -268,8 +275,9 @@ class SettingsManager with ChangeNotifier { } /// Get the default index of each card - int getDefaultCardIndex(PreferencesFlag flag) => - flag.index - PreferencesFlag.aboutUsCard.index; + int getDefaultCardIndex(PreferencesFlag flag) { + return flag.index - PreferencesFlag.broadcastCard.index; + } bool get calendarViewSetting => _remoteConfigService.scheduleListViewDefault; } diff --git a/lib/core/services/remote_config_service.dart b/lib/core/services/remote_config_service.dart index df559bf3a..995ed91a0 100644 --- a/lib/core/services/remote_config_service.dart +++ b/lib/core/services/remote_config_service.dart @@ -8,14 +8,32 @@ import 'package:notredame/locator.dart'; /// Manage the analytics of the application class RemoteConfigService { + static const String tag = "RemoteConfigService"; static const _serviceIsDown = "service_is_down"; + + // dashboard message remote config keys + static const _dashboardMsgToggle = "dashboard_message_toggle"; + static const _dashboardMsgFr = "dashboard_message_fr"; + static const _dashboardMsgEn = "dashboard_message_en"; + static const _dashboardMsgTitleFr = "dashboard_message_title_fr"; + static const _dashboardMsgTitleEn = "dashboard_message_title_en"; + static const _dashboardMsgColor = "dashboard_message_color"; + static const _dashboardMsgUrl = "dashboard_message_url"; + static const _dashboardMsgType = "dashboard_message_type"; + static const _scheduleListViewDefault = "schedule_list_view_default"; final FirebaseRemoteConfig _remoteConfig = FirebaseRemoteConfig.instance; final defaults = { _serviceIsDown: false, + _dashboardMsgFr: "", + _dashboardMsgEn: "", + _dashboardMsgTitleFr: "", + _dashboardMsgTitleEn: "", + _dashboardMsgColor: "", + _dashboardMsgUrl: "", + _dashboardMsgType: "", _scheduleListViewDefault: true }; - static const String tag = "RemoteConfigService"; Future initialize() async { await _remoteConfig.setDefaults(defaults); @@ -27,11 +45,51 @@ class RemoteConfigService { return _remoteConfig.getBool(_serviceIsDown); } + bool get dashboardMessageActive { + fetch(); + return _remoteConfig.getBool(_dashboardMsgToggle); + } + bool get scheduleListViewDefault { fetch(); return _remoteConfig.getBool(_scheduleListViewDefault); } + String get dashboardMessageFr { + fetch(); + return _remoteConfig.getString(_dashboardMsgFr); + } + + String get dashboardMessageEn { + fetch(); + return _remoteConfig.getString(_dashboardMsgEn); + } + + String get dashboardMessageTitleFr { + fetch(); + return _remoteConfig.getString(_dashboardMsgTitleFr); + } + + String get dashboardMessageTitleEn { + fetch(); + return _remoteConfig.getString(_dashboardMsgTitleEn); + } + + String get dashboardMsgColor { + fetch(); + return _remoteConfig.getString(_dashboardMsgColor); + } + + String get dashboardMsgUrl { + fetch(); + return _remoteConfig.getString(_dashboardMsgUrl); + } + + String get dashboardMsgType { + fetch(); + return _remoteConfig.getString(_dashboardMsgType); + } + Future fetch() async { final AnalyticsService analyticsService = locator(); try { diff --git a/lib/core/viewmodels/dashboard_viewmodel.dart b/lib/core/viewmodels/dashboard_viewmodel.dart index 45612c77f..2f9752706 100644 --- a/lib/core/viewmodels/dashboard_viewmodel.dart +++ b/lib/core/viewmodels/dashboard_viewmodel.dart @@ -21,6 +21,7 @@ import 'package:notredame/core/services/siren_flutter_service.dart'; import 'package:notredame/core/services/preferences_service.dart'; import 'package:notredame/core/services/analytics_service.dart'; import 'package:notredame/core/services/app_widget_service.dart'; +import 'package:notredame/core/services/remote_config_service.dart'; // MODEL import 'package:notredame/core/models/widget_models.dart'; @@ -36,9 +37,12 @@ class DashboardViewModel extends FutureViewModel> { static const String tag = "DashboardViewModel"; final SettingsManager _settingsManager = locator(); + final PreferencesService _preferencesService = locator(); final CourseRepository _courseRepository = locator(); final AnalyticsService _analyticsService = locator(); final AppWidgetService _appWidgetService = locator(); + final RemoteConfigService remoteConfigService = + locator(); /// All dashboard displayable cards Map _cards; @@ -58,6 +62,14 @@ class DashboardViewModel extends FutureViewModel> { /// Numbers of days elapsed and total number of days of the current session List _sessionDays = [0, 0]; + /// Message to display in case of urgent/important broadcast need (Firebase + /// remote config), and the associated card title + String broadcastMessage = ""; + String broadcastTitle = ""; + String broadcastColor = ""; + String broadcastUrl = ""; + String broadcastType = ""; + /// Get progress of the session double get progress => _progress; @@ -178,6 +190,8 @@ class DashboardViewModel extends FutureViewModel> { _cards = dashboard; + await checkForBroadcastChange(); + getCardsToDisplay(); // load data for both grade cards & grades home screen widget @@ -189,6 +203,7 @@ class DashboardViewModel extends FutureViewModel> { Future loadDataAndUpdateWidget() async { return Future.wait([ + futureToRunBroadcast(), futureToRunGrades(), futureToRunSessionProgressBar(), futureToRunSchedule() @@ -282,13 +297,13 @@ class DashboardViewModel extends FutureViewModel> { void setAllCardsVisible() { _cards.updateAll((key, value) { _settingsManager - .setInt(key, key.index - PreferencesFlag.aboutUsCard.index) + .setInt(key, key.index - PreferencesFlag.broadcastCard.index) .then((value) { if (!value) { Fluttertoast.showToast(msg: _appIntl.error); } }); - return key.index - PreferencesFlag.aboutUsCard.index; + return key.index - PreferencesFlag.broadcastCard.index; }); getCardsToDisplay(); @@ -316,6 +331,27 @@ class DashboardViewModel extends FutureViewModel> { _analyticsService.logEvent(tag, "Restoring cards"); } + Future checkForBroadcastChange() async { + final broadcastChange = + await _preferencesService.getString(PreferencesFlag.broadcastChange) ?? + ""; + if (broadcastChange != remoteConfigService.dashboardMessageEn) { + // Update pref + _preferencesService.setString(PreferencesFlag.broadcastChange, + remoteConfigService.dashboardMessageEn); + if (_cards[PreferencesFlag.broadcastCard] < 0) { + _cards.updateAll((key, value) { + if (value >= 0) { + return value + 1; + } else { + return value; + } + }); + _cards[PreferencesFlag.broadcastCard] = 0; + } + } + } + Future> futureToRunSessionProgressBar() async { String progressBarText = await _settingsManager.getString(PreferencesFlag.progressBarText); @@ -535,4 +571,29 @@ class DashboardViewModel extends FutureViewModel> { PreferencesFlag.updateAskedVersion, storeVersion.toString()); } } + + Future futureToRunBroadcast() async { + setBusyForObject(broadcastMessage, true); + setBusyForObject(broadcastTitle, true); + setBusyForObject(broadcastColor, true); + setBusyForObject(broadcastUrl, true); + setBusyForObject(broadcastType, true); + + if (_appIntl.localeName == "fr") { + broadcastMessage = remoteConfigService.dashboardMessageFr; + broadcastTitle = remoteConfigService.dashboardMessageTitleFr; + } else { + broadcastMessage = remoteConfigService.dashboardMessageEn; + broadcastTitle = remoteConfigService.dashboardMessageTitleEn; + } + broadcastColor = remoteConfigService.dashboardMsgColor; + broadcastUrl = remoteConfigService.dashboardMsgUrl; + broadcastType = remoteConfigService.dashboardMsgType; + + setBusyForObject(broadcastMessage, false); + setBusyForObject(broadcastTitle, false); + setBusyForObject(broadcastColor, false); + setBusyForObject(broadcastUrl, false); + setBusyForObject(broadcastType, false); + } } diff --git a/lib/ui/views/dashboard_view.dart b/lib/ui/views/dashboard_view.dart index b4e8608f2..ec86874da 100644 --- a/lib/ui/views/dashboard_view.dart +++ b/lib/ui/views/dashboard_view.dart @@ -1,4 +1,5 @@ // FLUTTER / DART / THIRD-PARTIES +import 'package:auto_size_text/auto_size_text.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -100,8 +101,16 @@ class _DashboardViewState extends State List _buildCards(DashboardViewModel model) { final List cards = List.empty(growable: true); + // always try to build broadcast cart so the user doesn't miss out on + // important info if they dismissed it previously + for (final PreferencesFlag element in model.cardsToDisplay) { switch (element) { + case PreferencesFlag.broadcastCard: + if (model.remoteConfigService.dashboardMessageActive) { + cards.add(_buildMessageBroadcastCard(model, element)); + } + break; case PreferencesFlag.aboutUsCard: cards.add(_buildAboutUsCard(model, element)); break; @@ -394,6 +403,79 @@ class _DashboardViewState extends State ]), ); + Widget _buildMessageBroadcastCard( + DashboardViewModel model, PreferencesFlag flag) { + final broadcastMsgColor = Color(int.parse(model.broadcastColor)); + final broadcastMsgType = model.broadcastType; + final broadcastMsgUrl = model.broadcastUrl; + return DismissibleCard( + key: UniqueKey(), + onDismissed: (DismissDirection direction) { + dismissCard(model, flag); + }, + isBusy: model.busy(model.broadcastMessage), + cardColor: broadcastMsgColor, + child: Padding( + padding: const EdgeInsets.fromLTRB(17, 10, 15, 20), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + // title row + Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Text(model.broadcastTitle, + style: Theme.of(context).primaryTextTheme.headline6), + ), + ), + Align( + alignment: Alignment.centerRight, + child: InkWell( + child: getBroadcastIcon(broadcastMsgType, broadcastMsgUrl), + ), + ), + ], + ), + // main text + AutoSizeText(model.broadcastMessage ?? "", + style: Theme.of(context).primaryTextTheme.bodyText2) + ]), + )); + } + + Widget getBroadcastIcon(String type, String url) { + switch (type) { + case "warning": + return const Icon( + Icons.warning_rounded, + color: AppTheme.lightThemeBackground, + size: 36.0, + ); + case "alert": + return const Icon( + Icons.error, + color: AppTheme.lightThemeBackground, + size: 36.0, + ); + case "link": + return IconButton( + onPressed: () { + Utils.launchURL(url, AppIntl.of(context)); + }, + icon: const Icon( + Icons.open_in_new, + color: AppTheme.lightThemeBackground, + size: 30.0, + ), + ); + } + return const Icon( + Icons.campaign, + color: AppTheme.lightThemeBackground, + size: 36.0, + ); + } + void dismissCard(DashboardViewModel model, PreferencesFlag flag) { model.hideCard(flag); } diff --git a/pubspec.yaml b/pubspec.yaml index 3f58d02db..30cf5dc5a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: The 4th generation of ÉTSMobile, the main gateway between the Éco # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 4.27.0+1 +version: 4.28.0+1 environment: sdk: ">=2.10.0 <3.0.0" diff --git a/test/managers/settings_manager_test.dart b/test/managers/settings_manager_test.dart index 08584dcf3..731eb5c5c 100644 --- a/test/managers/settings_manager_test.dart +++ b/test/managers/settings_manager_test.dart @@ -440,10 +440,11 @@ void main() { // Cards final Map expected = { - PreferencesFlag.aboutUsCard: 0, - PreferencesFlag.scheduleCard: 1, - PreferencesFlag.progressBarCard: 2, - PreferencesFlag.gradesCard: 3 + PreferencesFlag.broadcastCard: 0, + PreferencesFlag.aboutUsCard: 1, + PreferencesFlag.scheduleCard: 2, + PreferencesFlag.progressBarCard: 3, + PreferencesFlag.gradesCard: 4 }; expect( @@ -451,6 +452,8 @@ void main() { expected, ); + verify(preferencesService.getInt(PreferencesFlag.broadcastCard)) + .called(1); verify(preferencesService.getInt(PreferencesFlag.aboutUsCard)) .called(1); verify(preferencesService.getInt(PreferencesFlag.scheduleCard)) @@ -464,30 +467,35 @@ void main() { }); test("validate the loading of the cards", () async { + PreferencesServiceMock.stubGetInt( + preferencesService as PreferencesServiceMock, + PreferencesFlag.broadcastCard, + toReturn: 0); PreferencesServiceMock.stubGetInt( preferencesService as PreferencesServiceMock, PreferencesFlag.aboutUsCard, - // ignore: avoid_redundant_argument_values - toReturn: 1); + toReturn: 2); PreferencesServiceMock.stubGetInt( preferencesService as PreferencesServiceMock, PreferencesFlag.scheduleCard, - toReturn: 2); + toReturn: 3); PreferencesServiceMock.stubGetInt( preferencesService as PreferencesServiceMock, PreferencesFlag.progressBarCard, - toReturn: 0); + // ignore: avoid_redundant_argument_values + toReturn: 1); PreferencesServiceMock.stubGetInt( preferencesService as PreferencesServiceMock, PreferencesFlag.gradesCard, - toReturn: 3); + toReturn: 4); // Cards final Map expected = { - PreferencesFlag.aboutUsCard: 1, - PreferencesFlag.scheduleCard: 2, - PreferencesFlag.progressBarCard: 0, - PreferencesFlag.gradesCard: 3 + PreferencesFlag.broadcastCard: 0, + PreferencesFlag.aboutUsCard: 2, + PreferencesFlag.scheduleCard: 3, + PreferencesFlag.progressBarCard: 1, + PreferencesFlag.gradesCard: 4 }; expect( @@ -495,6 +503,8 @@ void main() { expected, ); + verify(preferencesService.getInt(PreferencesFlag.broadcastCard)) + .called(1); verify(preferencesService.getInt(PreferencesFlag.aboutUsCard)) .called(1); verify(preferencesService.getInt(PreferencesFlag.scheduleCard)) diff --git a/test/mock/services/remote_config_service_mock.dart b/test/mock/services/remote_config_service_mock.dart index c3a1792fb..567653208 100644 --- a/test/mock/services/remote_config_service_mock.dart +++ b/test/mock/services/remote_config_service_mock.dart @@ -9,4 +9,44 @@ class RemoteConfigServiceMock extends Mock implements RemoteConfigService { {bool toReturn = true}) { when(mock.scheduleListViewDefault).thenReturn(toReturn); } + + static void stubGetBroadcastEnabled(RemoteConfigServiceMock mock, + {bool toReturn = true}) { + when(mock.dashboardMessageActive).thenReturn(toReturn); + } + + static void stubGetBroadcastColor(RemoteConfigServiceMock mock, + {String toReturn = "0xffd48404"}) { + when(mock.dashboardMsgColor).thenReturn(toReturn); + } + + static void stubGetBroadcastTitleEn(RemoteConfigServiceMock mock, + {String toReturn = "TitleEn"}) { + when(mock.dashboardMessageTitleEn).thenReturn(toReturn); + } + + static void stubGetBroadcastTitleFr(RemoteConfigServiceMock mock, + {String toReturn = "TitleFr"}) { + when(mock.dashboardMessageTitleFr).thenReturn(toReturn); + } + + static void stubGetBroadcastEn(RemoteConfigServiceMock mock, + {String toReturn = "En"}) { + when(mock.dashboardMessageEn).thenReturn(toReturn); + } + + static void stubGetBroadcastFr(RemoteConfigServiceMock mock, + {String toReturn = "Fr"}) { + when(mock.dashboardMessageFr).thenReturn(toReturn); + } + + static void stubGetBroadcastType(RemoteConfigServiceMock mock, + {String toReturn = "info"}) { + when(mock.dashboardMsgType).thenReturn(toReturn); + } + + static void stubGetBroadcastUrl(RemoteConfigServiceMock mock, + {String toReturn = "https://clubapplets.ca/"}) { + when(mock.dashboardMsgUrl).thenReturn(toReturn); + } } diff --git a/test/ui/views/dashboard_view_test.dart b/test/ui/views/dashboard_view_test.dart index f84dbe8c2..66af76823 100644 --- a/test/ui/views/dashboard_view_test.dart +++ b/test/ui/views/dashboard_view_test.dart @@ -20,6 +20,9 @@ import 'package:notredame/ui/widgets/grade_button.dart'; // CONSTANTS import 'package:notredame/core/constants/preferences_flags.dart'; +// SERVICES +import 'package:notredame/core/services/remote_config_service.dart'; + // OTHERS import '../../helpers.dart'; @@ -27,10 +30,12 @@ import '../../helpers.dart'; import '../../mock/managers/course_repository_mock.dart'; import '../../mock/managers/settings_manager_mock.dart'; import '../../mock/services/in_app_review_service_mock.dart'; +import '../../mock/services/remote_config_service_mock.dart'; void main() { SettingsManager settingsManager; CourseRepository courseRepository; + RemoteConfigService remoteConfigService; AppIntl intl; InAppReviewServiceMock inAppReviewServiceMock; @@ -72,10 +77,11 @@ void main() { // Cards Map dashboard = { - PreferencesFlag.aboutUsCard: 0, - PreferencesFlag.scheduleCard: 1, - PreferencesFlag.progressBarCard: 2, - PreferencesFlag.gradesCard: 3 + PreferencesFlag.broadcastCard: 0, + PreferencesFlag.aboutUsCard: 1, + PreferencesFlag.scheduleCard: 2, + PreferencesFlag.progressBarCard: 3, + PreferencesFlag.gradesCard: 4 }; final numberOfCards = dashboard.entries.length; @@ -168,7 +174,7 @@ void main() { // Find schedule card in second position by its title return tester.firstWidget(find.descendant( - of: find.byType(Dismissible).at(1), + of: find.byType(Dismissible, skipOffstage: false).at(2), matching: find.byType(Text), )); } @@ -178,6 +184,7 @@ void main() { intl = await setupAppIntl(); settingsManager = setupSettingsManagerMock(); courseRepository = setupCourseRepositoryMock(); + remoteConfigService = setupRemoteConfigServiceMock(); setupNavigationServiceMock(); courseRepository = setupCourseRepositoryMock(); setupNetworkingServiceMock(); @@ -216,10 +223,30 @@ void main() { courseRepository as CourseRepositoryMock, fromCacheOnly: false); + RemoteConfigServiceMock.stubGetBroadcastEnabled( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastColor( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastEn( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastFr( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastTitleEn( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastTitleFr( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastType( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastUrl( + remoteConfigService as RemoteConfigServiceMock); + SettingsManagerMock.stubGetBool(settingsManager as SettingsManagerMock, PreferencesFlag.discoveryDashboard, toReturn: true); + SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, + PreferencesFlag.broadcastCard); + SettingsManagerMock.stubSetInt( settingsManager as SettingsManagerMock, PreferencesFlag.aboutUsCard); @@ -262,7 +289,8 @@ void main() { expect(restoreCardsIcon, findsOneWidget); // Find cards - expect(find.byType(Card), findsNWidgets(numberOfCards)); + expect(find.byType(Card, skipOffstage: false), + findsNWidgets(numberOfCards)); }); testWidgets('Has card aboutUs displayed properly', @@ -310,7 +338,7 @@ void main() { // Find three activities in the card expect( find.descendant( - of: find.byType(Dismissible), + of: find.byType(Dismissible, skipOffstage: false), matching: find.byType(CourseActivityTile), ), findsNWidgets(3)); @@ -338,7 +366,7 @@ void main() { // Find one activities in the card expect( find.descendant( - of: find.byType(Dismissible), + of: find.byType(Dismissible, skipOffstage: false), matching: find.byType(CourseActivityTile), ), findsNWidgets(1)); @@ -357,10 +385,10 @@ void main() { // Find no activity and no grade available text boxes expect( find.descendant( - of: find.byType(SizedBox), + of: find.byType(SizedBox, skipOffstage: false), matching: find.byType(Text), ), - findsNWidgets(2)); + findsNWidgets(1)); }); }); @@ -381,6 +409,9 @@ void main() { settingsManager as SettingsManagerMock, toReturn: dashboard); + SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, + PreferencesFlag.broadcastCard); + SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, PreferencesFlag.aboutUsCard); @@ -398,16 +429,18 @@ void main() { await tester.pumpAndSettle(); // Find Dismissible Cards - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); expect(find.text(intl.card_applets_title), findsOneWidget); // Swipe Dismissible aboutUs Card horizontally - await tester.drag( - find.byType(Dismissible).first, const Offset(1000.0, 0.0)); + await tester.drag(find.byType(Dismissible, skipOffstage: false).at(1), + const Offset(1000.0, 0.0)); // Check that the card is now absent from the view await tester.pumpAndSettle(); - expect(find.byType(Dismissible), findsNWidgets(numberOfCards - 1)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards - 1)); expect(find.text(intl.card_applets_title), findsNothing); // Tap the restoreCards button @@ -416,7 +449,8 @@ void main() { await tester.pumpAndSettle(); // Check that the card is now present in the view - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); expect(find.text(intl.card_applets_title), findsOneWidget); }); @@ -435,6 +469,9 @@ void main() { courseRepository as CourseRepositoryMock, fromCacheOnly: false); + SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, + PreferencesFlag.broadcastCard); + SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, PreferencesFlag.aboutUsCard); @@ -452,14 +489,15 @@ void main() { await tester.pumpAndSettle(); // Find Dismissible Cards - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); // Find aboutUs card expect(find.text(intl.card_applets_title), findsOneWidget); // Check that the aboutUs card is in the first position var text = tester.firstWidget(find.descendant( - of: find.byType(Dismissible).first, + of: find.byType(Dismissible, skipOffstage: false).at(1), matching: find.byType(Text), )); @@ -474,11 +512,12 @@ void main() { await tester.pumpAndSettle(); - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); // Check that the card is now in last position text = tester.firstWidget(find.descendant( - of: find.byType(Dismissible).last, + of: find.byType(Dismissible, skipOffstage: false).last, matching: find.byType(Text), )); expect((text as Text).data, intl.card_applets_title); @@ -489,11 +528,12 @@ void main() { await tester.pumpAndSettle(); text = tester.firstWidget(find.descendant( - of: find.byType(Dismissible).first, + of: find.byType(Dismissible, skipOffstage: false).at(1), matching: find.byType(Text), )); - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); // Check that the first card is now AboutUs expect((text as Text).data, intl.card_applets_title); @@ -510,18 +550,20 @@ void main() { await tester.pumpAndSettle(); // Find Dismissible Cards - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); expect(find.widgetWithText(Dismissible, intl.title_schedule), findsOneWidget); // Swipe Dismissible schedule Card horizontally - await tester.drag( - find.byType(Dismissible).at(1), const Offset(1000.0, 0.0)); + await tester.drag(find.byType(Dismissible, skipOffstage: false).at(2), + const Offset(1000.0, 0.0)); // Check that the card is now absent from the view await tester.pumpAndSettle(); - expect(find.byType(Dismissible), findsNWidgets(numberOfCards - 1)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards - 1)); expect(find.widgetWithText(Dismissible, intl.title_schedule), findsNothing); @@ -531,7 +573,8 @@ void main() { await tester.pumpAndSettle(); // Check that the card is now present in the view - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); expect(find.widgetWithText(Dismissible, intl.title_schedule), findsOneWidget); }); @@ -548,16 +591,18 @@ void main() { await tester.pumpAndSettle(); // Find grades card - final gradesCard = find.widgetWithText(Card, intl.grades_title); + final gradesCard = + find.widgetWithText(Card, intl.grades_title, skipOffstage: false); expect(gradesCard, findsOneWidget); // Find grades card Title - final gradesTitle = find.text(intl.grades_title); + final gradesTitle = find.text(intl.grades_title, skipOffstage: false); expect(gradesTitle, findsOneWidget); // Find empty grades card - final gradesEmptyTitle = - find.text(intl.grades_msg_no_grades.split("\n").first); + final gradesEmptyTitle = find.text( + intl.grades_msg_no_grades.split("\n").first, + skipOffstage: false); expect(gradesEmptyTitle, findsOneWidget); }); @@ -584,20 +629,24 @@ void main() { await tester.pumpAndSettle(); // Find grades card - final gradesCard = find.widgetWithText(Card, intl.grades_title); + final gradesCard = + find.widgetWithText(Card, intl.grades_title, skipOffstage: false); expect(gradesCard, findsOneWidget); // Find grades card Title - final gradesTitle = find.text(intl.grades_title); + final gradesTitle = find.text(intl.grades_title, skipOffstage: false); expect(gradesTitle, findsOneWidget); // Find grades buttons in the card - final gradesButtons = find.byType(GradeButton); + final gradesButtons = find.byType(GradeButton, skipOffstage: false); expect(gradesButtons, findsNWidgets(2)); }); testWidgets('gradesCard is dismissible and can be restored', (WidgetTester tester) async { + SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, + PreferencesFlag.broadcastCard); + SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, PreferencesFlag.aboutUsCard); @@ -618,11 +667,15 @@ void main() { await tester.pumpAndSettle(); // Find Dismissible Cards - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); - expect(find.text(intl.grades_title), findsOneWidget); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); + expect(find.text(intl.grades_title, skipOffstage: false), + findsOneWidget); // Swipe Dismissible grades Card horizontally - await tester.drag(find.widgetWithText(Dismissible, intl.grades_title), + await tester.drag( + find.widgetWithText(Dismissible, intl.grades_title, + skipOffstage: false), const Offset(1000.0, 0.0)); // Check that the card is now absent from the view @@ -636,8 +689,10 @@ void main() { await tester.pumpAndSettle(); // Check that the card is now present in the view - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); - expect(find.text(intl.grades_title), findsOneWidget); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); + expect(find.text(intl.grades_title, skipOffstage: false), + findsOneWidget); }); }); }); @@ -677,7 +732,8 @@ void main() { await tester.pumpAndSettle(); // Find Dismissible Cards - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); expect(find.text(intl.progress_bar_title), findsOneWidget); // Swipe Dismissible progress Card horizontally @@ -687,7 +743,8 @@ void main() { // Check that the card is now absent from the view await tester.pumpAndSettle(); - expect(find.byType(Dismissible), findsNWidgets(numberOfCards - 1)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards - 1)); expect(find.text(intl.progress_bar_title), findsNothing); // Tap the restoreCards button @@ -696,7 +753,8 @@ void main() { await tester.pumpAndSettle(); // Check that the card is now present in the view - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); expect(find.text(intl.progress_bar_title), findsOneWidget); }); @@ -712,7 +770,8 @@ void main() { await tester.pumpAndSettle(); // Find Dismissible Cards - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); // Find progressBar card expect(find.text(intl.progress_bar_title), findsOneWidget); @@ -734,7 +793,8 @@ void main() { await tester.pumpAndSettle(); - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); // Check that the card is now in last position text = tester.firstWidget(find.descendant( @@ -753,7 +813,8 @@ void main() { matching: find.byType(Text), )); - expect(find.byType(Dismissible), findsNWidgets(numberOfCards)); + expect(find.byType(Dismissible, skipOffstage: false), + findsNWidgets(numberOfCards)); // Check that the first card is now AboutUs expect((text as Text).data, intl.progress_bar_title); @@ -766,10 +827,14 @@ void main() { }); testWidgets("Applets Card", (WidgetTester tester) async { + RemoteConfigServiceMock.stubGetBroadcastEnabled( + remoteConfigService as RemoteConfigServiceMock, + toReturn: false); tester.binding.window.physicalSizeTestValue = const Size(800, 1410); - dashboard = { - PreferencesFlag.aboutUsCard: 0, + final Map dashboard = { + PreferencesFlag.broadcastCard: 0, + PreferencesFlag.aboutUsCard: 1, }; SettingsManagerMock.stubGetDashboard( @@ -785,6 +850,9 @@ void main() { }); testWidgets("Schedule card", (WidgetTester tester) async { + RemoteConfigServiceMock.stubGetBroadcastEnabled( + remoteConfigService as RemoteConfigServiceMock, + toReturn: false); tester.binding.window.physicalSizeTestValue = const Size(800, 1410); CourseRepositoryMock.stubCoursesActivities( @@ -797,7 +865,8 @@ void main() { fromCacheOnly: false); dashboard = { - PreferencesFlag.scheduleCard: 0, + PreferencesFlag.broadcastCard: 0, + PreferencesFlag.scheduleCard: 1, }; SettingsManagerMock.stubGetDashboard( @@ -812,10 +881,14 @@ void main() { matchesGoldenFile(goldenFilePath("dashboardView_scheduleCard_1"))); }); testWidgets("progressBar Card", (WidgetTester tester) async { + RemoteConfigServiceMock.stubGetBroadcastEnabled( + remoteConfigService as RemoteConfigServiceMock, + toReturn: false); tester.binding.window.physicalSizeTestValue = const Size(800, 1410); dashboard = { - PreferencesFlag.progressBarCard: 0, + PreferencesFlag.broadcastCard: 0, + PreferencesFlag.progressBarCard: 1, }; SettingsManagerMock.stubGetDashboard( diff --git a/test/viewmodels/dashboard_viewmodel_test.dart b/test/viewmodels/dashboard_viewmodel_test.dart index 9133a7ad7..e8cf55e2e 100644 --- a/test/viewmodels/dashboard_viewmodel_test.dart +++ b/test/viewmodels/dashboard_viewmodel_test.dart @@ -16,6 +16,7 @@ import 'package:ets_api_clients/models.dart'; // SERVICE import 'package:notredame/core/services/preferences_service.dart'; import 'package:notredame/core/services/analytics_service.dart'; +import 'package:notredame/core/services/remote_config_service.dart'; // VIEWMODEL import 'package:notredame/core/viewmodels/dashboard_viewmodel.dart'; @@ -28,12 +29,14 @@ import '../mock/managers/course_repository_mock.dart'; import '../mock/managers/settings_manager_mock.dart'; import '../mock/services/in_app_review_service_mock.dart'; import '../mock/services/preferences_service_mock.dart'; +import '../mock/services/remote_config_service_mock.dart'; void main() { PreferencesService preferenceService; SettingsManager settingsManager; DashboardViewModel viewModel; CourseRepository courseRepository; + RemoteConfigService remoteConfigService; PreferencesServiceMock preferencesServiceMock; InAppReviewServiceMock inAppReviewServiceMock; AnalyticsService analyticsService; @@ -163,23 +166,26 @@ void main() { // Cards final Map dashboard = { - PreferencesFlag.aboutUsCard: 0, - PreferencesFlag.scheduleCard: 1, - PreferencesFlag.progressBarCard: 2, + PreferencesFlag.broadcastCard: 0, + PreferencesFlag.aboutUsCard: 1, + PreferencesFlag.scheduleCard: 2, + PreferencesFlag.progressBarCard: 3, }; // Reorderered Cards final Map reorderedDashboard = { - PreferencesFlag.aboutUsCard: 1, - PreferencesFlag.scheduleCard: 2, + PreferencesFlag.broadcastCard: 1, + PreferencesFlag.aboutUsCard: 2, + PreferencesFlag.scheduleCard: 3, PreferencesFlag.progressBarCard: 0, }; // Reorderered Cards with hidden scheduleCard final Map hiddenCardDashboard = { - PreferencesFlag.aboutUsCard: 0, + PreferencesFlag.broadcastCard: 0, + PreferencesFlag.aboutUsCard: 1, PreferencesFlag.scheduleCard: -1, - PreferencesFlag.progressBarCard: 1, + PreferencesFlag.progressBarCard: 2, }; // Session @@ -202,6 +208,7 @@ void main() { setUp(() async { // Setting up mocks courseRepository = setupCourseRepositoryMock(); + remoteConfigService = setupRemoteConfigServiceMock(); settingsManager = setupSettingsManagerMock(); preferenceService = setupPreferencesServiceMock(); analyticsService = setupAnalyticsServiceMock(); @@ -229,6 +236,12 @@ void main() { settingsManager as SettingsManagerMock, toReturn: DateTime(2020)); + RemoteConfigServiceMock.stubGetBroadcastEnabled( + remoteConfigService as RemoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastEn( + remoteConfigService as RemoteConfigServiceMock, + toReturn: ""); + inAppReviewServiceMock = setupInAppReviewServiceMock() as InAppReviewServiceMock; }); @@ -376,6 +389,7 @@ void main() { await viewModel.futureToRun(); expect(viewModel.cards, dashboard); expect(viewModel.cardsToDisplay, [ + PreferencesFlag.broadcastCard, PreferencesFlag.aboutUsCard, PreferencesFlag.scheduleCard, PreferencesFlag.progressBarCard @@ -397,6 +411,9 @@ void main() { CourseRepositoryMock.stubGetCourses( courseRepository as CourseRepositoryMock, toReturn: courses); + SettingsManagerMock.stubGetDashboard( + settingsManager as SettingsManagerMock, + toReturn: dashboard); final now = DateTime.now(); SettingsManagerMock.stubDateTimeNow( settingsManager as SettingsManagerMock, @@ -429,6 +446,9 @@ void main() { CourseRepositoryMock.stubGetCourses( courseRepository as CourseRepositoryMock, toReturn: courses); + SettingsManagerMock.stubGetDashboard( + settingsManager as SettingsManagerMock, + toReturn: dashboard); final now = DateTime.now(); SettingsManagerMock.stubDateTimeNow( settingsManager as SettingsManagerMock, @@ -460,6 +480,9 @@ void main() { CourseRepositoryMock.stubGetCourses( courseRepository as CourseRepositoryMock, toReturn: courses); + SettingsManagerMock.stubGetDashboard( + settingsManager as SettingsManagerMock, + toReturn: dashboard); final now = DateTime.now(); SettingsManagerMock.stubDateTimeNow( settingsManager as SettingsManagerMock, @@ -492,6 +515,9 @@ void main() { CourseRepositoryMock.stubGetCourses( courseRepository as CourseRepositoryMock, toReturn: courses); + SettingsManagerMock.stubGetDashboard( + settingsManager as SettingsManagerMock, + toReturn: dashboard); final now = DateTime.now(); SettingsManagerMock.stubDateTimeNow( settingsManager as SettingsManagerMock, @@ -624,6 +650,9 @@ void main() { CourseRepositoryMock.stubGetCourses( courseRepository as CourseRepositoryMock); + PreferencesServiceMock.stubException( + preferenceService as PreferencesServiceMock, + PreferencesFlag.broadcastCard); PreferencesServiceMock.stubException( preferenceService as PreferencesServiceMock, PreferencesFlag.aboutUsCard); @@ -739,6 +768,8 @@ void main() { group("interact with cards - ", () { test("can hide a card and reset cards to default layout", () async { + SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, + PreferencesFlag.broadcastCard); SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, PreferencesFlag.aboutUsCard); SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, @@ -761,26 +792,32 @@ void main() { settingsManager.setInt(PreferencesFlag.scheduleCard, -1)); expect(viewModel.cards, hiddenCardDashboard); - expect(viewModel.cardsToDisplay, - [PreferencesFlag.aboutUsCard, PreferencesFlag.progressBarCard]); + expect(viewModel.cardsToDisplay, [ + PreferencesFlag.broadcastCard, + PreferencesFlag.aboutUsCard, + PreferencesFlag.progressBarCard + ]); verify(analyticsService.logEvent( "DashboardViewModel", "Deleting scheduleCard")); verify(settingsManager.setInt(PreferencesFlag.scheduleCard, -1)) .called(1); - verify(settingsManager.setInt(PreferencesFlag.aboutUsCard, 0)) + verify(settingsManager.setInt(PreferencesFlag.broadcastCard, 0)) .called(1); - verify(settingsManager.setInt(PreferencesFlag.progressBarCard, 1)) + verify(settingsManager.setInt(PreferencesFlag.aboutUsCard, 1)) + .called(1); + verify(settingsManager.setInt(PreferencesFlag.progressBarCard, 2)) .called(1); // Call the setter. viewModel.setAllCardsVisible(); await untilCalled( - settingsManager.setInt(PreferencesFlag.progressBarCard, 2)); + settingsManager.setInt(PreferencesFlag.progressBarCard, 3)); expect(viewModel.cards, dashboard); expect(viewModel.cardsToDisplay, [ + PreferencesFlag.broadcastCard, PreferencesFlag.aboutUsCard, PreferencesFlag.scheduleCard, PreferencesFlag.progressBarCard @@ -789,11 +826,13 @@ void main() { verify( analyticsService.logEvent("DashboardViewModel", "Restoring cards")); verify(settingsManager.getDashboard()).called(1); - verify(settingsManager.setInt(PreferencesFlag.aboutUsCard, 0)) + verify(settingsManager.setInt(PreferencesFlag.broadcastCard, 0)) .called(1); - verify(settingsManager.setInt(PreferencesFlag.scheduleCard, 1)) + verify(settingsManager.setInt(PreferencesFlag.aboutUsCard, 1)) .called(1); - verify(settingsManager.setInt(PreferencesFlag.progressBarCard, 2)) + verify(settingsManager.setInt(PreferencesFlag.scheduleCard, 2)) + .called(1); + verify(settingsManager.setInt(PreferencesFlag.progressBarCard, 3)) .called(1); verify(settingsManager.getString(PreferencesFlag.progressBarText)) .called(2); @@ -813,6 +852,8 @@ void main() { settingsManager as SettingsManagerMock, toReturn: dashboard); + SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, + PreferencesFlag.broadcastCard); SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, PreferencesFlag.aboutUsCard); SettingsManagerMock.stubSetInt(settingsManager as SettingsManagerMock, @@ -824,6 +865,7 @@ void main() { expect(viewModel.cards, dashboard); expect(viewModel.cardsToDisplay, [ + PreferencesFlag.broadcastCard, PreferencesFlag.aboutUsCard, PreferencesFlag.scheduleCard, PreferencesFlag.progressBarCard, @@ -838,6 +880,7 @@ void main() { expect(viewModel.cards, reorderedDashboard); expect(viewModel.cardsToDisplay, [ PreferencesFlag.progressBarCard, + PreferencesFlag.broadcastCard, PreferencesFlag.aboutUsCard, PreferencesFlag.scheduleCard ]); @@ -847,9 +890,11 @@ void main() { verify(settingsManager.getDashboard()).called(1); verify(settingsManager.setInt(PreferencesFlag.progressBarCard, 0)) .called(1); - verify(settingsManager.setInt(PreferencesFlag.aboutUsCard, 1)) + verify(settingsManager.setInt(PreferencesFlag.broadcastCard, 1)) .called(1); - verify(settingsManager.setInt(PreferencesFlag.scheduleCard, 2)) + verify(settingsManager.setInt(PreferencesFlag.aboutUsCard, 2)) + .called(1); + verify(settingsManager.setInt(PreferencesFlag.scheduleCard, 3)) .called(1); verify(settingsManager.getString(PreferencesFlag.progressBarText)) .called(1);