From d79682a01115f32404ff12c9667003b3684eb3fc Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 5 Dec 2022 19:15:00 +0100 Subject: [PATCH] Added test for login page --- android/app/build.gradle | 5 + .../login/login_integration_test.dart | 17 -- integration_test/login_integration_test.dart | 235 ++++++++++++++++++ integration_test/src/framework.dart | 41 +++ lib/core/bloc/connectivity_cubit.dart | 22 +- .../authentication.interceptor.dart | 2 + .../response_conversion.interceptor.dart | 2 + lib/core/logic/timeout_client.dart | 2 + .../service/connectivity_status.service.dart | 2 +- lib/core/store/local_vault.dart | 23 +- lib/di_initializer.dart | 4 +- lib/di_modules.dart | 37 +++ lib/di_test_mocks.dart | 69 +++++ lib/features/login/view/login_page.dart | 1 + .../client_certificate_form_field.dart | 2 + .../widgets/server_address_form_field.dart | 1 + .../widgets/user_credentials_form_field.dart | 2 + lib/main.dart | 23 +- pubspec.lock | 2 +- pubspec.yaml | 2 +- 20 files changed, 444 insertions(+), 50 deletions(-) delete mode 100644 integration_test/login/login_integration_test.dart create mode 100644 integration_test/login_integration_test.dart create mode 100644 integration_test/src/framework.dart create mode 100644 lib/di_test_mocks.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index e058376..d5ceef6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -53,6 +53,7 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { @@ -77,4 +78,8 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } diff --git a/integration_test/login/login_integration_test.dart b/integration_test/login/login_integration_test.dart deleted file mode 100644 index 9ad8155..0000000 --- a/integration_test/login/login_integration_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('screenshot', (WidgetTester tester) async { - // Build the app. - - // This is required prior to taking the screenshot (Android only). - await binding.convertFlutterSurfaceToImage(); - - // Trigger a frame. - await tester.pumpAndSettle(); - await binding.takeScreenshot('screenshot-1'); - }); -} diff --git a/integration_test/login_integration_test.dart b/integration_test/login_integration_test.dart new file mode 100644 index 0000000..a69f092 --- /dev/null +++ b/integration_test/login_integration_test.dart @@ -0,0 +1,235 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/service/connectivity_status.service.dart'; +import 'package:paperless_mobile/core/store/local_vault.dart'; +import 'package:paperless_mobile/di_initializer.dart'; +import 'package:paperless_mobile/di_test_mocks.mocks.dart'; +import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; +import 'package:paperless_mobile/features/settings/model/view_type.dart'; + +import 'src/framework.dart'; + +void main() async { + final t = await initializeTestingFramework(languageCode: 'de'); + + const testServerUrl = 'https://example.com'; + const testUsername = 'user'; + const testPassword = 'pass'; + + final serverAddressField = find.byKey(const ValueKey('login-server-address')); + final usernameField = find.byKey(const ValueKey('login-username')); + final passwordField = find.byKey(const ValueKey('login-password')); + final loginBtn = find.byKey(const ValueKey('login-login-button')); + + testWidgets('Test successful login flow', (WidgetTester tester) async { + await initAndLaunchTestApp(tester, () async { + // Initialize dat for mocked classes + when((getIt()).connectivityChanges()) + .thenAnswer((i) => Stream.value(true)); + when((getIt() as MockLocalVault) + .loadAuthenticationInformation()) + .thenAnswer((realInvocation) async => null); + when((getIt() as MockLocalVault).loadApplicationSettings()) + .thenAnswer((realInvocation) async => ApplicationSettingsState( + preferredLocaleSubtag: 'en', + preferredThemeMode: ThemeMode.light, + isLocalAuthenticationEnabled: false, + preferredViewType: ViewType.list, + showInboxOnStartup: false, + )); + when(getIt().login( + username: testUsername, + password: testPassword, + serverUrl: testServerUrl, + )).thenAnswer((i) => Future.value("eyTestToken")); + + await getIt().initialize(); + await getIt().initialize(); + await getIt().initialize(); + }); + + // Mocked classes + + await t.binding.waitUntilFirstFrameRasterized; + await tester.pumpAndSettle(); + + await tester.enterText(serverAddressField, testServerUrl); + await tester.pumpAndSettle(); + + await tester.enterText(usernameField, testUsername); + await tester.pumpAndSettle(); + + await tester.enterText(passwordField, testPassword); + + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + + await tester.tap(loginBtn); + + verify(getIt().login( + username: testUsername, + password: testPassword, + serverUrl: testServerUrl, + )).called(1); + }); + + testWidgets('Test login validation missing password', + (WidgetTester tester) async { + await initAndLaunchTestApp(tester, () async { + when((getIt() as MockConnectivityStatusService) + .connectivityChanges()) + .thenAnswer((i) => Stream.value(true)); + when((getIt() as MockLocalVault) + .loadAuthenticationInformation()) + .thenAnswer((realInvocation) async => null); + + when((getIt() as MockLocalVault).loadApplicationSettings()) + .thenAnswer((realInvocation) async => ApplicationSettingsState( + preferredLocaleSubtag: 'en', + preferredThemeMode: ThemeMode.light, + isLocalAuthenticationEnabled: false, + preferredViewType: ViewType.list, + showInboxOnStartup: false, + )); + + await getIt().initialize(); + await getIt().initialize(); + await getIt().initialize(); + }); + // Mocked classes + + // Initialize dat for mocked classes + + await t.binding.waitUntilFirstFrameRasterized; + await tester.pumpAndSettle(); + + await tester.enterText(serverAddressField, testServerUrl); + await tester.pumpAndSettle(); + + await tester.enterText(usernameField, testUsername); + await tester.pumpAndSettle(); + + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + + await tester.tap(loginBtn); + await tester.pumpAndSettle(); + + verifyNever( + (getIt() as MockPaperlessAuthenticationApi) + .login( + username: testUsername, + password: testPassword, + serverUrl: testServerUrl, + )); + expect( + find.textContaining(t.translations.loginPagePasswordValidatorMessageText), + findsOneWidget, + ); + }); + + testWidgets('Test login validation missing username', + (WidgetTester tester) async { + await initAndLaunchTestApp(tester, () async { + when((getIt() as MockConnectivityStatusService) + .connectivityChanges()) + .thenAnswer((i) => Stream.value(true)); + when((getIt() as MockLocalVault) + .loadAuthenticationInformation()) + .thenAnswer((realInvocation) async => null); + when((getIt() as MockLocalVault).loadApplicationSettings()) + .thenAnswer((realInvocation) async => ApplicationSettingsState( + preferredLocaleSubtag: 'en', + preferredThemeMode: ThemeMode.light, + isLocalAuthenticationEnabled: false, + preferredViewType: ViewType.list, + showInboxOnStartup: false, + )); + await getIt().initialize(); + await getIt().initialize(); + await getIt().initialize(); + }); + + await t.binding.waitUntilFirstFrameRasterized; + await tester.pumpAndSettle(); + + await tester.enterText(serverAddressField, testServerUrl); + await tester.pumpAndSettle(); + + await tester.enterText(passwordField, testPassword); + await tester.pumpAndSettle(); + + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + + await tester.tap(loginBtn); + await tester.pumpAndSettle(); + + verifyNever( + (getIt() as MockPaperlessAuthenticationApi) + .login( + username: testUsername, + password: testPassword, + serverUrl: testServerUrl, + )); + expect( + find.textContaining(t.translations.loginPageUsernameValidatorMessageText), + findsOneWidget, + ); + }); + + testWidgets('Test login validation missing server address', + (WidgetTester tester) async { + initAndLaunchTestApp(tester, () async { + when((getIt()).connectivityChanges()) + .thenAnswer((i) => Stream.value(true)); + + when((getIt()).loadAuthenticationInformation()) + .thenAnswer((realInvocation) async => null); + + when((getIt()).loadApplicationSettings()) + .thenAnswer((realInvocation) async => ApplicationSettingsState( + preferredLocaleSubtag: 'en', + preferredThemeMode: ThemeMode.light, + isLocalAuthenticationEnabled: false, + preferredViewType: ViewType.list, + showInboxOnStartup: false, + )); + + await getIt().initialize(); + await getIt().initialize(); + await getIt().initialize(); + }); + + await t.binding.waitUntilFirstFrameRasterized; + await tester.pumpAndSettle(); + + await tester.enterText(usernameField, testUsername); + await tester.pumpAndSettle(); + + await tester.enterText(passwordField, testPassword); + await tester.pumpAndSettle(); + + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + + await tester.tap(loginBtn); + await tester.pumpAndSettle(); + + verifyNever(getIt().login( + username: testUsername, + password: testPassword, + serverUrl: testServerUrl, + )); + expect( + find.textContaining( + t.translations.loginPageServerUrlValidatorMessageText), + findsOneWidget, + ); + }); +} diff --git a/integration_test/src/framework.dart b/integration_test/src/framework.dart new file mode 100644 index 0000000..06717a8 --- /dev/null +++ b/integration_test/src/framework.dart @@ -0,0 +1,41 @@ +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:paperless_mobile/di_initializer.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/main.dart'; + +Future initializeTestingFramework( + {String languageCode = 'en'}) async { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + configureDependencies('test'); + final translations = await S.load( + Locale.fromSubtags( + languageCode: languageCode, + ), + ); + return TestingFrameworkVariables( + binding: binding, + translations: translations, + ); +} + +class TestingFrameworkVariables { + final IntegrationTestWidgetsFlutterBinding binding; + final S translations; + + TestingFrameworkVariables({ + required this.binding, + required this.translations, + }); +} + +Future initAndLaunchTestApp( + WidgetTester tester, + Future Function() initializationCallback, +) async { + await initializationCallback(); + runApp(const PaperlessMobileEntrypoint()); +} diff --git a/lib/core/bloc/connectivity_cubit.dart b/lib/core/bloc/connectivity_cubit.dart index 5535ee6..b437d90 100644 --- a/lib/core/bloc/connectivity_cubit.dart +++ b/lib/core/bloc/connectivity_cubit.dart @@ -7,28 +7,30 @@ import 'package:injectable/injectable.dart'; @singleton class ConnectivityCubit extends Cubit { final ConnectivityStatusService connectivityStatusService; - late final StreamSubscription _sub; + StreamSubscription? _sub; ConnectivityCubit(this.connectivityStatusService) : super(ConnectivityState.undefined); Future initialize() async { - final bool isConnected = - await connectivityStatusService.isConnectedToInternet(); - emit(isConnected - ? ConnectivityState.connected - : ConnectivityState.notConnected); - _sub = - connectivityStatusService.connectivityChanges().listen((isConnected) { + if (_sub == null) { + final bool isConnected = + await connectivityStatusService.isConnectedToInternet(); emit(isConnected ? ConnectivityState.connected : ConnectivityState.notConnected); - }); + _sub = + connectivityStatusService.connectivityChanges().listen((isConnected) { + emit(isConnected + ? ConnectivityState.connected + : ConnectivityState.notConnected); + }); + } } @override Future close() { - _sub.cancel(); + _sub?.cancel(); return super.close(); } } diff --git a/lib/core/interceptor/authentication.interceptor.dart b/lib/core/interceptor/authentication.interceptor.dart index 92e3c9a..c9d0b59 100644 --- a/lib/core/interceptor/authentication.interceptor.dart +++ b/lib/core/interceptor/authentication.interceptor.dart @@ -7,6 +7,8 @@ import 'package:http_interceptor/http_interceptor.dart'; import 'package:injectable/injectable.dart'; @injectable +@dev +@prod class AuthenticationInterceptor implements InterceptorContract { final LocalVault _localVault; AuthenticationInterceptor(this._localVault); diff --git a/lib/core/interceptor/response_conversion.interceptor.dart b/lib/core/interceptor/response_conversion.interceptor.dart index 29b4020..44c9c3e 100644 --- a/lib/core/interceptor/response_conversion.interceptor.dart +++ b/lib/core/interceptor/response_conversion.interceptor.dart @@ -5,6 +5,8 @@ import 'package:injectable/injectable.dart'; const interceptedRoutes = ['thumb/']; @injectable +@dev +@prod class ResponseConversionInterceptor implements InterceptorContract { @override Future interceptRequest({required BaseRequest request}) async => diff --git a/lib/core/logic/timeout_client.dart b/lib/core/logic/timeout_client.dart index 4d8173d..e6a2345 100644 --- a/lib/core/logic/timeout_client.dart +++ b/lib/core/logic/timeout_client.dart @@ -13,6 +13,8 @@ import 'package:injectable/injectable.dart'; /// Convenience class which handles timeout errors. /// @Injectable(as: BaseClient) +@dev +@prod @Named("timeoutClient") class TimeoutClient implements BaseClient { final ConnectivityStatusService connectivityStatusService; diff --git a/lib/core/service/connectivity_status.service.dart b/lib/core/service/connectivity_status.service.dart index 66a5875..c8dacee 100644 --- a/lib/core/service/connectivity_status.service.dart +++ b/lib/core/service/connectivity_status.service.dart @@ -9,7 +9,7 @@ abstract class ConnectivityStatusService { Stream connectivityChanges(); } -@Injectable(as: ConnectivityStatusService) +@Injectable(as: ConnectivityStatusService, env: ['prod', 'dev']) class ConnectivityStatusServiceImpl implements ConnectivityStatusService { final Connectivity connectivity; diff --git a/lib/core/store/local_vault.dart b/lib/core/store/local_vault.dart index b8adcb9..beb3f1f 100644 --- a/lib/core/store/local_vault.dart +++ b/lib/core/store/local_vault.dart @@ -8,15 +8,27 @@ import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:injectable/injectable.dart'; -@singleton -class LocalVault { +abstract class LocalVault { + Future storeAuthenticationInformation(AuthenticationInformation auth); + Future loadAuthenticationInformation(); + Future loadCertificate(); + Future storeApplicationSettings(ApplicationSettingsState settings); + Future loadApplicationSettings(); + Future clear(); +} + +@Injectable(as: LocalVault) +@prod +@dev +class LocalVaultImpl implements LocalVault { static const applicationSettingsKey = "applicationSettings"; static const authenticationKey = "authentication"; final EncryptedSharedPreferences sharedPreferences; - LocalVault(this.sharedPreferences); + LocalVaultImpl(this.sharedPreferences); + @override Future storeAuthenticationInformation( AuthenticationInformation auth, ) async { @@ -26,6 +38,7 @@ class LocalVault { ); } + @override Future loadAuthenticationInformation() async { if ((await sharedPreferences.getString(authenticationKey)).isEmpty) { return null; @@ -35,11 +48,13 @@ class LocalVault { ); } + @override Future loadCertificate() async { return loadAuthenticationInformation() .then((value) => value?.clientCertificate); } + @override Future storeApplicationSettings(ApplicationSettingsState settings) { return sharedPreferences.setString( applicationSettingsKey, @@ -47,6 +62,7 @@ class LocalVault { ); } + @override Future loadApplicationSettings() async { final settings = await sharedPreferences.getString(applicationSettingsKey); if (settings.isEmpty) { @@ -58,6 +74,7 @@ class LocalVault { ); } + @override Future clear() { return sharedPreferences.clear(); } diff --git a/lib/di_initializer.dart b/lib/di_initializer.dart index 0652c12..f0e4a3f 100644 --- a/lib/di_initializer.dart +++ b/lib/di_initializer.dart @@ -7,13 +7,13 @@ import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; final getIt = GetIt.instance..allowReassignment; - @InjectableInit( initializerName: r'$initGetIt', // default preferRelativeImports: true, // default asExtension: false, // default ) -void configureDependencies() => $initGetIt(getIt); +void configureDependencies(String environment) => + $initGetIt(getIt, environment: environment); /// /// Registers new security context, which will be used by the HttpClient, see [RegisterModule]. diff --git a/lib/di_modules.dart b/lib/di_modules.dart index aa91789..c688c0f 100644 --- a/lib/di_modules.dart +++ b/lib/di_modules.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/interceptor/authentication.interceptor.dart'; @@ -16,18 +17,33 @@ import 'package:local_auth/local_auth.dart'; @module abstract class RegisterModule { @singleton + @dev + @prod LocalAuthentication get localAuthentication => LocalAuthentication(); + @singleton + @dev + @prod EncryptedSharedPreferences get encryptedSharedPreferences => EncryptedSharedPreferences(); + @singleton + @dev + @prod + @test SecurityContext get securityContext => SecurityContext(); + @singleton + @dev + @prod Connectivity get connectivity => Connectivity(); /// /// Factory method creating an [HttpClient] with the currently registered [SecurityContext]. /// + @injectable + @dev + @prod HttpClient getHttpClient(SecurityContext securityContext) => HttpClient(context: securityContext) ..connectionTimeout = const Duration(seconds: 10); @@ -35,6 +51,9 @@ abstract class RegisterModule { /// /// Factory method creating a [InterceptedClient] on top of the currently registered [HttpClient]. /// + @injectable + @dev + @prod BaseClient getBaseClient( AuthenticationInterceptor authInterceptor, ResponseConversionInterceptor responseConversionInterceptor, @@ -50,28 +69,46 @@ abstract class RegisterModule { client: IOClient(client), ); + @injectable + @dev + @prod CacheManager getCacheManager(BaseClient client) => CacheManager( Config('cacheKey', fileService: HttpFileService(httpClient: client))); + @injectable + @dev + @prod PaperlessAuthenticationApi authenticationModule(BaseClient client) => PaperlessAuthenticationApiImpl(client); + @injectable + @dev + @prod PaperlessLabelsApi labelsModule( @Named('timeoutClient') BaseClient timeoutClient, ) => PaperlessLabelApiImpl(timeoutClient); + @injectable + @dev + @prod PaperlessDocumentsApi documentsModule( @Named('timeoutClient') BaseClient timeoutClient, HttpClient httpClient, ) => PaperlessDocumentsApiImpl(timeoutClient, httpClient); + @injectable + @dev + @prod PaperlessSavedViewsApi savedViewsModule( @Named('timeoutClient') BaseClient timeoutClient, ) => PaperlessSavedViewsApiImpl(timeoutClient); + @injectable + @dev + @prod PaperlessServerStatsApi serverStatsModule( @Named('timeoutClient') BaseClient timeoutClient, ) => diff --git a/lib/di_test_mocks.dart b/lib/di_test_mocks.dart new file mode 100644 index 0000000..b36b4a5 --- /dev/null +++ b/lib/di_test_mocks.dart @@ -0,0 +1,69 @@ +import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:injectable/injectable.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:mockito/annotations.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/service/connectivity_status.service.dart'; +import 'package:paperless_mobile/core/store/local_vault.dart'; +import 'di_test_mocks.mocks.dart'; + +@module +abstract class DiMocksModule { + // All fields must be singleton in order to verify behavior in tests. + @singleton + @test + CacheManager get testCacheManager => CacheManager(Config('testKey')); + + @singleton + @test + PaperlessDocumentsApi get mockDocumentsApi => MockPaperlessDocumentsApi(); + + @singleton + @test + PaperlessLabelsApi get mockLabelsApi => MockPaperlessLabelsApi(); + + @singleton + @test + PaperlessSavedViewsApi get mockSavedViewsApi => MockPaperlessSavedViewsApi(); + + @singleton + @test + PaperlessAuthenticationApi get mockAuthenticationApi => + MockPaperlessAuthenticationApi(); + + @singleton + @test + PaperlessServerStatsApi get mockServerStatsApi => + MockPaperlessServerStatsApi(); + + @singleton + @test + LocalVault get mockLocalVault => MockLocalVault(); + + @singleton + @test + EncryptedSharedPreferences get mockSharedPreferences => + MockEncryptedSharedPreferences(); + + @singleton + @test + ConnectivityStatusService get mockConnectivityStatusService => + MockConnectivityStatusService(); + + @singleton + @test + LocalAuthentication get localAuthentication => MockLocalAuthentication(); +} diff --git a/lib/features/login/view/login_page.dart b/lib/features/login/view/login_page.dart index 8ce3832..e6cec2a 100644 --- a/lib/features/login/view/login_page.dart +++ b/lib/features/login/view/login_page.dart @@ -72,6 +72,7 @@ class _LoginPageState extends State { Widget _buildLoginButton() { return ElevatedButton( + key: const ValueKey('login-login-button'), style: ButtonStyle( backgroundColor: MaterialStatePropertyAll( Theme.of(context).colorScheme.primaryContainer, diff --git a/lib/features/login/view/widgets/client_certificate_form_field.dart b/lib/features/login/view/widgets/client_certificate_form_field.dart index d1a1e10..0768333 100644 --- a/lib/features/login/view/widgets/client_certificate_form_field.dart +++ b/lib/features/login/view/widgets/client_certificate_form_field.dart @@ -23,6 +23,7 @@ class _ClientCertificateFormFieldState @override Widget build(BuildContext context) { return FormBuilderField( + key: const ValueKey('login-client-cert'), initialValue: null, validator: (value) { if (value == null) { @@ -70,6 +71,7 @@ class _ClientCertificateFormFieldState ), if (_selectedFile != null) ...[ ObscuredInputTextFormField( + key: const ValueKey('login-client-cert-passphrase'), initialValue: field.value?.passphrase, onChanged: (value) => field.didChange( field.value?.copyWith(passphrase: value), diff --git a/lib/features/login/view/widgets/server_address_form_field.dart b/lib/features/login/view/widgets/server_address_form_field.dart index 14fd996..1b5488d 100644 --- a/lib/features/login/view/widgets/server_address_form_field.dart +++ b/lib/features/login/view/widgets/server_address_form_field.dart @@ -21,6 +21,7 @@ class _ServerAddressFormFieldState extends State { @override Widget build(BuildContext context) { return FormBuilderTextField( + key: const ValueKey('login-server-address'), name: ServerAddressFormField.fkServerAddress, validator: FormBuilderValidators.required( errorText: S.of(context).loginPageServerUrlValidatorMessageText, diff --git a/lib/features/login/view/widgets/user_credentials_form_field.dart b/lib/features/login/view/widgets/user_credentials_form_field.dart index fd792e7..a4d1596 100644 --- a/lib/features/login/view/widgets/user_credentials_form_field.dart +++ b/lib/features/login/view/widgets/user_credentials_form_field.dart @@ -24,6 +24,7 @@ class _UserCredentialsFormFieldState extends State { child: Column( children: [ TextFormField( + key: const ValueKey('login-username'), textCapitalization: TextCapitalization.words, autovalidateMode: AutovalidateMode.onUserInteraction, // USERNAME @@ -41,6 +42,7 @@ class _UserCredentialsFormFieldState extends State { ), ), ObscuredInputTextFormField( + key: const ValueKey('login-password'), label: S.of(context).loginPagePasswordFieldLabel, onChanged: (password) => field.didChange( field.value?.copyWith(password: password) ?? diff --git a/lib/main.dart b/lib/main.dart index b16a870..eb78da7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -33,7 +33,7 @@ import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -void main() async { +Future startAppProd() async { Bloc.observer = BlocChangesObserver(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); @@ -42,25 +42,22 @@ void main() async { // Required for self signed client certificates HttpOverrides.global = X509HttpOverrides(); - configureDependencies(); + configureDependencies('prod'); // Remove temporarily downloaded files. (await FileService.temporaryDirectory).deleteSync(recursive: true); kPackageInfo = await PackageInfo.fromPlatform(); // Load application settings and stored authentication data - getIt().initialize(); + await getIt().initialize(); await getIt().initialize(); await getIt().initialize(); - // Preload asset images - // WARNING: This seems to bloat up the app up to almost 200mb! - // await Future.forEach( - // AssetImages.values.map((e) => e.image), - // (img) => loadImage(img), - // ); - runApp(const PaperlessMobileEntrypoint()); } +void main() async { + await startAppProd(); +} + class PaperlessMobileEntrypoint extends StatefulWidget { const PaperlessMobileEntrypoint({Key? key}) : super(key: key); @@ -114,11 +111,7 @@ class _PaperlessMobileEntrypointState extends State { ), ), themeMode: settings.preferredThemeMode, - supportedLocales: const [ - Locale('en'), // Default if system locale is not available - Locale('de'), - Locale('cs'), - ], + supportedLocales: S.delegate.supportedLocales, locale: Locale.fromSubtags( languageCode: settings.preferredLocaleSubtag), localizationsDelegates: const [ diff --git a/pubspec.lock b/pubspec.lock index 900c6be..e440191 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -610,7 +610,7 @@ packages: name: form_builder_validators url: "https://pub.dartlang.org" source: hosted - version: "8.3.0" + version: "8.4.0" frontend_server_client: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2533bdb..2130926 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: git: url: https://github.com/flutter-form-builder-ecosystem/form_builder_extra_fields.git ref: main - form_builder_validators: ^8.3.0 + form_builder_validators: ^8.4.0 infinite_scroll_pagination: ^3.2.0 sliding_up_panel: ^2.0.0+1 package_info_plus: ^1.4.3+1