diff --git a/design_system/lib/theme/app_text_styles.dart b/design_system/lib/theme/app_text_styles.dart index 8e649adb..9eff0095 100644 --- a/design_system/lib/theme/app_text_styles.dart +++ b/design_system/lib/theme/app_text_styles.dart @@ -1,13 +1,16 @@ //ignore_for_file: unused-files, unused-code +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; const FontWeight _semiboldWeight = FontWeight.w600; -class AppTextStyles extends TextTheme { +final _isTesting = Platform.environment.containsKey('FLUTTER_TEST'); +class AppTextStyles extends TextTheme { const AppTextStyles({ super.headlineLarge, super.headlineMedium, @@ -45,12 +48,17 @@ class AppTextStyles extends TextTheme { double fontSize, FontWeight fontWeight, ) => - GoogleFonts.roboto( - fontSize: fontSize, - fontWeight: fontWeight, - ); + _isTesting + ? TextStyle(fontSize: fontSize, fontWeight: fontWeight) + : GoogleFonts.roboto( + fontSize: fontSize, + fontWeight: fontWeight, + ); - static AppTextStyles getDefaultAppStyles() => AppTextStyles.fromTextTheme( + static AppTextStyles getDefaultAppStyles() => + _isTesting ? _testingTextTheme() : _appTextTheme(); + + static AppTextStyles _appTextTheme() => AppTextStyles.fromTextTheme( textTheme: GoogleFonts.robotoTextTheme().copyWith( labelLarge: _robotoTextStyle(20.sp, FontWeight.normal), labelMedium: _robotoTextStyle(16.sp, FontWeight.normal), @@ -60,6 +68,16 @@ class AppTextStyles extends TextTheme { ), ); + static AppTextStyles _testingTextTheme() => AppTextStyles.fromTextTheme( + textTheme: TextTheme( + labelLarge: _robotoTextStyle(20.sp, FontWeight.normal), + labelMedium: _robotoTextStyle(16.sp, FontWeight.normal), + labelSmall: _robotoTextStyle(14.sp, FontWeight.normal), + headlineMedium: _robotoTextStyle(20.sp, FontWeight.bold), + headlineLarge: _robotoTextStyle(24.sp, FontWeight.bold), + ), + ); + TextTheme getThemeData() => getDefaultAppStyles(); } diff --git a/design_system/lib/theme/custom_text_styles.dart b/design_system/lib/theme/custom_text_styles.dart index a34d22a6..07f3f849 100644 --- a/design_system/lib/theme/custom_text_styles.dart +++ b/design_system/lib/theme/custom_text_styles.dart @@ -1,10 +1,12 @@ // ignore_for_file: overridden_fields +import 'dart:io'; + import 'package:design_system/extensions/color_extensions.dart'; +import 'package:design_system/theme/custom_colors.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:design_system/theme/custom_colors.dart'; const FontWeight _semiboldWeight = FontWeight.w600; @@ -57,11 +59,17 @@ class CustomTextStyles extends ThemeExtension { FontWeight fontWeight, CustomColors customColors, ) => - GoogleFonts.roboto( - fontSize: fontSize, - fontWeight: fontWeight, - color: customColors.textColor!.getShade(500), - ); + Platform.environment.containsKey('FLUTTER_TEST') + ? TextStyle( + fontSize: fontSize, + fontWeight: fontWeight, + color: customColors.textColor!.getShade(500), + ) + : GoogleFonts.roboto( + fontSize: fontSize, + fontWeight: fontWeight, + color: customColors.textColor!.getShade(500), + ); @override CustomTextStyles copyWith({MaterialColor? primary}) => diff --git a/lib/core/common/config.dart b/lib/core/common/config.dart index 8ac036d9..97d3add5 100644 --- a/lib/core/common/config.dart +++ b/lib/core/common/config.dart @@ -1,6 +1,7 @@ // ignore_for_file: constant_identifier_names import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_template/core/common/environments.dart'; @@ -13,7 +14,7 @@ interface class Config { static const String environmentFolder = 'environments'; static const debugMode = kDebugMode; - + static bool testingMode = Platform.environment.containsKey('FLUTTER_TEST'); static late String apiBaseUrl; static late String supabaseApiKey; static late String appDirectoryPath; diff --git a/lib/core/di/di_utils_module.dart b/lib/core/di/di_utils_module.dart index 83f6e467..b0a676fe 100644 --- a/lib/core/di/di_utils_module.dart +++ b/lib/core/di/di_utils_module.dart @@ -13,6 +13,6 @@ class UtilsDiModule { extension _GetItDiModuleExtensions on GetIt { void _setupModule() { - registerLazySingleton(() => AppRouter(get())); + registerLazySingleton(() => AppRouter(sessionRepository: get())); } } diff --git a/lib/ui/router/app_router.dart b/lib/ui/router/app_router.dart index 69c9d1f1..070110d8 100644 --- a/lib/ui/router/app_router.dart +++ b/lib/ui/router/app_router.dart @@ -13,10 +13,11 @@ part 'app_router.gr.dart'; class AppRouter extends _$AppRouter { @override final List routes; + final String? initialRoute; ReevaluateListenable authReevaluateListenable; - AppRouter(SessionRepository sessionRepository) + AppRouter({required SessionRepository sessionRepository, this.initialRoute}) : authReevaluateListenable = ReevaluateListenable.stream( sessionRepository.status.distinct().skip(1), ), @@ -26,7 +27,7 @@ class AppRouter extends _$AppRouter { path: '/', guards: [UnauthenticatedGuard(sessionRepository)], children: [ - RedirectRoute(path: '', redirectTo: 'login'), + RedirectRoute(path: '', redirectTo: initialRoute ?? 'login'), AutoRoute(path: 'login', page: SignInRoute.page), ], ), @@ -35,7 +36,7 @@ class AppRouter extends _$AppRouter { guards: [AuthenticatedGuard(sessionRepository)], path: '/', children: [ - RedirectRoute(path: '', redirectTo: 'welcome'), + RedirectRoute(path: '', redirectTo: initialRoute ?? 'welcome'), AutoRoute(path: 'welcome', page: WelcomeRoute.page), ], ), diff --git a/pubspec.lock b/pubspec.lock index 68d21fca..8f364669 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -812,7 +812,7 @@ packages: source: hosted version: "2.2.1" path_provider_platform_interface: - dependency: transitive + dependency: "direct dev" description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" @@ -844,7 +844,7 @@ packages: source: hosted version: "3.1.4" plugin_platform_interface: - dependency: transitive + dependency: "direct dev" description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" diff --git a/pubspec.yaml b/pubspec.yaml index 018cb66f..c6c71520 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,8 @@ dev_dependencies: json_serializable: 6.8.0 lints: 4.0.0 mocktail: 1.0.3 + path_provider_platform_interface: 2.1.2 + plugin_platform_interface: 2.1.8 ## TODO remove this when dart_code_linter updates the dependencies dependency_overrides: diff --git a/test/mocks/mock_app.dart b/test/mocks/mock_app.dart new file mode 100644 index 00000000..777e96df --- /dev/null +++ b/test/mocks/mock_app.dart @@ -0,0 +1,92 @@ +import 'dart:async'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_template/core/common/config.dart'; +import 'package:flutter_template/core/common/logger.dart'; +import 'package:flutter_template/core/di/di_provider.dart'; +import 'package:flutter_template/core/source/common/http_service.dart'; +import 'package:flutter_template/main.dart'; +import 'package:flutter_template/ui/router/app_router.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'services/http_service.dart'; +import 'services/path_provider.dart'; +import 'sources/secure_storage.dart'; + +class SimpleTesteableApp extends MyApp { + final Map? apiOverrides; + + final String initialRoute; + const SimpleTesteableApp._({ + super.key, + this.apiOverrides, + this.initialRoute = '/', + }) : super(); + + static Future getUnauthenticatedApp({ + Map? apiOverrides, + String initialRoute = '/', + Map? secureStorageData, + Map? sharedPreferencesData, + }) async { + await _initMockSdks( + apiOverrides: apiOverrides, + initialRoute: initialRoute, + sharedPreferenceData: secureStorageData, + secureStorageData: secureStorageData, + ); + final app = SimpleTesteableApp._( + apiOverrides: apiOverrides, + initialRoute: initialRoute, + ); + return app; + } +} + +Future _initMockSdks({ + Map? apiOverrides, + Map? secureStorageData, + Map? sharedPreferenceData, + String? initialRoute, +}) async { + _initializeLocalSources( + secureStorageData: secureStorageData, + sharedPreferenceData: sharedPreferenceData, + ); + await _initializeProviders(apiOverrides, initialRoute); + await Logger.init(); + await Config.initialize(); + Hive.init(Config.appDirectoryPath); +} + +void _initializeLocalSources({ + Map? secureStorageData, + Map? sharedPreferenceData, +}) { + FakeFlutterSecureStorage.setInitialData(secureStorageData ?? {}); + PathProviderPlatform.instance = FakePathProviderPlatform(); + SharedPreferences.setMockInitialValues(sharedPreferenceData ?? {}); +} + +Future _initializeProviders( + Map? apiOverrides, + String? initialRoute, +) async { + await DiProvider.init(); + DiProvider.instance + ..allowReassignment = true + ..registerLazySingleton( + () => FakeFlutterSecureStorage(), + ) + ..registerLazySingleton( + () => FakeHttpService()..mockApi(apiOverrides ?? {}), + ) + ..registerLazySingleton( + () => AppRouter( + initialRoute: initialRoute, + sessionRepository: DiProvider.get(), + ), + ); +} diff --git a/test/mocks/services/http_service.dart b/test/mocks/services/http_service.dart new file mode 100644 index 00000000..491293a3 --- /dev/null +++ b/test/mocks/services/http_service.dart @@ -0,0 +1,72 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_template/core/source/common/http_service.dart'; +import 'package:mocktail/mocktail.dart'; + +class FakeHttpService extends Mock implements HttpServiceDio {} + +typedef ResponseData = Map; +typedef ApiOverrideKey = ({String path, ApiMethod method}); + +enum ApiMethod { apiPost, apiGet, apiDelete, apiPut } + +extension MockApiServiceExtension on FakeHttpService { + void mockApi(Map apiOverrides) { + for (final element in apiOverrides.entries) { + switch (element.key.method) { + case ApiMethod.apiPost: + _mockPost(element.key.path, element.value); + case ApiMethod.apiGet: + _mockGet(element.key.path, element.value); + case ApiMethod.apiDelete: + _mockDelete(element.key.path, element.value); + case ApiMethod.apiPut: + _mockPut(element.key.path, element.value); + } + } + } + + void _mockPost( + String path, + Map response, { + Map? parameters, + }) => + when(() => post(path, queryParameters: parameters)).thenAnswer( + (_) async => Response( + requestOptions: RequestOptions(path: path), + data: response, + ), + ); + void _mockGet( + String path, + Map response, { + Map? parameters, + }) => + when(() => get(path, queryParameters: parameters)).thenAnswer( + (_) async => Response( + requestOptions: RequestOptions(path: path), + data: response, + ), + ); + void _mockDelete( + String path, + Map response, { + Map? parameters, + }) => + when(() => delete(path, queryParameters: parameters)).thenAnswer( + (_) async => Response( + requestOptions: RequestOptions(path: path), + data: response, + ), + ); + void _mockPut( + String path, + Map response, { + Map? parameters, + }) => + when(() => put(path, queryParameters: parameters)).thenAnswer( + (_) async => Response( + requestOptions: RequestOptions(path: path), + data: response, + ), + ); +} diff --git a/test/mocks/services/path_provider.dart b/test/mocks/services/path_provider.dart new file mode 100644 index 00000000..8b40bdda --- /dev/null +++ b/test/mocks/services/path_provider.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +const String kTemporaryPath = 'temporaryPath'; +const String kApplicationSupportPath = 'applicationSupportPath'; +const String kDownloadsPath = 'downloadsPath'; +const String kLibraryPath = 'libraryPath'; +const String kApplicationDocumentsPath = 'applicationDocumentsPath'; +const String kExternalCachePath = 'externalCachePath'; +const String kExternalStoragePath = 'externalStoragePath'; + +class FakePathProviderPlatform extends Fake + with MockPlatformInterfaceMixin + implements PathProviderPlatform { + @override + Future getTemporaryPath() async => kTemporaryPath; + + @override + Future getApplicationSupportPath() async => kApplicationSupportPath; + + @override + Future getLibraryPath() async => kLibraryPath; + + @override + Future getApplicationDocumentsPath() async => + kApplicationDocumentsPath; + + @override + Future getExternalStoragePath() async => kExternalStoragePath; + + @override + Future?> getExternalCachePaths() async => + [kExternalCachePath]; + + @override + Future?> getExternalStoragePaths({ + StorageDirectory? type, + }) async => + [kExternalStoragePath]; + + @override + Future getDownloadsPath() async => kDownloadsPath; +} diff --git a/test/mocks/sources/secure_storage.dart b/test/mocks/sources/secure_storage.dart new file mode 100644 index 00000000..66ac1e0b --- /dev/null +++ b/test/mocks/sources/secure_storage.dart @@ -0,0 +1,7 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class FakeFlutterSecureStorage extends FlutterSecureStorage { + static void setInitialData(Map initialData) { + FlutterSecureStorage.setMockInitialValues(initialData); + } +} diff --git a/test/widget_test.dart b/test/widget_test.dart index 00c02a8a..4b6f2027 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,7 +1,24 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_template/ui/signin/signin_screen.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import 'mocks/mock_app.dart'; void main() { - testWidgets('Example test', (WidgetTester tester) async { - expect(0, 0); - }); + tearDown(rootBundle.clear); + setUp(() => GoogleFonts.config.allowRuntimeFetching = false); + + testWidgets( + 'Initialize app from login screen', + (WidgetTester tester) async { + await tester.pumpWidget( + await SimpleTesteableApp.getUnauthenticatedApp( + initialRoute: 'login', + ), + ); + await tester.pumpAndSettle(); + expect(find.byType(SignInScreen), findsOneWidget); + }, + ); }