From 25a603474c8102660468ed20ce069581ab519644 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Wed, 2 Dec 2020 16:59:31 +0100 Subject: [PATCH] [MBL-14971][Parent] Manage remote config flags (#1128) * [MBL-14971][Parent] Added the Remote config screen refs: MBL-14971 affects: Parent release note: Added the Remote config params screen to modify remote configs. test plan: In debug mode, under settings a new menu is available 'Remote Config Params'. In the new screen you can update the local remote config values. --- .../remote_config_interactor.dart | 26 +++++++ .../remote_config/remote_config_screen.dart | 72 +++++++++++++++++++ .../screens/settings/settings_interactor.dart | 5 ++ .../lib/screens/settings/settings_screen.dart | 17 ++++- .../lib/utils/remote_config_utils.dart | 18 +++-- .../lib/utils/service_locator.dart | 2 + .../remote_config_interactor_test.dart | 31 ++++++++ .../remote_config_screen_test.dart | 50 +++++++++++++ .../settings/settings_interactor_test.dart | 14 ++++ .../settings/settings_screen_test.dart | 16 +++++ .../test/utils/remote_config_utils_test.dart | 11 +++ 11 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 apps/flutter_parent/lib/screens/remote_config/remote_config_interactor.dart create mode 100644 apps/flutter_parent/lib/screens/remote_config/remote_config_screen.dart create mode 100644 apps/flutter_parent/test/screens/remote_config_params/remote_config_interactor_test.dart create mode 100644 apps/flutter_parent/test/screens/remote_config_params/remote_config_screen_test.dart diff --git a/apps/flutter_parent/lib/screens/remote_config/remote_config_interactor.dart b/apps/flutter_parent/lib/screens/remote_config/remote_config_interactor.dart new file mode 100644 index 0000000000..39c6fa40df --- /dev/null +++ b/apps/flutter_parent/lib/screens/remote_config/remote_config_interactor.dart @@ -0,0 +1,26 @@ +// Copyright (C) 2020 - present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:flutter_parent/utils/remote_config_utils.dart'; + +class RemoteConfigInteractor { + + Map getRemoteConfigParams() { + return Map.fromIterable(RemoteConfigParams.values, key: (rc) => rc, value: (rc) => RemoteConfigUtils.getStringValue(rc)); + } + + void updateRemoteConfig(RemoteConfigParams rcKey, String value) { + RemoteConfigUtils.updateRemoteConfig(rcKey, value); + } +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/screens/remote_config/remote_config_screen.dart b/apps/flutter_parent/lib/screens/remote_config/remote_config_screen.dart new file mode 100644 index 0000000000..a0e0dc4858 --- /dev/null +++ b/apps/flutter_parent/lib/screens/remote_config/remote_config_screen.dart @@ -0,0 +1,72 @@ +// Copyright (C) 2020 - present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:flutter/material.dart'; +import 'package:flutter_parent/screens/remote_config/remote_config_interactor.dart'; +import 'package:flutter_parent/utils/remote_config_utils.dart'; +import 'package:flutter_parent/utils/service_locator.dart'; + +class RemoteConfigScreen extends StatefulWidget { + @override + _RemoteConfigScreenState createState() => _RemoteConfigScreenState(); +} + +class _RemoteConfigScreenState extends State { + RemoteConfigInteractor _interactor = locator(); + + Map _remoteConfig; + + @override + void initState() { + _remoteConfig = _interactor.getRemoteConfigParams(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Remote Config Params')), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: ListView( + key: Key('remote_config_params_list'), + children: _createListItems(_remoteConfig), + ), + ), + ); + } + + List _createListItems(Map remoteConfigParams) { + return remoteConfigParams.entries.map((e) => _createListItem(e)).toList(); + } + + Widget _createListItem(MapEntry entry) { + return Row(children: [ + Align( + child: Text(RemoteConfigUtils.getRemoteConfigName(entry.key)), + alignment: Alignment.centerLeft, + ), + Flexible( + child: Padding( + padding: EdgeInsets.only(left: 8.0), + child: TextField( + controller: TextEditingController()..text = entry.value, + onChanged: (value) => { + _interactor.updateRemoteConfig(entry.key, value) + }, + ), + )) + ]); + } +} diff --git a/apps/flutter_parent/lib/screens/settings/settings_interactor.dart b/apps/flutter_parent/lib/screens/settings/settings_interactor.dart index 04f5ec93ca..5eb14752d1 100644 --- a/apps/flutter_parent/lib/screens/settings/settings_interactor.dart +++ b/apps/flutter_parent/lib/screens/settings/settings_interactor.dart @@ -14,6 +14,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; +import 'package:flutter_parent/screens/remote_config/remote_config_screen.dart'; import 'package:flutter_parent/utils/debug_flags.dart'; import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_parent/utils/design/theme_transition/theme_transition_target.dart'; @@ -28,6 +29,10 @@ class SettingsInteractor { void routeToThemeViewer(BuildContext context) { locator().push(context, ThemeViewerScreen()); } + + void routeToRemoteConfig(BuildContext context) { + locator().push(context, RemoteConfigScreen()); + } void toggleDarkMode(context, anchorKey) { if (ParentTheme.of(context).isDarkMode) { diff --git a/apps/flutter_parent/lib/screens/settings/settings_screen.dart b/apps/flutter_parent/lib/screens/settings/settings_screen.dart index 8b6256c314..6efb9f81bc 100644 --- a/apps/flutter_parent/lib/screens/settings/settings_screen.dart +++ b/apps/flutter_parent/lib/screens/settings/settings_screen.dart @@ -55,6 +55,7 @@ class _SettingsScreenState extends State { if (ParentTheme.of(context).isDarkMode) _webViewDarkModeSwitch(context), _highContrastModeSwitch(context), if (_interactor.isDebugMode()) _themeViewer(context), + if (_interactor.isDebugMode()) _remoteConfigs(context) ], ), ), @@ -193,6 +194,19 @@ class _SettingsScreenState extends State { onTap: () => _interactor.routeToThemeViewer(context), ); + Widget _remoteConfigs(BuildContext context) => + ListTile( + key: Key('remote-configs'), + title: Row( + children: [ + _debugLabel(context), + SizedBox(width: 16), + Text('Remote Config Params') + ], + ), + onTap: () => _interactor.routeToRemoteConfig(context), + ); + Container _debugLabel(BuildContext context) { return Container( decoration: BoxDecoration( @@ -200,7 +214,8 @@ class _SettingsScreenState extends State { borderRadius: BorderRadius.circular(32), ), padding: const EdgeInsets.all(4), - child: Icon(Icons.bug_report, color: Theme.of(context).accentIconTheme.color, size: 16), + child: Icon(Icons.bug_report, + color: Theme.of(context).accentIconTheme.color, size: 16), ); } } diff --git a/apps/flutter_parent/lib/utils/remote_config_utils.dart b/apps/flutter_parent/lib/utils/remote_config_utils.dart index 092f58f6d0..864c48200e 100644 --- a/apps/flutter_parent/lib/utils/remote_config_utils.dart +++ b/apps/flutter_parent/lib/utils/remote_config_utils.dart @@ -50,7 +50,8 @@ class RemoteConfigUtils { */ @visibleForTesting static Future initializeExplicit(RemoteConfig remoteConfig) async { - if (_remoteConfig != null) throw StateError('double-initialization of RemoteConfigUtils'); + if (_remoteConfig != null) + throw StateError('double-initialization of RemoteConfigUtils'); _remoteConfig = remoteConfig; @@ -70,7 +71,7 @@ class RemoteConfigUtils { if (updated) { // If we actually fetched something, then store the fetched info into _prefs RemoteConfigParams.values.forEach((rc) { - String rcParamName = _getRemoteConfigName(rc); + String rcParamName = getRemoteConfigName(rc); String rcParamValue = _remoteConfig.getString(rcParamName); String rcPreferencesName = _getSharedPreferencesName(rc); print( @@ -82,7 +83,7 @@ class RemoteConfigUtils { // a local remote-config settings page, which is not supported at this time. print('RemoteConfigUtils.initialize(): No update'); RemoteConfigParams.values.forEach((rc) { - String rcParamName = _getRemoteConfigName(rc); + String rcParamName = getRemoteConfigName(rc); String rcPreferencesName = _getSharedPreferencesName(rc); String rcParamValue = _prefs.getString(rcPreferencesName); print( @@ -93,7 +94,8 @@ class RemoteConfigUtils { /** Fetch the value (in string form) of the specified RemoteConfigParams element. */ static String getStringValue(RemoteConfigParams rcParam) { - if (_remoteConfig == null) throw StateError('RemoteConfigUtils not yet initialized'); + if (_remoteConfig == null) + throw StateError('RemoteConfigUtils not yet initialized'); var rcDefault = _getRemoteConfigDefaultValue(rcParam); var rcPreferencesName = _getSharedPreferencesName(rcParam); @@ -111,7 +113,7 @@ class RemoteConfigUtils { // Switch statements are required to cover all possible cases, so if we add // a new element in RemoveConfigParams, we'll be forced to add handling for // it here. - static String _getRemoteConfigName(RemoteConfigParams rcParam) { + static String getRemoteConfigName(RemoteConfigParams rcParam) { switch (rcParam) { case RemoteConfigParams.TEST_STRING: return 'test_string'; @@ -141,6 +143,10 @@ class RemoteConfigUtils { // that corresponds to rcParam. Just prepends an 'rc_' to the // remote config name for rcParam. static String _getSharedPreferencesName(RemoteConfigParams rcParam) { - return 'rc_${_getRemoteConfigName(rcParam)}'; + return 'rc_${getRemoteConfigName(rcParam)}'; + } + + static void updateRemoteConfig(RemoteConfigParams rcParam, String newValue) { + _prefs.setString(_getSharedPreferencesName(rcParam), newValue); } } diff --git a/apps/flutter_parent/lib/utils/service_locator.dart b/apps/flutter_parent/lib/utils/service_locator.dart index d66b6f5736..1588671191 100644 --- a/apps/flutter_parent/lib/utils/service_locator.dart +++ b/apps/flutter_parent/lib/utils/service_locator.dart @@ -58,6 +58,7 @@ import 'package:flutter_parent/screens/pairing/pairing_interactor.dart'; import 'package:flutter_parent/screens/pairing/pairing_util.dart'; import 'package:flutter_parent/screens/qr_login/qr_login_tutorial_screen_interactor.dart'; import 'package:flutter_parent/screens/qr_login/qr_login_util.dart'; +import 'package:flutter_parent/screens/remote_config/remote_config_interactor.dart'; import 'package:flutter_parent/screens/settings/settings_interactor.dart'; import 'package:flutter_parent/screens/splash/splash_screen_interactor.dart'; import 'package:flutter_parent/screens/web_login/web_login_interactor.dart'; @@ -137,6 +138,7 @@ void setupLocator() { locator.registerFactory(() => MasqueradeScreenInteractor()); locator.registerFactory(() => PairingInteractor()); locator.registerFactory(() => QRLoginTutorialScreenInteractor()); + locator.registerFactory(() => RemoteConfigInteractor()); locator.registerFactory(() => SettingsInteractor()); locator.registerFactory(() => SplashScreenInteractor()); locator.registerFactory(() => StudentColorPickerInteractor()); diff --git a/apps/flutter_parent/test/screens/remote_config_params/remote_config_interactor_test.dart b/apps/flutter_parent/test/screens/remote_config_params/remote_config_interactor_test.dart new file mode 100644 index 0000000000..04be99ec7a --- /dev/null +++ b/apps/flutter_parent/test/screens/remote_config_params/remote_config_interactor_test.dart @@ -0,0 +1,31 @@ +// Copyright (C) 2020 - present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +import 'package:flutter/widgets.dart'; +import 'package:flutter_parent/screens/remote_config/remote_config_interactor.dart'; +import 'package:flutter_parent/utils/remote_config_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../utils/platform_config.dart'; +import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.dart'; + +void main() { + test('getRemoteConfigParams returns correct map', () async { + final mockRemoteConfig = setupMockRemoteConfig(valueSettings: {'test_string': 'fetched value', 'mobile_verify_beta_enabled' : 'false'}); + await setupPlatformChannels( + config: PlatformConfig(initRemoteConfig: mockRemoteConfig)); + expect(RemoteConfigInteractor().getRemoteConfigParams(), equals({RemoteConfigParams.TEST_STRING: 'fetched value', RemoteConfigParams.MOBILE_VERIFY_BETA_ENABLED: 'false'})); + }); +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/remote_config_params/remote_config_screen_test.dart b/apps/flutter_parent/test/screens/remote_config_params/remote_config_screen_test.dart new file mode 100644 index 0000000000..0c9756e102 --- /dev/null +++ b/apps/flutter_parent/test/screens/remote_config_params/remote_config_screen_test.dart @@ -0,0 +1,50 @@ +// Copyright (C) 2020 - present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:flutter/widgets.dart'; +import 'package:flutter_parent/screens/remote_config/remote_config_interactor.dart'; +import 'package:flutter_parent/screens/remote_config/remote_config_screen.dart'; +import 'package:flutter_parent/utils/remote_config_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../utils/accessibility_utils.dart'; +import '../../utils/platform_config.dart'; +import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.dart'; + +void main() { + + testWidgetsWithAccessibilityChecks('Shows the correct list', (tester) async { + var interactor = _MockInteractor(); + setupTestLocator((locator) => locator.registerFactory(() => interactor)); + + Map remoteConfigs = { + RemoteConfigParams.MOBILE_VERIFY_BETA_ENABLED: 'false', + RemoteConfigParams.TEST_STRING: 'fetched value' + }; + + when(interactor.getRemoteConfigParams()).thenReturn(remoteConfigs); + + await tester.pumpWidget(TestApp(RemoteConfigScreen())); + await tester.pumpAndSettle(); + + expect(find.text(RemoteConfigUtils.getRemoteConfigName(RemoteConfigParams.MOBILE_VERIFY_BETA_ENABLED)), findsOneWidget); + expect(find.text('false'), findsOneWidget); + expect(find.text(RemoteConfigUtils.getRemoteConfigName(RemoteConfigParams.TEST_STRING)), findsOneWidget); + expect(find.text('fetched value'), findsOneWidget); + }); +} + +class _MockInteractor extends Mock implements RemoteConfigInteractor {} diff --git a/apps/flutter_parent/test/screens/settings/settings_interactor_test.dart b/apps/flutter_parent/test/screens/settings/settings_interactor_test.dart index 8dcd6df7f1..a043798e18 100644 --- a/apps/flutter_parent/test/screens/settings/settings_interactor_test.dart +++ b/apps/flutter_parent/test/screens/settings/settings_interactor_test.dart @@ -15,6 +15,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; +import 'package:flutter_parent/screens/remote_config/remote_config_screen.dart'; import 'package:flutter_parent/screens/settings/settings_interactor.dart'; import 'package:flutter_parent/screens/theme_viewer_screen.dart'; import 'package:flutter_parent/utils/design/parent_theme.dart'; @@ -44,6 +45,19 @@ void main() { expect(screen, isA()); }); + test('routeToRemoteConfig call through to navigator', () { + var nav = _MockNav(); + setupTestLocator((locator) { + locator.registerLazySingleton(() => nav); + }); + + var context = _MockContext(); + SettingsInteractor().routeToRemoteConfig(context); + + var screen = verify(nav.push(context, captureAny)).captured[0]; + expect(screen, isA()); + }); + testNonWidgetsWithContext('toggle dark mode sets dark mode to true', (tester) async { await setupPlatformChannels(); final analytics = _MockAnalytics(); diff --git a/apps/flutter_parent/test/screens/settings/settings_screen_test.dart b/apps/flutter_parent/test/screens/settings/settings_screen_test.dart index cf4f849718..59fadba746 100644 --- a/apps/flutter_parent/test/screens/settings/settings_screen_test.dart +++ b/apps/flutter_parent/test/screens/settings/settings_screen_test.dart @@ -32,6 +32,7 @@ void main() { AppLocalizations l10n = AppLocalizations(); themeViewerButton() => find.byKey(Key('theme-viewer')); + remoteConfigsButton() => find.byKey(Key('remote-configs')); darkModeButton() => find.byKey(Key('dark-mode-button')); lightModeButton() => find.byKey(Key('light-mode-button')); hcToggle() => find.text(l10n.highContrastLabel); @@ -64,6 +65,14 @@ void main() { expect(themeViewerButton(), findsOneWidget); }); + testWidgetsWithAccessibilityChecks('Displays remote config params button in debug mode', (tester) async { + await tester.pumpWidget(TestApp(SettingsScreen())); + await tester.pumpAndSettle(); + await ensureVisibleByScrolling(themeViewerButton(), tester, scrollFrom: ScreenVerticalLocation.MID_BOTTOM); + await tester.pumpAndSettle(); + expect(remoteConfigsButton(), findsOneWidget); + }); + testWidgetsWithAccessibilityChecks('Hides theme viewer button in non-debug mode', (tester) async { when(interactor.isDebugMode()).thenReturn(false); await tester.pumpWidget(TestApp(SettingsScreen())); @@ -71,6 +80,13 @@ void main() { expect(themeViewerButton(), findsNothing); }); + testWidgetsWithAccessibilityChecks('Hide remote config params button in non-debug mode', (tester) async { + when(interactor.isDebugMode()).thenReturn(false); + await tester.pumpWidget(TestApp(SettingsScreen())); + await tester.pumpAndSettle(); + expect(remoteConfigsButton(), findsNothing); + }); + testWidgetsWithAccessibilityChecks( '(In light mode) Dark mode button is enabled, light mode button is disabled', (tester) async { diff --git a/apps/flutter_parent/test/utils/remote_config_utils_test.dart b/apps/flutter_parent/test/utils/remote_config_utils_test.dart index db7f5a78e1..069c689caa 100644 --- a/apps/flutter_parent/test/utils/remote_config_utils_test.dart +++ b/apps/flutter_parent/test/utils/remote_config_utils_test.dart @@ -70,4 +70,15 @@ void main() { expect(RemoteConfigUtils.getStringValue(RemoteConfigParams.TEST_STRING), 'fetched value'); }); + + test('update cached value', () async { + final mockRemoteConfig = setupMockRemoteConfig(); + final platformConfig = + PlatformConfig(mockPrefs: {'rc_test_string': 'cached value'}, initRemoteConfig: mockRemoteConfig); + await setupPlatformChannels(config: platformConfig); + + RemoteConfigUtils.updateRemoteConfig(RemoteConfigParams.TEST_STRING, 'updated cached value'); + + expect(RemoteConfigUtils.getStringValue(RemoteConfigParams.TEST_STRING), 'updated cached value'); + }); }