diff --git a/lib/configuration/app_key_constants.dart b/lib/configuration/app_key_constants.dart new file mode 100644 index 0000000..9d82495 --- /dev/null +++ b/lib/configuration/app_key_constants.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +const Key drawerButtonLogout = Key("drawerButtonLogout"); +const Key drawerButtonLogoutYes = Key("drawerButtonLogoutYes"); +const Key drawerButtonLogoutNo = Key("drawerButtonLogoutNo"); \ No newline at end of file diff --git a/lib/configuration/constants.dart b/lib/configuration/constants.dart index 793ddcf..5443a94 100644 --- a/lib/configuration/constants.dart +++ b/lib/configuration/constants.dart @@ -2,4 +2,5 @@ class LocaleConstants { static const Map languages = {'en': 'English'}; static const String langStorageKey = 'locale'; static const String logoLightUrl = 'assets/images/logoLight.png'; + static const String defaultImgUrl = 'assets/images/img.png'; } diff --git a/lib/data/repository/account_repository.dart b/lib/data/repository/account_repository.dart index 4446359..29ef995 100644 --- a/lib/data/repository/account_repository.dart +++ b/lib/data/repository/account_repository.dart @@ -67,7 +67,7 @@ class AccountRepository { final httpResponse = await HttpUtils.getRequest("/$_resource"); var response = HttpUtils.decodeUTF8(httpResponse.body.toString()); var result = User.fromJsonString(response)!; - debugPrint("END:getAccount successful - response : $response}"); + debugPrint("END:getAccount successful - response : ${response.length}}"); return result; } diff --git a/lib/main/app.dart b/lib/main/app.dart index 283b764..a98dcea 100644 --- a/lib/main/app.dart +++ b/lib/main/app.dart @@ -45,87 +45,109 @@ import '../presentation/screen/user/list/list_user_screen.dart'; class App extends StatelessWidget { final String language; + final AdaptiveThemeMode initialTheme; - const App({super.key, required this.language}); + App({super.key, required this.language, required this.initialTheme}); @override Widget build(BuildContext context) { + return buildHomeApp(); + } + + AdaptiveTheme buildHomeApp() { return AdaptiveTheme( - light: ThemeData( - useMaterial3: false, - brightness: Brightness.light, - colorSchemeSeed: Colors.blueGrey, - ), - dark: ThemeData( - useMaterial3: false, - brightness: Brightness.dark, - primarySwatch: Colors.blueGrey, - ), + light: _buildLightTheme(), + dark: _buildDarkTheme(), debugShowFloatingThemeButton: false, - initial: AdaptiveThemeMode.dark, + initial: initialTheme, builder: (light, dark) { - return MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => AuthorityBloc(authorityRepository: AuthorityRepository())), - BlocProvider(create: (_) => AccountBloc(accountRepository: AccountRepository())), - BlocProvider(create: (_) => UserBloc(userRepository: UserRepository())), - BlocProvider(create: (_) => CityBloc(cityRepository: CityRepository())), - BlocProvider(create: (_) => DistrictBloc(districtRepository: DistrictRepository())), - BlocProvider(create: (_) => DrawerBloc(loginRepository: LoginRepository(), menuRepository: MenuRepository())), - ], - child: GetMaterialApp( - theme: light, - darkTheme: dark, - debugShowCheckedModeBanner: ProfileConstants.isDevelopment, - debugShowMaterialGrid: false, - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: S.delegate.supportedLocales, - locale: Locale(language), - initialRoute: initialRouteControl(), - routes: { - ApplicationRoutes.home: (context) { - return BlocProvider( - create: (context) => AccountBloc(accountRepository: AccountRepository())..add(const AccountLoad()), child: HomeScreen()); - }, - ApplicationRoutes.account: (context) { - return BlocProvider( - create: (context) => AccountBloc(accountRepository: AccountRepository())..add(const AccountLoad()), child: AccountsScreen()); - }, - ApplicationRoutes.login: (context) { - return BlocProvider(create: (context) => LoginBloc(loginRepository: LoginRepository()), child: LoginScreen()); - }, - ApplicationRoutes.settings: (context) { - return BlocProvider( - create: (context) => SettingsBloc(accountRepository: AccountRepository()), child: const SettingsScreen()); - }, - ApplicationRoutes.forgotPassword: (context) { - return BlocProvider( - create: (context) => ForgotPasswordBloc(accountRepository: AccountRepository()), child: ForgotPasswordScreen()); - }, - ApplicationRoutes.register: (context) { - return BlocProvider( - create: (context) => RegisterBloc(accountRepository: AccountRepository()), child: RegisterScreen()); - }, - ApplicationRoutes.changePassword: (context) { - return BlocProvider( - create: (context) => ChangePasswordBloc(accountRepository: AccountRepository()), child: ChangePasswordScreen()); - }, - ApplicationRoutes.logout: (context) => const LogoutConfirmationDialog(), - ApplicationRoutes.createUser: (context) { - return BlocProvider(create: (context) => UserBloc(userRepository: UserRepository()), child: CreateUserScreen()); - }, - ApplicationRoutes.listUsers: (context) { - return BlocProvider(create: (context) => UserBloc(userRepository: UserRepository()), child: ListUserScreen()); - }, - }, - ), - ); + return _buildMultiBlocProvider(light, dark); }, ); } + + ThemeData _buildDarkTheme() { + return ThemeData( + useMaterial3: false, + brightness: Brightness.dark, + primarySwatch: Colors.blueGrey, + ); + } + + ThemeData _buildLightTheme() { + return ThemeData( + useMaterial3: false, + brightness: Brightness.light, + colorSchemeSeed: Colors.blueGrey, + ); + } + + MultiBlocProvider _buildMultiBlocProvider(ThemeData light, ThemeData dark) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => AuthorityBloc(authorityRepository: AuthorityRepository())), + BlocProvider(create: (_) => AccountBloc(accountRepository: AccountRepository())), + BlocProvider(create: (_) => UserBloc(userRepository: UserRepository())), + BlocProvider(create: (_) => CityBloc(cityRepository: CityRepository())), + BlocProvider(create: (_) => DistrictBloc(districtRepository: DistrictRepository())), + BlocProvider(create: (_) => DrawerBloc(loginRepository: LoginRepository(), menuRepository: MenuRepository())), + ], + child: _buildGetMaterialApp(light, dark), + ); + } + + GetMaterialApp _buildGetMaterialApp(ThemeData light, ThemeData dark) { + return GetMaterialApp( + theme: light, + darkTheme: dark, + debugShowCheckedModeBanner: ProfileConstants.isDevelopment, + debugShowMaterialGrid: false, + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + locale: Locale(language), + initialRoute: initialRouteControl(), + routes: _initialRoutes, + ); + } + + final _initialRoutes = { + ApplicationRoutes.home: (context) { + return BlocProvider( + create: (context) => AccountBloc(accountRepository: AccountRepository())..add(const AccountLoad()), child: HomeScreen()); + }, + ApplicationRoutes.account: (context) { + return BlocProvider( + create: (context) => AccountBloc(accountRepository: AccountRepository())..add(const AccountLoad()), child: AccountsScreen()); + }, + ApplicationRoutes.login: (context) { + return BlocProvider(create: (context) => LoginBloc(loginRepository: LoginRepository()), child: LoginScreen()); + }, + ApplicationRoutes.settings: (context) { + return BlocProvider( + create: (context) => SettingsBloc(accountRepository: AccountRepository()), child: const SettingsScreen()); + }, + ApplicationRoutes.forgotPassword: (context) { + return BlocProvider( + create: (context) => ForgotPasswordBloc(accountRepository: AccountRepository()), child: ForgotPasswordScreen()); + }, + ApplicationRoutes.register: (context) { + return BlocProvider(create: (context) => RegisterBloc(accountRepository: AccountRepository()), child: RegisterScreen()); + }, + ApplicationRoutes.changePassword: (context) { + return BlocProvider( + create: (context) => ChangePasswordBloc(accountRepository: AccountRepository()), child: ChangePasswordScreen()); + }, + ApplicationRoutes.logout: (context) => const LogoutConfirmationDialog(), + ApplicationRoutes.createUser: (context) { + return BlocProvider(create: (context) => UserBloc(userRepository: UserRepository()), child: CreateUserScreen()); + }, + ApplicationRoutes.listUsers: (context) { + return BlocProvider(create: (context) => UserBloc(userRepository: UserRepository()), child: ListUserScreen()); + }, + }; } diff --git a/lib/main/main_local.dart b/lib/main/main_local.dart index 8c7932a..65dbd41 100644 --- a/lib/main/main_local.dart +++ b/lib/main/main_local.dart @@ -1,3 +1,4 @@ +import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc_advance/configuration/local_storage.dart'; @@ -16,10 +17,11 @@ void main() async { initializeJsonMapper(); WidgetsFlutterBinding.ensureInitialized(); const defaultLanguage = "en"; + const initialTheme = AdaptiveThemeMode.dark; await AppLocalStorage().save(StorageKeys.language.name, defaultLanguage); WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]).then((_) { - runApp(const App(language: defaultLanguage)); + runApp(App(language: defaultLanguage, initialTheme: initialTheme)); }); } diff --git a/lib/main/main_prod.dart b/lib/main/main_prod.dart index c1e3da3..3422423 100644 --- a/lib/main/main_prod.dart +++ b/lib/main/main_prod.dart @@ -1,3 +1,4 @@ +import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc_advance/configuration/environment.dart'; @@ -17,10 +18,11 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); const defaultLanguage = "en"; + const initialTheme = AdaptiveThemeMode.dark; await AppLocalStorage().save(StorageKeys.language.name, defaultLanguage); WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]).then((_) { - runApp(const App(language: defaultLanguage)); + runApp(App(language: defaultLanguage, initialTheme: initialTheme)); }); } diff --git a/lib/presentation/common_blocs/account/account_bloc.dart b/lib/presentation/common_blocs/account/account_bloc.dart index bacb11a..5c25e2d 100644 --- a/lib/presentation/common_blocs/account/account_bloc.dart +++ b/lib/presentation/common_blocs/account/account_bloc.dart @@ -33,6 +33,7 @@ class AccountBloc extends Bloc { try { User user = await _accountRepository.getAccount(); await AppLocalStorage().save(StorageKeys.roles.name, user.authorities); + await AppLocalStorage().save(StorageKeys.username.name, user.login); emit(state.copyWith(account: user, status: AccountStatus.success)); log("AccountBloc._onLoad end : ${state.account}, ${state.status}"); diff --git a/lib/presentation/common_widgets/drawer/drawer_widget.dart b/lib/presentation/common_widgets/drawer/drawer_widget.dart index 9d1d73b..ddd8432 100644 --- a/lib/presentation/common_widgets/drawer/drawer_widget.dart +++ b/lib/presentation/common_widgets/drawer/drawer_widget.dart @@ -2,6 +2,7 @@ import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:expansion_tile_card/expansion_tile_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_bloc_advance/configuration/app_key_constants.dart'; import 'package:flutter_bloc_advance/configuration/local_storage.dart'; import 'package:flutter_bloc_advance/utils/security_utils.dart'; import 'package:string_2_icon/string_2_icon.dart'; @@ -55,6 +56,7 @@ class ApplicationDrawer extends StatelessWidget { child: SizedBox( width: double.infinity, child: ElevatedButton( + key: drawerButtonLogout, style: ElevatedButton.styleFrom(elevation: 0), onPressed: () => logOutDialog(context), child: Text(S.of(context).logout, textAlign: TextAlign.center), @@ -83,13 +85,13 @@ class ApplicationDrawer extends StatelessWidget { ListTile _buildMenuListListTile(List parentMenus, int index, BuildContext context) { return ListTile( - leading: Icon(String2Icon.getIconDataFromString(parentMenus[index].icon)), - title: Text(S.of(context).translate_menu_title(parentMenus[index].name), style: Theme.of(context).textTheme.bodyMedium), - onTap: () { - Navigator.pop(context); - Navigator.pushNamed(context, parentMenus[index].url); - }, - ); + leading: Icon(String2Icon.getIconDataFromString(parentMenus[index].icon)), + title: Text(S.of(context).translate_menu_title(parentMenus[index].name), style: Theme.of(context).textTheme.bodyMedium), + onTap: () { + Navigator.pop(context); + Navigator.pushNamed(context, parentMenus[index].url); + }, + ); } ExpansionTileCard _buildMenuListUserManagement(DrawerState state, List parentMenus, int index, BuildContext context) { @@ -168,10 +170,12 @@ class ApplicationDrawer extends StatelessWidget { content: Text(S.of(context).logout_sure), actions: [ TextButton( + key: drawerButtonLogoutYes, onPressed: () => onLogout(context), child: Text(S.of(context).yes), ), TextButton( + key: drawerButtonLogoutNo, onPressed: () => onCancel(context), child: Text(S.of(context).no), ), diff --git a/lib/presentation/screen/home/home_screen.dart b/lib/presentation/screen/home/home_screen.dart index f7e2ad6..a51cbdb 100644 --- a/lib/presentation/screen/home/home_screen.dart +++ b/lib/presentation/screen/home/home_screen.dart @@ -1,11 +1,12 @@ import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_bloc_advance/configuration/constants.dart'; +import 'package:flutter_bloc_advance/utils/app_constants.dart'; import '../../../configuration/routes.dart'; import '../../../data/repository/login_repository.dart'; import '../../../data/repository/menu_repository.dart'; -import '../../../generated/l10n.dart'; import '../../common_blocs/account/account.dart'; import '../../common_widgets/drawer/drawer_bloc/drawer_bloc.dart'; import '../../common_widgets/drawer/drawer_widget.dart'; @@ -32,7 +33,7 @@ class HomeScreen extends StatelessWidget { builder: (context, state) { if (state.status == AccountStatus.success) { return Scaffold( - appBar: AppBar(title: Text(S.of(context).description)), + appBar: AppBar(title: const Text(AppConstants.appName)), key: _scaffoldKey, body: Center(child: Column(children: [backgroundImage(context)])), drawer: _buildDrawer(context), @@ -54,7 +55,7 @@ class HomeScreen extends StatelessWidget { height: 300, width: 300, decoration: const BoxDecoration( - image: DecorationImage(image: AssetImage("assets/images/logoLight.png"), scale: 1, fit: BoxFit.contain), + image: DecorationImage(image: AssetImage(LocaleConstants.logoLightUrl), scale: 1, fit: BoxFit.contain), ), ), ), @@ -68,7 +69,7 @@ class HomeScreen extends StatelessWidget { width: double.infinity, decoration: BoxDecoration( image: DecorationImage( - image: const AssetImage("assets/images/img.png"), + image: const AssetImage(LocaleConstants.defaultImgUrl), colorFilter: ColorFilter.mode( AdaptiveTheme.of(context).mode.isDark ? Colors.black.withOpacity(0.1) : Colors.white.withOpacity(0.1), BlendMode.dstIn), ), diff --git a/test/presentation/screen/Home/home_screen_test.dart b/test/presentation/screen/Home/home_screen_test.dart new file mode 100644 index 0000000..bc8c019 --- /dev/null +++ b/test/presentation/screen/Home/home_screen_test.dart @@ -0,0 +1,166 @@ +import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc_advance/configuration/app_key_constants.dart'; +import 'package:flutter_bloc_advance/configuration/local_storage.dart'; +import 'package:flutter_bloc_advance/main/app.dart'; +import 'package:flutter_bloc_advance/presentation/common_widgets/drawer/drawer_widget.dart'; +import 'package:flutter_bloc_advance/presentation/screen/home/home_screen.dart'; +import 'package:flutter_bloc_advance/presentation/screen/login/login_screen.dart'; +import 'package:flutter_bloc_advance/utils/app_constants.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../test_utils.dart'; + +void main() { + //region setup + setUp(() async { + await TestUtils().setupUnitTest(); + }); + tearDown(() async { + await TestUtils().tearDownUnitTest(); + TestWidgetsFlutterBinding.instance.reset(); + }); + + const language = "en"; + const darkTheme = AdaptiveThemeMode.dark; + const lightTheme = AdaptiveThemeMode.light; + + //endregion setup + + // main application unittest end-to-end + group("HomeScreen Test Most critical APP UnitTest ***** ", () { + testWidgets("Given valid AccessToken and lightTheme when open homeScreen then load AppBar successfully", (tester) async { + TestUtils().setupAuthentication(); + + // Given: + await tester.pumpWidget(App(language: language, initialTheme: lightTheme).buildHomeApp()); + //When: + await tester.pumpAndSettle(const Duration(seconds: 5)); + //Then: + + // appBar test + debugPrint("AppBar Testing"); + expect(find.byType(AppBar), findsOneWidget); + expect(find.text(AppConstants.appName), findsOneWidget); + expect(find.byType(DrawerButton), findsOneWidget); + debugPrint("AppBar Tested"); + // tester.allWidgets.forEach((e) { + // print(e.toString()); + // }); + + debugPrint("Menu finder Testing"); + // menu finder + final drawerButtonFinder = find.byType(DrawerButton); + await tester.tap(drawerButtonFinder); + await tester.pumpAndSettle(const Duration(seconds: 5)); + debugPrint("drawerButton PumpAndSettle"); + + debugPrint("Menu list Testing"); + // Menu Test + expect(find.byType(Drawer), findsOneWidget); + expect(find.byType(ThemeSwitchButton), findsOneWidget); + expect(find.byType(LanguageSwitchButton), findsOneWidget); + expect(find.text("Logout"), findsOneWidget); + expect(find.text("Account"), findsOneWidget); + expect(find.text("Settings"), findsOneWidget); + debugPrint("Menu list Tested"); + + // storage and cache test + debugPrint("storage Testing"); + String? sLang = await AppLocalStorage().read(StorageKeys.language.name); + String? username = await AppLocalStorage().read(StorageKeys.username.name); + List? authorities = await AppLocalStorage().read(StorageKeys.roles.name); + expect(sLang, "en"); + expect(username, "admin"); + expect(authorities, ["ROLE_ADMIN", "ROLE_USER"]); + debugPrint("storage tested"); + + // language test + final langFinder = find.byType(LanguageSwitchButton); + await tester.tap(langFinder); + await tester.pumpAndSettle(const Duration(seconds: 5)); + // open menu + await tester.tap(drawerButtonFinder); + await tester.pumpAndSettle(const Duration(seconds: 5)); + debugPrint("drawerButton PumpAndSettle"); + + await tester.tap(langFinder); + await tester.pumpAndSettle(const Duration(seconds: 5)); + + debugPrint("language tested"); + ///////////////////////////////////////////////////////// + + // open menu + await tester.tap(drawerButtonFinder); + await tester.pumpAndSettle(const Duration(seconds: 5)); + debugPrint("drawerButton PumpAndSettle"); + + //theme test + final themeFinder = find.byType(ThemeSwitchButton); + await tester.tap(themeFinder); + await tester.pumpAndSettle(const Duration(seconds: 5)); + debugPrint("ThemeSwitchButton PumpAndSettle"); + + // open menu + await tester.tap(drawerButtonFinder); + await tester.pumpAndSettle(const Duration(seconds: 5)); + debugPrint("drawerButton PumpAndSettle"); + + // logout test alert button No + final logoutFinder = find.byKey(drawerButtonLogout); + await tester.tap(logoutFinder); + await tester.pumpAndSettle(const Duration(seconds: 5)); + final noButtonFinder = find.byKey(drawerButtonLogoutNo); + await tester.tap(noButtonFinder); + await tester.pumpAndSettle(const Duration(seconds: 5)); + + expect(find.byType(HomeScreen), findsOneWidget); + expect(find.byType(Drawer), findsOneWidget); + debugPrint("LogoutButton No PumpAndSettle"); + + // logout test alert button yes + await tester.tap(logoutFinder); + await tester.pumpAndSettle(const Duration(seconds: 5)); + final yesButtonFinder = find.byKey(drawerButtonLogoutYes); + await tester.tap(yesButtonFinder); + await tester.pumpAndSettle(const Duration(seconds: 5)); + + debugPrint("LogoutButton YES PumpAndSettle"); + + // tester.allWidgets.forEach((e) { + // print(e.toString()); + // }); + + // clear storage test + sLang = await AppLocalStorage().read(StorageKeys.language.name); + username = await AppLocalStorage().read(StorageKeys.username.name); + authorities = await AppLocalStorage().read(StorageKeys.roles.name); + expect(sLang, null); + expect(username, null); + expect(authorities, null); + + // dispose test + expect(find.byType(HomeScreen), findsNothing); + expect(find.byType(LoginScreen), findsOneWidget); + }); + + testWidgets("Given an invalid AccessToken when HomeScreen is opened then navigate to loginScreen", (tester) async { + AdaptiveTheme getWidget({AdaptiveThemeMode mode = AdaptiveThemeMode.dark}) => App(language: language, initialTheme: mode).buildHomeApp(); + + // Given: + await tester.pumpWidget(getWidget(mode: darkTheme)); + //When: + await tester.pumpAndSettle(); + //Then: + expect(find.byType(HomeScreen), findsNothing); + expect(find.byType(LoginScreen), findsOneWidget); + }); + + testWidgets(skip: true, "Given valid token when open Drawer menu then open successfully", (tester) async {}); + + // Validate theme + + // home page image validation + + }); +}