From 06bf05417e2b10631d6621ed2d97a962be75e942 Mon Sep 17 00:00:00 2001 From: phoenixit99 Date: Fri, 27 Dec 2024 17:30:46 +0700 Subject: [PATCH] refactor: apply singleton pattern for SharedPreferences --- CHANGELOG.md | 5 + lib/main.dart | 20 +- .../core/common/widgets/theme_switcher.dart | 2 +- lib/src/core/di/service_locator.dart | 12 + .../services/shared_preferences_service.dart | 15 +- .../presentation/bloc/language_bloc.dart | 1 + .../features/main/theme/bloc/theme_bloc.dart | 12 +- .../features/main/theme/bloc/theme_event.dart | 8 +- .../features/main/theme/bloc/theme_state.dart | 10 +- .../presentation/widgets/theme_selector.dart | 2 +- .../main/theme/theme_data/app_theme_data.dart | 287 ++---------------- .../theme_data/pallets/on_surface_pallet.dart | 2 +- .../theme_data/pallets/surface_pallet.dart | 2 +- pubspec.yaml | 5 +- test/helpers/test_service_locator.dart | 21 ++ .../presentation/bloc/language_bloc_test.dart | 159 +++++++--- 16 files changed, 230 insertions(+), 333 deletions(-) create mode 100644 lib/src/core/di/service_locator.dart create mode 100644 test/helpers/test_service_locator.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e56654..a4432f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 1.9.0+10 + +- Migrate to `Fluent UI` Framework/DesignSystem + - feat: add some changes on theme configurations for using fluent_ui package [#45](https://github.com/pactus-project/pactus-gui/pull/45) + # 1.8.0+9 - Implement or replace the Easy Localization package for efficient localization management diff --git a/lib/main.dart b/lib/main.dart index a69102b..083814c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; +import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gui/src/core/constants/configurations.dart'; +import 'package:gui/src/core/di/service_locator.dart'; import 'package:gui/src/core/router/app_router.dart'; import 'package:gui/src/core/utils/gen/localization/codegen_loader.g.dart'; import 'package:gui/src/features/main/theme/bloc/theme_bloc.dart'; @@ -12,26 +13,25 @@ import 'src/features/main/language/presentation/bloc/language_bloc.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); - - final sharedPreferencesService = await SharedPreferencesService.initialize(); + await setupSharedPreferences(); runApp( MultiBlocProvider( providers: [ BlocProvider( - create: (_) => LanguageBloc(sharedPreferencesService), + create: (_) => LanguageBloc(locator()), ), BlocProvider( - create: (_) => ThemeBloc(sharedPreferencesService), + create: (_) => ThemeBloc(locator()), ), ], - child: const MyApp(), + child: PactusGuiApp(), ), ); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class PactusGuiApp extends StatelessWidget { + const PactusGuiApp({super.key}); @override Widget build(BuildContext context) { @@ -51,10 +51,10 @@ class MyApp extends StatelessWidget { } return BlocBuilder( builder: (context, themeState) { - return MaterialApp.router( + return FluentApp.router( debugShowCheckedModeBanner: false, routerConfig: routerConfig, - title: 'Flutter Demo', + title: 'Pactus Gui App', theme: themeState.themeData, localizationsDelegates: context.localizationDelegates, supportedLocales: AppConfigs.supportedLocales, diff --git a/lib/src/core/common/widgets/theme_switcher.dart b/lib/src/core/common/widgets/theme_switcher.dart index fdb9c59..c956e19 100644 --- a/lib/src/core/common/widgets/theme_switcher.dart +++ b/lib/src/core/common/widgets/theme_switcher.dart @@ -36,7 +36,7 @@ class ThemeSwitcher extends StatelessWidget { GestureDetector( onTap: () { context.read().add( - ChangeTheme( + ThemeChanged( isLightTheme ? ThemeState.darkTheme : ThemeState.lightTheme, ), ); diff --git a/lib/src/core/di/service_locator.dart b/lib/src/core/di/service_locator.dart new file mode 100644 index 0000000..026faee --- /dev/null +++ b/lib/src/core/di/service_locator.dart @@ -0,0 +1,12 @@ +import 'package:get_it/get_it.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../services/shared_preferences_service.dart'; + +final locator = GetIt.instance; + +Future setupSharedPreferences() async { + final preferences = await SharedPreferences.getInstance(); + locator.registerSingleton( + SharedPreferencesService(preferences), + ); +} diff --git a/lib/src/core/services/shared_preferences_service.dart b/lib/src/core/services/shared_preferences_service.dart index b41d558..633900b 100644 --- a/lib/src/core/services/shared_preferences_service.dart +++ b/lib/src/core/services/shared_preferences_service.dart @@ -8,17 +8,10 @@ class SharedPreferencesService { SharedPreferencesService(this._preferences); final SharedPreferences _preferences; - static Future initialize() async { - final preferences = await SharedPreferences.getInstance(); - return SharedPreferencesService(preferences); - } - Future getSelectedTheme() async { - final prefs = await SharedPreferences.getInstance(); - final savedTheme = prefs.getString(AppConstants.themePrefsKey); + final savedTheme = _preferences.getString(AppConstants.themePrefsKey); if (savedTheme == null) { - // Return system theme if no preference is saved final brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness; @@ -31,12 +24,10 @@ class SharedPreferencesService { } Future saveSelectedTheme(String themeCode) async { - final prefs = await SharedPreferences.getInstance(); if (themeCode.isEmpty) { - // Remove saved preference to follow system theme - await prefs.remove(AppConstants.themePrefsKey); + await _preferences.remove(AppConstants.themePrefsKey); } else { - await prefs.setString(AppConstants.themePrefsKey, themeCode); + await _preferences.setString(AppConstants.themePrefsKey, themeCode); } } diff --git a/lib/src/features/main/language/presentation/bloc/language_bloc.dart b/lib/src/features/main/language/presentation/bloc/language_bloc.dart index 0f92234..ccef629 100644 --- a/lib/src/features/main/language/presentation/bloc/language_bloc.dart +++ b/lib/src/features/main/language/presentation/bloc/language_bloc.dart @@ -41,6 +41,7 @@ class LanguageBloc extends Bloc { await _sharedPreferencesService.saveSelectedLanguage( event.selectedLanguage.code, ); + emit(state.copyWith(selectedLanguage: event.selectedLanguage)); } } diff --git a/lib/src/features/main/theme/bloc/theme_bloc.dart b/lib/src/features/main/theme/bloc/theme_bloc.dart index 056a9b3..a5185c5 100644 --- a/lib/src/features/main/theme/bloc/theme_bloc.dart +++ b/lib/src/features/main/theme/bloc/theme_bloc.dart @@ -1,7 +1,8 @@ // lib/src/features/main/theme/bloc/theme_bloc.dart import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:gui/src/core/enums/theme_modes.dart'; import 'package:gui/src/features/main/theme/bloc/theme_state.dart'; import '../../../../core/services/shared_preferences_service.dart'; @@ -11,7 +12,7 @@ class ThemeBloc extends Bloc { ThemeBloc(this._sharedPreferencesService) : super(ThemeState(themeData: ThemeState.lightTheme)) { on(_onInitializeTheme); - on(_onChangeTheme); + on(_onChangeTheme); add(InitializeThemeEvent()); } @@ -25,18 +26,19 @@ class ThemeBloc extends Bloc { final themeData = themeCode == ThemeMode.dark.name ? ThemeState.darkTheme : ThemeState.lightTheme; + await _sharedPreferencesService.saveSelectedTheme(themeCode); emit(state.copyWith(themeData: themeData)); } Future _onChangeTheme( - ChangeTheme event, + ThemeChanged event, Emitter emit, ) async { - final themeCode = event.themeData.brightness == Brightness.dark + final themeCode = event.theme.brightness == Brightness.dark ? ThemeMode.dark.name : ThemeMode.light.name; await _sharedPreferencesService.saveSelectedTheme(themeCode); - emit(state.copyWith(themeData: event.themeData)); + emit(state.copyWith(themeData: event.theme)); } } diff --git a/lib/src/features/main/theme/bloc/theme_event.dart b/lib/src/features/main/theme/bloc/theme_event.dart index dd90002..2c9190d 100644 --- a/lib/src/features/main/theme/bloc/theme_event.dart +++ b/lib/src/features/main/theme/bloc/theme_event.dart @@ -10,12 +10,12 @@ abstract class ThemeEvent extends Equatable { class InitializeThemeEvent extends ThemeEvent {} -class ChangeTheme extends ThemeEvent { - const ChangeTheme(this.themeData); - final ThemeData themeData; +class ThemeChanged extends ThemeEvent { + const ThemeChanged(this.theme); + final FluentThemeData theme; @override - List get props => [themeData]; + List get props => [theme]; } class SystemThemeChanged extends ThemeEvent {} diff --git a/lib/src/features/main/theme/bloc/theme_state.dart b/lib/src/features/main/theme/bloc/theme_state.dart index 3407557..ca8746b 100644 --- a/lib/src/features/main/theme/bloc/theme_state.dart +++ b/lib/src/features/main/theme/bloc/theme_state.dart @@ -1,19 +1,19 @@ // lib/src/features/main/theme/bloc/theme_state.dart import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; +import 'package:fluent_ui/fluent_ui.dart'; class ThemeState extends Equatable { const ThemeState({ required this.themeData, }); - final ThemeData themeData; + final FluentThemeData themeData; - static ThemeData lightTheme = ThemeData( + static FluentThemeData lightTheme = FluentThemeData( brightness: Brightness.light, ); - static ThemeData darkTheme = ThemeData.dark(); + static FluentThemeData darkTheme = FluentThemeData.dark(); - ThemeState copyWith({ThemeData? themeData}) { + ThemeState copyWith({FluentThemeData? themeData}) { return ThemeState( themeData: themeData ?? this.themeData, ); diff --git a/lib/src/features/main/theme/presentation/widgets/theme_selector.dart b/lib/src/features/main/theme/presentation/widgets/theme_selector.dart index 8a9ed87..05d8a73 100644 --- a/lib/src/features/main/theme/presentation/widgets/theme_selector.dart +++ b/lib/src/features/main/theme/presentation/widgets/theme_selector.dart @@ -17,7 +17,7 @@ class ThemeSelector extends StatelessWidget { ElevatedButton( onPressed: () { context.read().add( - ChangeTheme( + ThemeChanged( isLightTheme ? ThemeState.darkTheme : ThemeState.lightTheme, ), ); diff --git a/lib/src/features/main/theme/theme_data/app_theme_data.dart b/lib/src/features/main/theme/theme_data/app_theme_data.dart index acd4b36..72ccf22 100644 --- a/lib/src/features/main/theme/theme_data/app_theme_data.dart +++ b/lib/src/features/main/theme/theme_data/app_theme_data.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:fluent_ui/fluent_ui.dart'; import 'package:gui/src/core/enums/theme_modes.dart'; import 'package:gui/src/core/utils/gen/assets/fonts.gen.dart'; import 'package:gui/src/features/main/theme/theme_data/pallets/on_surface_pallet.dart'; @@ -6,278 +6,57 @@ import 'package:gui/src/features/main/theme/theme_data/pallets/surface_pallet.da /// ## [AppThemeData] Documentation /// -/// The [AppThemeData] class provides theming configurations for the -/// application. -/// It contains a static map that holds the `ThemeData` for both light and dark -/// modes, allowing for easy theme management based on the user's preference. +/// The `AppThemeData` class provides theming configs for the application, +/// supporting both light and dark themes. /// +/// ### Properties +/// +/// - **`themeDataModes`**: A static map containing theme configurations for: +/// - **Light Theme**: Configured with `lightTypography` and light palette. +/// - **Dark Theme**: Configured with `darkTypography` and dark palette. +/// +/// - **`lightTypography`**: The typography config used for the light theme. +/// +/// - **`darkTypography`**: The typography config used for the dark theme. +/// +/// ### Example Usage +/// +/// ```dart +/// final theme = AppThemeData.themeDataModes[ThemeModes.light]; +/// ``` class AppThemeData { const AppThemeData._(); - static final Map themeDataModes = { + + static final Map themeDataModes = { // light ThemeData. - ThemeModes.light: ThemeData( - bottomNavigationBarTheme: BottomNavigationBarThemeData( - backgroundColor: SurfacePallet.light.surface3, - ), + ThemeModes.light: FluentThemeData( + fontFamily: FontFamily.inter, scaffoldBackgroundColor: SurfacePallet.light.surface3, brightness: Brightness.light, - dividerColor: OnSurfacePallet.light.onSurface1, - textTheme: lightTextTheme, + typography: lightTypography, cardColor: SurfacePallet.light.surface3, extensions: const >[ OnSurfacePallet.light, SurfacePallet.light, ], ), - // dark ThemeData. - ThemeModes.dark: ThemeData( + ThemeModes.dark: FluentThemeData( + fontFamily: FontFamily.inter, + scaffoldBackgroundColor: SurfacePallet.dark.surface3, brightness: Brightness.dark, - textTheme: darkTextTheme, - extensions: >[ + typography: Typography.raw(), + cardColor: SurfacePallet.dark.surface3, + extensions: const >[ OnSurfacePallet.dark, SurfacePallet.dark, ], ), }; - static final lightTextTheme = TextTheme( - displaySmall: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w700, - fontSize: 16, - height: 24 / 12, - leadingDistribution: TextLeadingDistribution.even, - ), - headlineLarge: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w300, - fontSize: 36, - height: 52 / 36, - leadingDistribution: TextLeadingDistribution.even, - ), - headlineMedium: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w400, - fontSize: 32, - height: 44 / 32, - leadingDistribution: TextLeadingDistribution.even, - ), - headlineSmall: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w700, - fontSize: 24, - height: 36 / 24, - leadingDistribution: TextLeadingDistribution.even, - ), - titleLarge: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w300, - fontSize: 20, - height: 28 / 20, - leadingDistribution: TextLeadingDistribution.even, - ), - titleMedium: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w400, - fontSize: 16, - height: 24 / 16, - leadingDistribution: TextLeadingDistribution.even, - ), - titleSmall: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w400, - fontSize: 12, - height: 16 / 12, - leadingDistribution: TextLeadingDistribution.even, - ), - bodyLarge: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w300, - fontSize: 16, - height: 24 / 16, - leadingDistribution: TextLeadingDistribution.even, - ), - bodyMedium: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w400, - fontSize: 14, - height: 20 / 14, - leadingDistribution: TextLeadingDistribution.even, - ), - bodySmall: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w400, - fontSize: 12, - height: 18 / 12, - leadingDistribution: TextLeadingDistribution.even, - ), - labelLarge: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w400, - fontSize: 16, - height: 20 / 16, - leadingDistribution: TextLeadingDistribution.even, - ), - labelMedium: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 20 / 14, - leadingDistribution: TextLeadingDistribution.even, - ), - labelSmall: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w500, - fontSize: 10, - height: 16 / 10, - leadingDistribution: TextLeadingDistribution.even, - ), - ); + // TODO(Esmaeil): Update this part based on the new text style in the Figma. + static final lightTypography = Typography.raw(); - static final darkTextTheme = TextTheme( - displaySmall: TextStyle( - fontFamily: FontFamily.inter, - color: SurfacePallet.light.surface3, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w700, - fontSize: 16, - height: 24 / 12, - leadingDistribution: TextLeadingDistribution.even, - ), - headlineLarge: TextStyle( - fontFamily: FontFamily.inter, - color: OnSurfacePallet.light.onSurface4, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w300, - fontSize: 36, - height: 52 / 36, - leadingDistribution: TextLeadingDistribution.even, - ), - headlineMedium: TextStyle( - fontFamily: FontFamily.inter, - color: OnSurfacePallet.light.onSurface4, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w400, - fontSize: 32, - height: 44 / 32, - leadingDistribution: TextLeadingDistribution.even, - ), - headlineSmall: TextStyle( - fontFamily: FontFamily.inter, - color: OnSurfacePallet.light.onSurface4, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w700, - fontSize: 24, - height: 36 / 24, - leadingDistribution: TextLeadingDistribution.even, - ), - titleLarge: TextStyle( - fontFamily: FontFamily.inter, - color: OnSurfacePallet.light.onSurface4, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w300, - fontSize: 20, - height: 28 / 20, - leadingDistribution: TextLeadingDistribution.even, - ), - titleMedium: TextStyle( - fontFamily: FontFamily.inter, - color: OnSurfacePallet.light.onSurface4, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w400, - fontSize: 16, - height: 24 / 16, - leadingDistribution: TextLeadingDistribution.even, - ), - titleSmall: TextStyle( - fontFamily: FontFamily.inter, - color: OnSurfacePallet.light.onSurface4, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w400, - fontSize: 12, - height: 16 / 12, - leadingDistribution: TextLeadingDistribution.even, - ), - bodyLarge: TextStyle( - fontFamily: FontFamily.inter, - color: OnSurfacePallet.light.onSurface4, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w300, - fontSize: 16, - height: 24 / 16, - leadingDistribution: TextLeadingDistribution.even, - ), - bodyMedium: TextStyle( - fontFamily: FontFamily.inter, - color: OnSurfacePallet.light.onSurface4, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w400, - fontSize: 14, - height: 20 / 14, - leadingDistribution: TextLeadingDistribution.even, - ), - bodySmall: TextStyle( - fontFamily: FontFamily.inter, - color: OnSurfacePallet.light.onSurface4, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w400, - fontSize: 12, - height: 18 / 12, - leadingDistribution: TextLeadingDistribution.even, - ), - labelLarge: TextStyle( - fontFamily: FontFamily.inter, - color: OnSurfacePallet.light.onSurface4, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w400, - fontSize: 16, - height: 20 / 16, - leadingDistribution: TextLeadingDistribution.even, - ), - labelMedium: TextStyle( - fontFamily: FontFamily.inter, - color: OnSurfacePallet.light.onSurface4, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 20 / 14, - leadingDistribution: TextLeadingDistribution.even, - ), - labelSmall: TextStyle( - fontFamily: FontFamily.inter, - color: OnSurfacePallet.light.onSurface4, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.w500, - fontSize: 10, - height: 16 / 10, - leadingDistribution: TextLeadingDistribution.even, - ), - ); + // TODO(Esmaeil): Update this part based on the new text style in the Figma. + static final darkTypography = Typography.raw(); } diff --git a/lib/src/features/main/theme/theme_data/pallets/on_surface_pallet.dart b/lib/src/features/main/theme/theme_data/pallets/on_surface_pallet.dart index c7dee25..84ca066 100644 --- a/lib/src/features/main/theme/theme_data/pallets/on_surface_pallet.dart +++ b/lib/src/features/main/theme/theme_data/pallets/on_surface_pallet.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:fluent_ui/fluent_ui.dart'; @immutable class OnSurfacePallet extends ThemeExtension { diff --git a/lib/src/features/main/theme/theme_data/pallets/surface_pallet.dart b/lib/src/features/main/theme/theme_data/pallets/surface_pallet.dart index f9c165a..55eb4fc 100644 --- a/lib/src/features/main/theme/theme_data/pallets/surface_pallet.dart +++ b/lib/src/features/main/theme/theme_data/pallets/surface_pallet.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:fluent_ui/fluent_ui.dart'; @immutable class SurfacePallet extends ThemeExtension { diff --git a/pubspec.yaml b/pubspec.yaml index 6aa0cf5..af99b23 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: gui description: "Pactus Flutter GUI" publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.8.0+9 +version: 1.9.0+10 environment: sdk: ^3.5.3 @@ -12,6 +12,7 @@ dependencies: cupertino_icons: ^1.0.8 easy_localization: ^3.0.7 equatable: ^2.0.7 + fluent_ui: ^4.9.2 flutter: sdk: flutter flutter_bloc: ^8.1.0 @@ -26,8 +27,8 @@ dependencies: go_router: ^14.6.1 intl: ^0.19.0 json_annotation: ^4.9.0 + mocktail: ^1.0.4 shared_preferences: ^2.3.4 - dev_dependencies: build_runner: easy_localization_generator: ^0.3.3 diff --git a/test/helpers/test_service_locator.dart b/test/helpers/test_service_locator.dart new file mode 100644 index 0000000..37f6c7a --- /dev/null +++ b/test/helpers/test_service_locator.dart @@ -0,0 +1,21 @@ +import 'package:get_it/get_it.dart'; +import 'package:gui/src/core/services/shared_preferences_service.dart'; +import 'package:mocktail/mocktail.dart'; + +final locator = GetIt.instance; + +class MockSharedPreferencesService extends Mock + implements SharedPreferencesService {} + +/// Initializes test dependencies within the [locator] +Future initTestDependencies() async { + // Register mocks + locator.registerSingleton( + MockSharedPreferencesService(), + ); +} + +/// Resets all registered dependencies +void resetDependencies() { + locator.reset(); +} diff --git a/test/src/features/main/language/presentation/bloc/language_bloc_test.dart b/test/src/features/main/language/presentation/bloc/language_bloc_test.dart index e59afdd..9bafcaa 100644 --- a/test/src/features/main/language/presentation/bloc/language_bloc_test.dart +++ b/test/src/features/main/language/presentation/bloc/language_bloc_test.dart @@ -3,51 +3,136 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:gui/src/core/services/shared_preferences_service.dart'; import 'package:gui/src/features/main/language/data/language_model.dart'; import 'package:gui/src/features/main/language/presentation/bloc/language_bloc.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../../../../helpers/test_service_locator.dart'; void main() { + late LanguageBloc languageBloc; + late MockSharedPreferencesService mockPrefsService; + + setUp(() async { + await initTestDependencies(); + mockPrefsService = + locator() as MockSharedPreferencesService; + + // Setup default mock behavior + when(() => mockPrefsService.getSelectedLanguage()) + .thenReturn(Language.english.code); + when(() => mockPrefsService.saveSelectedLanguage(any())) + .thenAnswer((_) async => true); + + languageBloc = LanguageBloc(mockPrefsService); + }); + + tearDown(() { + languageBloc.close(); + resetDependencies(); + }); + group('LanguageBloc', () { - late LanguageBloc languageBloc; + group('GetSelectedLanguage', () { + test('should return English when no language is saved', () { + // Given + when(() => mockPrefsService.getSelectedLanguage()).thenReturn(''); + when(() => mockPrefsService.saveSelectedLanguage(Language.english.code)) + .thenAnswer((_) async => true); - setUp(() async { - final sharedPreferencesService = - await SharedPreferencesService.initialize(); - languageBloc = LanguageBloc(sharedPreferencesService); - }); + // When + final bloc = LanguageBloc(mockPrefsService); - tearDown(() { - languageBloc.close(); - }); + // Then + expect(bloc.state.selectedLanguage, equals(Language.english)); + verify(() => mockPrefsService.getSelectedLanguage()).called(1); + verify( + () => mockPrefsService.saveSelectedLanguage(Language.english.code), + ).called(1); + }); + + test('should return Spanish when Spanish is saved', () async { + when(() => mockPrefsService.getSelectedLanguage()) + .thenReturn(Language.spanish.code); + when(() => mockPrefsService.saveSelectedLanguage(Language.spanish.code)) + .thenAnswer((_) async => true); + + final bloc = LanguageBloc(mockPrefsService) + ..add(ChangeLanguage(selectedLanguage: Language.spanish)); - test('initial state should be English', () { - expect(languageBloc.state.selectedLanguage, equals(Language.english)); + // Wait for the state to change + // ignore: inference_failure_on_instance_creation + await Future.delayed(Duration.zero); + + // Then + expect(bloc.state.selectedLanguage, equals(Language.spanish)); + }); + + test('should return English for invalid language code', () { + // Given + when(() => mockPrefsService.getSelectedLanguage()) + .thenReturn('invalid_code'); + when(() => mockPrefsService.saveSelectedLanguage(any())) + .thenAnswer((_) async => true); + + // When + final bloc = LanguageBloc(mockPrefsService); + + // Then + expect(bloc.state.selectedLanguage, equals(Language.english)); + verify(() => mockPrefsService.getSelectedLanguage()).called(1); + verify( + () => mockPrefsService.saveSelectedLanguage(Language.english.code), + ).called(1); + }); + + test('should handle empty string from preferences', () { + // Given + when(() => mockPrefsService.getSelectedLanguage()).thenReturn(''); + when(() => mockPrefsService.saveSelectedLanguage(any())) + .thenAnswer((_) async => true); + + // When + final bloc = LanguageBloc(mockPrefsService); + + // Then + expect(bloc.state.selectedLanguage, equals(Language.english)); + verify(() => mockPrefsService.getSelectedLanguage()).called(1); + verify( + () => mockPrefsService.saveSelectedLanguage(Language.english.code), + ).called(1); + }); }); - blocTest( - 'emits Spanish language when changed to Spanish', - build: () => languageBloc, - act: (bloc) => - bloc.add(ChangeLanguage(selectedLanguage: Language.spanish)), - expect: () => [ - isA().having( - (state) => state.selectedLanguage, - 'language', - Language.spanish, - ), - ], - ); - - blocTest( - 'emits French language when changed to French', - build: () => languageBloc, - act: (bloc) => - bloc.add(ChangeLanguage(selectedLanguage: Language.french)), - expect: () => [ - isA().having( - (state) => state.selectedLanguage, - 'language', - Language.french, + group('Language Change', () { + blocTest( + 'should save language when changed', + setUp: () { + when(() => mockPrefsService.saveSelectedLanguage(any())) + .thenAnswer((_) async => true); + }, + build: () => languageBloc, + act: (bloc) => bloc.add( + ChangeLanguage(selectedLanguage: Language.spanish), ), - ], - ); + verify: (_) { + verify( + () => mockPrefsService.saveSelectedLanguage(Language.spanish.code), + ).called(1); + }, + ); + + // test('should throw when save fails', () { + // // Given + // when(() => mockPrefsService.saveSelectedLanguage(any())) + // .thenThrow(Exception('Save failed')); + + // // Then + // expect( + // () => languageBloc.add( + // ChangeLanguage(selectedLanguage: Language.spanish), + // ), + // throwsException, + // ); + // }); + }); }); }