From a38f1560b13ac0cf3aa65809e9f40ac60eb95b84 Mon Sep 17 00:00:00 2001 From: Conner Date: Sat, 2 Nov 2024 20:22:57 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20Data=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=EC=9D=98=20dio=20=EA=B0=9D=EC=B2=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85=20=EB=B0=9B?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/utils/injections.dart | 6 ++++++ .../account/data/data_sources/mock/mock_api.dart | 10 ++++++++-- lib/feature/account/data/data_sources/remote/api.dart | 3 +-- .../account/data/data_sources/remote/api_impl.dart | 8 ++++++-- lib/feature/account/domain/domain.dart | 2 +- lib/feature/account/init_injections.dart | 4 +++- lib/feature/splash/presentation/bloc/splash_bloc.dart | 6 ++++++ pubspec.lock | 10 +++++++++- pubspec.yaml | 10 +++++++--- 9 files changed, 47 insertions(+), 12 deletions(-) diff --git a/lib/core/utils/injections.dart b/lib/core/utils/injections.dart index 9b0cdcd..a2d15f3 100644 --- a/lib/core/utils/injections.dart +++ b/lib/core/utils/injections.dart @@ -1,11 +1,17 @@ import 'package:get_it/get_it.dart'; +import 'package:withu_app/core/core.dart'; import 'package:withu_app/feature/account/account.dart'; import 'package:withu_app/feature/job_posting/init_injections.dart'; import 'package:withu_app/feature/splash/splash.dart'; final getIt = GetIt.instance; +void initCommonInjections() { + getIt.registerSingleton(API()); +} + Future initInjections() async { + initCommonInjections(); initAccountInjections(); initSplashInjections(); initJobPostingInjections(); diff --git a/lib/feature/account/data/data_sources/mock/mock_api.dart b/lib/feature/account/data/data_sources/mock/mock_api.dart index 01b864a..ae05f9d 100644 --- a/lib/feature/account/data/data_sources/mock/mock_api.dart +++ b/lib/feature/account/data/data_sources/mock/mock_api.dart @@ -1,9 +1,15 @@ import 'dart:async'; - +import 'package:http_mock_adapter/http_mock_adapter.dart'; import 'package:withu_app/core/core.dart'; import 'package:withu_app/feature/account/account.dart'; -class AccountMockApi extends AccountApiImpl with MockAPI { +class AccountMockApi extends AccountApiImpl { + late final DioAdapter dioAdapter; + + AccountMockApi({required super.api}) { + dioAdapter = DioAdapter(dio: api.dio); + } + /// 로그인 API @override FutureOr> login({ diff --git a/lib/feature/account/data/data_sources/remote/api.dart b/lib/feature/account/data/data_sources/remote/api.dart index 0870b48..b92aaa3 100644 --- a/lib/feature/account/data/data_sources/remote/api.dart +++ b/lib/feature/account/data/data_sources/remote/api.dart @@ -1,9 +1,8 @@ import 'dart:async'; - import 'package:withu_app/core/core.dart'; import 'package:withu_app/feature/account/data/data_sources/dto/dto.dart'; -abstract class AccountApi extends API { +abstract class AccountApi { /// API 주소 final path = "/api/account"; diff --git a/lib/feature/account/data/data_sources/remote/api_impl.dart b/lib/feature/account/data/data_sources/remote/api_impl.dart index 4c71bb9..335bb16 100644 --- a/lib/feature/account/data/data_sources/remote/api_impl.dart +++ b/lib/feature/account/data/data_sources/remote/api_impl.dart @@ -1,15 +1,19 @@ import 'dart:async'; -import 'package:withu_app/core/network/api_response.dart'; +import 'package:withu_app/core/core.dart'; import 'package:withu_app/feature/account/data/data_sources/dto/dto.dart'; import 'api.dart'; class AccountApiImpl extends AccountApi { + final API api; + + AccountApiImpl({required this.api}); + /// 로그인 API @override FutureOr> login({ required LoginRequestDto requestData, }) async { - return dio + return api.dio .post( loginPath, data: requestData.toJson(), diff --git a/lib/feature/account/domain/domain.dart b/lib/feature/account/domain/domain.dart index 483eec9..ac95599 100644 --- a/lib/feature/account/domain/domain.dart +++ b/lib/feature/account/domain/domain.dart @@ -1,3 +1,3 @@ export 'usecase/usecase.dart'; -export 'repository//repository.dart'; +export 'repository/repository.dart'; export 'entity/entity.dart'; diff --git a/lib/feature/account/init_injections.dart b/lib/feature/account/init_injections.dart index 16f01b7..7d002ea 100644 --- a/lib/feature/account/init_injections.dart +++ b/lib/feature/account/init_injections.dart @@ -3,7 +3,9 @@ import 'package:withu_app/feature/account/account.dart'; void initAccountInjections() { getIt.registerSingleton( - Environment.isProd ? AccountApiImpl() : AccountMockApi(), + Environment.isProd + ? AccountApiImpl(api: getIt()) + : AccountMockApi(api: getIt()), ); getIt.registerSingleton( AccountStorageImpl(), diff --git a/lib/feature/splash/presentation/bloc/splash_bloc.dart b/lib/feature/splash/presentation/bloc/splash_bloc.dart index eaf4d03..f575fb7 100644 --- a/lib/feature/splash/presentation/bloc/splash_bloc.dart +++ b/lib/feature/splash/presentation/bloc/splash_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:withu_app/core/core.dart'; import 'package:withu_app/feature/account/account.dart'; @@ -28,8 +29,13 @@ class SplashBloc extends BaseBloc { // 1초 대기 후 홈 화면으로 이동. await Future.delayed(const Duration(seconds: 1)); + final instance = await SharedPreferences.getInstance(); + instance.clear(); + final isLoggedIn = await accountUseCase.checkLogin(); + logger.i(isLoggedIn); + emit(state.copyWith( status: BaseBlocStatus.success(), isLoggedIn: isLoggedIn, diff --git a/pubspec.lock b/pubspec.lock index 413c8fe..f7e2cdc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -483,7 +483,7 @@ packages: source: hosted version: "1.2.2" http_mock_adapter: - dependency: "direct dev" + dependency: "direct main" description: name: http_mock_adapter sha256: "46399c78bd4a0af071978edd8c502d7aeeed73b5fb9860bca86b5ed647a63c1b" @@ -650,6 +650,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + mockito: + dependency: "direct main" + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c051c59..a45f2be 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,12 +44,19 @@ dependencies: intl: ^0.19.0 easy_localization: ^3.0.7 + # Test Mocking + mockito: ^5.4.4 + + # api mocking + http_mock_adapter: ^0.6.1 + # widget related packages infinite_scroll_pagination: ^4.0.0 calendar_date_picker2: ^1.1.7 flutter_keyboard_visibility: ^6.0.0 shared_preferences: ^2.3.2 + dev_dependencies: flutter_test: sdk: flutter @@ -68,9 +75,6 @@ dev_dependencies: # Resource manager flutter_gen_runner: - # api mocking - http_mock_adapter: ^0.6.1 - # Svg flutter_svg: From 37030c4047d1ea0bc99a1b6a1fbf3ed5dd71b7ed Mon Sep 17 00:00:00 2001 From: Conner Date: Sat, 2 Nov 2024 20:34:53 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20Api=20=EC=97=90=EC=84=9C=20DioNet?= =?UTF-8?q?work=EB=A1=9C=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/network/{api.dart => dio_network.dart} | 2 +- lib/core/network/mock_api.dart | 4 ++-- lib/core/network/network.dart | 2 +- lib/core/utils/injections.dart | 2 +- lib/feature/account/data/data_sources/mock/mock_api.dart | 4 ++-- lib/feature/account/data/data_sources/remote/api_impl.dart | 6 +++--- lib/feature/account/init_injections.dart | 4 ++-- .../data/data_sources/remote/job_posting_api.dart | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) rename lib/core/network/{api.dart => dio_network.dart} (98%) diff --git a/lib/core/network/api.dart b/lib/core/network/dio_network.dart similarity index 98% rename from lib/core/network/api.dart rename to lib/core/network/dio_network.dart index 57ee881..894dc2c 100644 --- a/lib/core/network/api.dart +++ b/lib/core/network/dio_network.dart @@ -3,7 +3,7 @@ import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:withu_app/feature/account/account.dart'; -class API { +class DioNetwork { final String url = 'https://example.com'; late final _dio = Dio(BaseOptions( diff --git a/lib/core/network/mock_api.dart b/lib/core/network/mock_api.dart index ebe0c8c..f77e6ef 100644 --- a/lib/core/network/mock_api.dart +++ b/lib/core/network/mock_api.dart @@ -1,8 +1,8 @@ import 'package:http_mock_adapter/http_mock_adapter.dart'; -import 'api.dart'; +import 'dio_network.dart'; -mixin MockAPI on API { +mixin MockAPI on DioNetwork { late final DioAdapter _dioAdapter = DioAdapter(dio: dio); DioAdapter get dioAdapter => _dioAdapter; diff --git a/lib/core/network/network.dart b/lib/core/network/network.dart index 4f63c58..b1e6ea4 100644 --- a/lib/core/network/network.dart +++ b/lib/core/network/network.dart @@ -1,4 +1,4 @@ -export 'api.dart'; +export 'dio_network.dart'; export 'mock_api.dart'; export 'api_response.dart'; export 'dto/dto.dart'; diff --git a/lib/core/utils/injections.dart b/lib/core/utils/injections.dart index a2d15f3..aa88c2f 100644 --- a/lib/core/utils/injections.dart +++ b/lib/core/utils/injections.dart @@ -7,7 +7,7 @@ import 'package:withu_app/feature/splash/splash.dart'; final getIt = GetIt.instance; void initCommonInjections() { - getIt.registerSingleton(API()); + getIt.registerSingleton(DioNetwork()); } Future initInjections() async { diff --git a/lib/feature/account/data/data_sources/mock/mock_api.dart b/lib/feature/account/data/data_sources/mock/mock_api.dart index ae05f9d..38d5683 100644 --- a/lib/feature/account/data/data_sources/mock/mock_api.dart +++ b/lib/feature/account/data/data_sources/mock/mock_api.dart @@ -6,8 +6,8 @@ import 'package:withu_app/feature/account/account.dart'; class AccountMockApi extends AccountApiImpl { late final DioAdapter dioAdapter; - AccountMockApi({required super.api}) { - dioAdapter = DioAdapter(dio: api.dio); + AccountMockApi({required super.network}) { + dioAdapter = DioAdapter(dio: network.dio); } /// 로그인 API diff --git a/lib/feature/account/data/data_sources/remote/api_impl.dart b/lib/feature/account/data/data_sources/remote/api_impl.dart index 335bb16..9833755 100644 --- a/lib/feature/account/data/data_sources/remote/api_impl.dart +++ b/lib/feature/account/data/data_sources/remote/api_impl.dart @@ -4,16 +4,16 @@ import 'package:withu_app/feature/account/data/data_sources/dto/dto.dart'; import 'api.dart'; class AccountApiImpl extends AccountApi { - final API api; + final DioNetwork network; - AccountApiImpl({required this.api}); + AccountApiImpl({required this.network}); /// 로그인 API @override FutureOr> login({ required LoginRequestDto requestData, }) async { - return api.dio + return network.dio .post( loginPath, data: requestData.toJson(), diff --git a/lib/feature/account/init_injections.dart b/lib/feature/account/init_injections.dart index 7d002ea..953171f 100644 --- a/lib/feature/account/init_injections.dart +++ b/lib/feature/account/init_injections.dart @@ -4,8 +4,8 @@ import 'package:withu_app/feature/account/account.dart'; void initAccountInjections() { getIt.registerSingleton( Environment.isProd - ? AccountApiImpl(api: getIt()) - : AccountMockApi(api: getIt()), + ? AccountApiImpl(network: getIt()) + : AccountMockApi(network: getIt()), ); getIt.registerSingleton( AccountStorageImpl(), diff --git a/lib/feature/job_posting/data/data_sources/remote/job_posting_api.dart b/lib/feature/job_posting/data/data_sources/remote/job_posting_api.dart index 11f57e7..f9414bc 100644 --- a/lib/feature/job_posting/data/data_sources/remote/job_posting_api.dart +++ b/lib/feature/job_posting/data/data_sources/remote/job_posting_api.dart @@ -3,7 +3,7 @@ import 'package:withu_app/core/core.dart'; import 'package:withu_app/feature/job_posting/data/data.dart'; import 'package:withu_app/shared/data/data.dart'; -abstract class JobPostingApi extends API { +abstract class JobPostingApi extends DioNetwork { final String path = '/job-postings'; /// 공고 목록 From 999bd83fcfaf0b7b2de498f828867de9529627e6 Mon Sep 17 00:00:00 2001 From: Conner Date: Sat, 2 Nov 2024 23:10:23 +0900 Subject: [PATCH 03/17] =?UTF-8?q?test:=20Account=20Domain=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + lib/core/network/api_response.dart | 6 ++ .../dto/login/request/login_request_dto.dart | 2 + .../login/request/login_request_dto.mock.dart | 12 +++ .../response/login_response_dto.mock.dart | 2 +- .../data/data_sources/remote/api_impl.dart | 4 +- .../data/data_source/remote/api_test.dart | 97 +++++++++++++++++++ .../repository/repository_impl_test.dart | 81 ++++++++++++++++ .../account/domain/usecase/usecase_test.dart | 52 ++++++++++ 9 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 lib/feature/account/data/data_sources/dto/login/request/login_request_dto.mock.dart create mode 100644 test/feature/account/data/data_source/remote/api_test.dart create mode 100644 test/feature/account/data/data_source/repository/repository_impl_test.dart create mode 100644 test/feature/account/domain/usecase/usecase_test.dart diff --git a/.gitignore b/.gitignore index 36d25cd..596d6db 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,7 @@ app.*.map.json # Freezed generated files *.freezed.dart *.g.dart + + +# Mockito generated file +*.mocks.dart diff --git a/lib/core/network/api_response.dart b/lib/core/network/api_response.dart index 0adf060..0eb55d4 100644 --- a/lib/core/network/api_response.dart +++ b/lib/core/network/api_response.dart @@ -53,4 +53,10 @@ extension ApiResponseExt on ApiResponse { success: (T data) => data, orElse: () => null, ); + + /// Fail Data 가져오기 + FailResponse? get failData => maybeWhen( + fail: (FailResponse failResponse) => failResponse, + orElse: () => null, + ); } diff --git a/lib/feature/account/data/data_sources/dto/login/request/login_request_dto.dart b/lib/feature/account/data/data_sources/dto/login/request/login_request_dto.dart index ef446ce..4740d42 100644 --- a/lib/feature/account/data/data_sources/dto/login/request/login_request_dto.dart +++ b/lib/feature/account/data/data_sources/dto/login/request/login_request_dto.dart @@ -5,6 +5,8 @@ part 'login_request_dto.freezed.dart'; part 'login_request_dto.g.dart'; +part 'login_request_dto.mock.dart'; + @freezed class LoginRequestDto with _$LoginRequestDto { factory LoginRequestDto({ diff --git a/lib/feature/account/data/data_sources/dto/login/request/login_request_dto.mock.dart b/lib/feature/account/data/data_sources/dto/login/request/login_request_dto.mock.dart new file mode 100644 index 0000000..d7379b7 --- /dev/null +++ b/lib/feature/account/data/data_sources/dto/login/request/login_request_dto.mock.dart @@ -0,0 +1,12 @@ +part of 'login_request_dto.dart'; + +extension LoginRequestDtoMock on LoginRequestDto { + static LoginRequestDto mock() { + return LoginRequestDto( + accountType: AccountType.company, + loginType: LoginType.email, + loginId: 'test@test.com', + password: '123qwe!@', + ); + } +} diff --git a/lib/feature/account/data/data_sources/dto/login/response/login_response_dto.mock.dart b/lib/feature/account/data/data_sources/dto/login/response/login_response_dto.mock.dart index 5adf6b7..bf2fdda 100644 --- a/lib/feature/account/data/data_sources/dto/login/response/login_response_dto.mock.dart +++ b/lib/feature/account/data/data_sources/dto/login/response/login_response_dto.mock.dart @@ -14,7 +14,7 @@ extension LoginResponseDtoMock on LoginResponseDto { ); } - static LoginResponseDto fail() { + static LoginResponseDto failure() { return LoginResponseDto( status: false, message: "존재하지 않는 계정입니다.", diff --git a/lib/feature/account/data/data_sources/remote/api_impl.dart b/lib/feature/account/data/data_sources/remote/api_impl.dart index 9833755..c0eac95 100644 --- a/lib/feature/account/data/data_sources/remote/api_impl.dart +++ b/lib/feature/account/data/data_sources/remote/api_impl.dart @@ -21,6 +21,8 @@ class AccountApiImpl extends AccountApi { .then((response) => ApiResponse.success( LoginResponseDto.fromJson(response.data), )) - .catchError((_) => const ApiResponse.error()); + .catchError( + (_) => ApiResponse.fail(FailResponse.error()), + ); } } diff --git a/test/feature/account/data/data_source/remote/api_test.dart b/test/feature/account/data/data_source/remote/api_test.dart new file mode 100644 index 0000000..ddda640 --- /dev/null +++ b/test/feature/account/data/data_source/remote/api_test.dart @@ -0,0 +1,97 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/account/account.dart'; + +import 'api_test.mocks.dart'; + +@GenerateMocks([DioNetwork, Dio]) +void main() { + late MockDio mockDio; + late MockDioNetwork mockDioNetwork; + late AccountApi api; + final LoginRequestDto requestData = LoginRequestDtoMock.mock(); + + setUp(() { + mockDio = MockDio(); + mockDioNetwork = MockDioNetwork(); + + // mockDioNetwork.dio가 호출될 때 mockDio를 반환하도록 설정 + when(mockDioNetwork.dio).thenReturn(mockDio); + + api = AccountApiImpl(network: mockDioNetwork); + }); + + group('AccountAPI 테스트', () { + test('로그인 요청 성공', () async { + // Given + when( + mockDio.post( + api.loginPath, + data: requestData.toJson(), + ), + ).thenAnswer((_) async { + return Response( + data: LoginResponseDtoMock.success().toJson(), + statusCode: 200, + requestOptions: RequestOptions(), + ); + }); + + // When + final result = await api.login(requestData: requestData); + + // Then + expect(result, ApiResponse.success(LoginResponseDtoMock.success())); + expect(result.successData?.status, true); + expect(result.successData?.loginId, 'test@test.com'); + }); + + test('로그인 실패', () async { + // Given + when( + mockDio.post( + api.loginPath, + data: requestData.toJson(), + ), + ).thenAnswer((_) async { + return Response( + data: LoginResponseDtoMock.failure().toJson(), + statusCode: 200, + requestOptions: RequestOptions(), + ); + }); + + // When + final result = await api.login(requestData: requestData); + + // Then + expect(result, ApiResponse.success(LoginResponseDtoMock.failure())); + expect(result.successData?.status, false); + }); + + test('API 500 에러', () async { + // Given + when( + mockDio.post( + api.loginPath, + data: requestData.toJson(), + ), + ).thenAnswer((_) async { + return Response( + data: {}, + statusCode: 500, + requestOptions: RequestOptions(), + ); + }); + + // When + final result = await api.login(requestData: requestData); + + // Then + expect(result, ApiResponse.fail(FailResponse.error())); + }); + }); +} diff --git a/test/feature/account/data/data_source/repository/repository_impl_test.dart b/test/feature/account/data/data_source/repository/repository_impl_test.dart new file mode 100644 index 0000000..891a80b --- /dev/null +++ b/test/feature/account/data/data_source/repository/repository_impl_test.dart @@ -0,0 +1,81 @@ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/account/account.dart'; + +import 'repository_impl_test.mocks.dart'; + +@GenerateMocks([AccountApiImpl, AccountStorageImpl]) +void main() { + late AccountApi api; + late AccountRepository repo; + final LoginRequestDto requestData = LoginRequestDtoMock.mock(); + + setUp(() { + api = MockAccountApiImpl(); + repo = AccountRepositoryImpl( + accountApi: api, + accountStorage: MockAccountStorageImpl(), + ); + }); + group('AccountRepository 테스트', () { + test('로그인 성공', () async { + // Given + when( + api.login(requestData: requestData), + ).thenAnswer( + (_) async => ApiResponse.success( + LoginResponseDtoMock.success(), + ), + ); + + // When + final result = await repo.login(requestData: requestData); + + // Then + expect(result, ApiResponse.success(LoginResponseDtoMock.success())); + expect(result.successData?.status, true); + expect(result.successData?.loginId, 'test@test.com'); + }); + + test('로그인 실패', () async { + // Given + when( + api.login(requestData: requestData), + ).thenAnswer( + (_) async => ApiResponse.fail( + FailResponse.fromJson( + LoginResponseDtoMock.failure().toJson(), + ), + ), + ); + + // When + final result = await repo.login(requestData: requestData); + + // Then + expect(result.isSuccess, true); + expect(result.successData?.status, false); + }); + + test('로그인 요청 API 500 에러', () async { + // Given + when( + api.login(requestData: requestData), + ).thenAnswer( + (_) async => ApiResponse.fail( + FailResponse.error(), + ), + ); + + // When + final result = await repo.login(requestData: requestData); + + // Then + expect(result.isSuccess, false); + expect(result.failData?.message, '서버 에러'); + }); + }); +} diff --git a/test/feature/account/domain/usecase/usecase_test.dart b/test/feature/account/domain/usecase/usecase_test.dart new file mode 100644 index 0000000..b02e442 --- /dev/null +++ b/test/feature/account/domain/usecase/usecase_test.dart @@ -0,0 +1,52 @@ +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/account/account.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'usecase_test.mocks.dart'; + +@GenerateMocks([AccountRepository]) +void main() { + late MockAccountRepository mockRepo; + late AccountUseCase useCase; + + setUp(() { + mockRepo = MockAccountRepository(); + useCase = AccountUseCaseImpl(accountRepo: mockRepo); + }); + + test('로그인 성공 테스트', () async { + final requestDto = LoginRequestDto( + accountType: AccountType.company, + loginType: LoginType.email, + loginId: 'test@test.com', + password: '123qwe!@', + ); + + final successResponseDto = LoginResponseDtoMock.success(); + + // arrange + when( + mockRepo.login(requestData: requestDto), + ).thenAnswer( + (_) async => ApiResponse.success(successResponseDto), + ); + + // act + final result = await useCase.login( + accountType: requestDto.accountType, + loginType: requestDto.loginType, + loginId: requestDto.loginId, + password: requestDto.password, + ); + + // assert + expect( + result, + LoginResultEntity( + isLoggedIn: true, + message: successResponseDto.message, + ), + ); + }); +} From 7537968f16867a74cda47af3d4219fd685c645c6 Mon Sep 17 00:00:00 2001 From: Conner Date: Sat, 2 Nov 2024 23:51:54 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20Login=20UseCase=EB=A5=BC=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=95=98=EA=B8=B0=20=EC=A2=8B?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. LoginRequestEntity 추가 2. LoginRequestEntity로 파라미터 변경 3. LoginRequestEntity를 위한 Converter 추가 --- lib/feature/account/domain/entity/entity.dart | 2 +- .../account/domain/entity/login/login.dart | 2 ++ .../login_request_entity.converter.dart | 13 +++++++++++++ .../login/request/login_request_entity.dart | 19 +++++++++++++++++++ .../request/login_request_entity.mock.dart | 12 ++++++++++++ .../login_result_entity.converter.dart} | 2 +- .../{ => result}/login_result_entity.dart | 2 +- .../account/domain/usecase/usecase.dart | 5 +---- .../account/domain/usecase/usecase_impl.dart | 15 ++++----------- .../presentation/bloc/login/login_bloc.dart | 2 ++ .../bloc/login/login_bloc.handler.dart | 5 +---- .../bloc/login/login_bloc_converter.dart | 12 ++++++++++++ 12 files changed, 69 insertions(+), 22 deletions(-) create mode 100644 lib/feature/account/domain/entity/login/login.dart create mode 100644 lib/feature/account/domain/entity/login/request/login_request_entity.converter.dart create mode 100644 lib/feature/account/domain/entity/login/request/login_request_entity.dart create mode 100644 lib/feature/account/domain/entity/login/request/login_request_entity.mock.dart rename lib/feature/account/domain/entity/login/{login_result_entity.parser.dart => result/login_result_entity.converter.dart} (91%) rename lib/feature/account/domain/entity/login/{ => result}/login_result_entity.dart (91%) create mode 100644 lib/feature/account/presentation/bloc/login/login_bloc_converter.dart diff --git a/lib/feature/account/domain/entity/entity.dart b/lib/feature/account/domain/entity/entity.dart index 9cb90a9..fdde49e 100644 --- a/lib/feature/account/domain/entity/entity.dart +++ b/lib/feature/account/domain/entity/entity.dart @@ -1 +1 @@ -export 'login/login_result_entity.dart'; +export 'login/login.dart'; diff --git a/lib/feature/account/domain/entity/login/login.dart b/lib/feature/account/domain/entity/login/login.dart new file mode 100644 index 0000000..abdef22 --- /dev/null +++ b/lib/feature/account/domain/entity/login/login.dart @@ -0,0 +1,2 @@ +export 'request/login_request_entity.dart'; +export 'result/login_result_entity.dart'; diff --git a/lib/feature/account/domain/entity/login/request/login_request_entity.converter.dart b/lib/feature/account/domain/entity/login/request/login_request_entity.converter.dart new file mode 100644 index 0000000..e743ef3 --- /dev/null +++ b/lib/feature/account/domain/entity/login/request/login_request_entity.converter.dart @@ -0,0 +1,13 @@ +part of 'login_request_entity.dart'; + +extension LoginRequestEntityConverter on LoginRequestEntity { + /// entity -> dto + LoginRequestDto toDto() { + return LoginRequestDto( + accountType: accountType, + loginType: loginType, + loginId: loginId, + password: password, + ); + } +} diff --git a/lib/feature/account/domain/entity/login/request/login_request_entity.dart b/lib/feature/account/domain/entity/login/request/login_request_entity.dart new file mode 100644 index 0000000..8d0ae12 --- /dev/null +++ b/lib/feature/account/domain/entity/login/request/login_request_entity.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/account/data/data_sources/dto/login/request/login_request_dto.dart'; + +part 'login_request_entity.freezed.dart'; + +part 'login_request_entity.mock.dart'; + +part 'login_request_entity.converter.dart'; + +@freezed +class LoginRequestEntity with _$LoginRequestEntity { + factory LoginRequestEntity({ + required AccountType accountType, + required LoginType loginType, + required String loginId, + required String password, + }) = _LoginRequestEntity; +} diff --git a/lib/feature/account/domain/entity/login/request/login_request_entity.mock.dart b/lib/feature/account/domain/entity/login/request/login_request_entity.mock.dart new file mode 100644 index 0000000..37d5c21 --- /dev/null +++ b/lib/feature/account/domain/entity/login/request/login_request_entity.mock.dart @@ -0,0 +1,12 @@ +part of 'login_request_entity.dart'; + +extension LoginRequestEntityMock on LoginRequestEntity { + static LoginRequestEntity mock() { + return LoginRequestEntity( + accountType: AccountType.company, + loginType: LoginType.email, + loginId: 'test@test.com', + password: '123qwe!@', + ); + } +} diff --git a/lib/feature/account/domain/entity/login/login_result_entity.parser.dart b/lib/feature/account/domain/entity/login/result/login_result_entity.converter.dart similarity index 91% rename from lib/feature/account/domain/entity/login/login_result_entity.parser.dart rename to lib/feature/account/domain/entity/login/result/login_result_entity.converter.dart index a4ff6b4..0b20749 100644 --- a/lib/feature/account/domain/entity/login/login_result_entity.parser.dart +++ b/lib/feature/account/domain/entity/login/result/login_result_entity.converter.dart @@ -1,6 +1,6 @@ part of 'login_result_entity.dart'; -extension LoginResultEntityParser on LoginResultEntity { +extension LoginResultEntityConverter on LoginResultEntity { /// isLoggedIn -> Bloc Status BaseBlocStatus get blocStatus => isLoggedIn ? BaseBlocStatus.success() : BaseBlocStatus.failure(); diff --git a/lib/feature/account/domain/entity/login/login_result_entity.dart b/lib/feature/account/domain/entity/login/result/login_result_entity.dart similarity index 91% rename from lib/feature/account/domain/entity/login/login_result_entity.dart rename to lib/feature/account/domain/entity/login/result/login_result_entity.dart index 4dfcc16..7503060 100644 --- a/lib/feature/account/domain/entity/login/login_result_entity.dart +++ b/lib/feature/account/domain/entity/login/result/login_result_entity.dart @@ -4,7 +4,7 @@ import 'package:withu_app/feature/account/data/data_sources/dto/login/response/l part 'login_result_entity.freezed.dart'; -part 'login_result_entity.parser.dart'; +part 'login_result_entity.converter.dart'; @freezed class LoginResultEntity with _$LoginResultEntity { diff --git a/lib/feature/account/domain/usecase/usecase.dart b/lib/feature/account/domain/usecase/usecase.dart index 24407f8..16dfded 100644 --- a/lib/feature/account/domain/usecase/usecase.dart +++ b/lib/feature/account/domain/usecase/usecase.dart @@ -10,10 +10,7 @@ abstract class AccountUseCase { /// 로그인 Future login({ - required AccountType accountType, - required LoginType loginType, - required String loginId, - required String password, + required LoginRequestEntity entity, }); /// 로그인 여부 diff --git a/lib/feature/account/domain/usecase/usecase_impl.dart b/lib/feature/account/domain/usecase/usecase_impl.dart index 8ccd814..d9bb900 100644 --- a/lib/feature/account/domain/usecase/usecase_impl.dart +++ b/lib/feature/account/domain/usecase/usecase_impl.dart @@ -9,22 +9,15 @@ class AccountUseCaseImpl implements AccountUseCase { /// 로그인 @override Future login({ - required AccountType accountType, - required LoginType loginType, - required String loginId, - required String password, + required LoginRequestEntity entity, }) async { final result = await accountRepo.login( - requestData: LoginRequestDto( - accountType: accountType, - loginType: loginType, - loginId: loginId, - password: password, - )); + requestData: entity.toDto(), + ); _storeSessionId(id: result.successData?.sessionId ?? ''); - return LoginResultEntityParser.fromDto(result: result); + return LoginResultEntityConverter.fromDto(result: result); } /// 세션 Id 저장 diff --git a/lib/feature/account/presentation/bloc/login/login_bloc.dart b/lib/feature/account/presentation/bloc/login/login_bloc.dart index 9781424..3652071 100644 --- a/lib/feature/account/presentation/bloc/login/login_bloc.dart +++ b/lib/feature/account/presentation/bloc/login/login_bloc.dart @@ -11,6 +11,8 @@ part 'login_bloc.freezed.dart'; part 'login_bloc.handler.dart'; +part 'login_bloc_converter.dart'; + class LoginBloc extends BaseBloc { final AccountUseCase accountUseCase; diff --git a/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart b/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart index 30ae8b2..84024f4 100644 --- a/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart +++ b/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart @@ -33,10 +33,7 @@ extension LoginBlocHandler on LoginBloc { emit(state.copyWith(status: BaseBlocStatus.loading())); final LoginResultEntity result = await accountUseCase.login( - accountType: state.selectedTab, - loginType: LoginType.email, - loginId: state.loginId, - password: state.password, + entity: toEntity(), ); emit(state.copyWith( diff --git a/lib/feature/account/presentation/bloc/login/login_bloc_converter.dart b/lib/feature/account/presentation/bloc/login/login_bloc_converter.dart new file mode 100644 index 0000000..fa7bb16 --- /dev/null +++ b/lib/feature/account/presentation/bloc/login/login_bloc_converter.dart @@ -0,0 +1,12 @@ +part of 'login_bloc.dart'; + +extension LoginBlocConverter on LoginBloc { + LoginRequestEntity toEntity() { + return LoginRequestEntity( + accountType: state.selectedTab, + loginType: LoginType.email, + loginId: state.loginId, + password: state.password, + ); + } +} From 4b62453987e92672b61c36d28cb99f070b0d367b Mon Sep 17 00:00:00 2001 From: Conner Date: Sat, 2 Nov 2024 23:52:25 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20Login=20UserCase=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/domain/usecase/usecase_test.dart | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/test/feature/account/domain/usecase/usecase_test.dart b/test/feature/account/domain/usecase/usecase_test.dart index b02e442..2f3cafd 100644 --- a/test/feature/account/domain/usecase/usecase_test.dart +++ b/test/feature/account/domain/usecase/usecase_test.dart @@ -15,38 +15,42 @@ void main() { useCase = AccountUseCaseImpl(accountRepo: mockRepo); }); - test('로그인 성공 테스트', () async { - final requestDto = LoginRequestDto( - accountType: AccountType.company, - loginType: LoginType.email, - loginId: 'test@test.com', - password: '123qwe!@', - ); - - final successResponseDto = LoginResponseDtoMock.success(); - - // arrange - when( - mockRepo.login(requestData: requestDto), - ).thenAnswer( - (_) async => ApiResponse.success(successResponseDto), - ); - - // act - final result = await useCase.login( - accountType: requestDto.accountType, - loginType: requestDto.loginType, - loginId: requestDto.loginId, - password: requestDto.password, - ); - - // assert - expect( - result, - LoginResultEntity( - isLoggedIn: true, - message: successResponseDto.message, - ), - ); + group('Account UseCase 테스트', () { + test('로그인 성공', () async { + // Given + final successResponseDto = LoginResponseDtoMock.success(); + + when( + mockRepo.login(requestData: LoginRequestDtoMock.mock()), + ).thenAnswer( + (_) async => ApiResponse.success(successResponseDto), + ); + + // When + final result = await useCase.login( + entity: LoginRequestEntityMock.mock(), + ); + + // Then + expect(result.isLoggedIn, true); + }); + + test('서버 에러로 인한 로그인 실패', () async { + // Given + when( + mockRepo.login(requestData: LoginRequestDtoMock.mock()), + ).thenAnswer( + (_) async => ApiResponse.fail(FailResponse.error()), + ); + + // When + final result = await useCase.login( + entity: LoginRequestEntityMock.mock(), + ); + + // Then + expect(result.isLoggedIn, false); + expect(result.message, StringRes.serverError.tr); + }); }); } From 1ae9f9c07c7eb0c63cbdbb2de65dc15b2d0a8ed3 Mon Sep 17 00:00:00 2001 From: Conner Date: Sun, 3 Nov 2024 01:35:50 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20Login=20Bloc=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/utils/bloc/base_bloc_state.dart | 2 + pubspec.lock | 96 ++++++++ pubspec.yaml | 4 + .../account/presentatino/bloc/bloc_test.dart | 218 ++++++++++++++++++ 4 files changed, 320 insertions(+) create mode 100644 test/feature/account/presentatino/bloc/bloc_test.dart diff --git a/lib/core/utils/bloc/base_bloc_state.dart b/lib/core/utils/bloc/base_bloc_state.dart index 2228113..9e3f3d3 100644 --- a/lib/core/utils/bloc/base_bloc_state.dart +++ b/lib/core/utils/bloc/base_bloc_state.dart @@ -14,6 +14,8 @@ abstract class BaseBlocStatus { factory BaseBlocStatus.failure() => BaseBlocStatusFailure(); + bool get isInitial => this is BaseBlocStatusInitial; + bool get isLoading => this is BaseBlocStatusLoading; bool get isSuccess => this is BaseBlocStatusSuccess; diff --git a/pubspec.lock b/pubspec.lock index f7e2cdc..ce39ddf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -62,6 +62,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" + bloc_test: + dependency: "direct dev" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -198,6 +206,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "88b0fddbe4c92910fefc09cc0248f5e7f0cd23e450ded4c28f16ab8ee8f83268" + url: "https://pub.dev" + source: hosted + version: "1.10.0" crypto: dependency: transitive description: @@ -230,6 +246,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" dio: dependency: "direct main" description: @@ -658,6 +682,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.4" + mocktail: + dependency: transitive + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" nested: dependency: transitive description: @@ -666,6 +698,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" package_config: dependency: transitive description: @@ -842,6 +882,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -879,6 +935,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -927,6 +999,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" + url: "https://pub.dev" + source: hosted + version: "1.25.7" test_api: dependency: transitive description: @@ -935,6 +1015,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + test_core: + dependency: transitive + description: + name: test_core + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" + url: "https://pub.dev" + source: hosted + version: "0.6.4" time: dependency: transitive description: @@ -1031,6 +1119,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a45f2be..09488e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,6 +78,10 @@ dev_dependencies: # Svg flutter_svg: + # Bloc test + test: ^1.25.7 + bloc_test: ^9.1.7 + flutter: uses-material-design: true diff --git a/test/feature/account/presentatino/bloc/bloc_test.dart b/test/feature/account/presentatino/bloc/bloc_test.dart new file mode 100644 index 0000000..4342740 --- /dev/null +++ b/test/feature/account/presentatino/bloc/bloc_test.dart @@ -0,0 +1,218 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/account/account.dart'; +import 'package:bloc_test/bloc_test.dart'; + +import 'bloc_test.mocks.dart'; + +@GenerateMocks([AccountUseCase]) +void main() { + group(LoginBloc, () { + late MockAccountUseCase accountUseCase; + late LoginBloc loginBloc; + + setUp(() { + accountUseCase = MockAccountUseCase(); + loginBloc = LoginBloc(accountUseCase: accountUseCase); + }); + + test('Initial state', () { + expect(loginBloc.state.status.isInitial, true); + expect(loginBloc.state.selectedTab, AccountType.company); + expect(loginBloc.state.loginId, ''); + expect(loginBloc.state.password, ''); + expect(loginBloc.state.isValidId, true); + expect(loginBloc.state.isValidPassword, true); + expect(loginBloc.state.isEnabledLogin, false); + }); + + blocTest( + '새로운 일 찾기 탭 클릭 이벤트', + build: () => loginBloc, + act: (bloc) => bloc.add(LoginTabPressed(type: AccountType.user)), + expect: () => [ + isA().having( + (state) => state.selectedTab, + 'selectedTab', + AccountType.user, + ), + ], + ); + + blocTest( + '이메일 입력 이벤트 검사', + build: () => loginBloc, + act: (bloc) => bloc.add(LoginIdInputted(id: 'test@test.com')), + expect: () => [ + isA() + .having( + (state) => state.loginId, + 'loginId', + 'test@test.com', + ) + .having( + (state) => state.isValidId, + 'isValidId', + true, + ) + .having( + (state) => state.isEnabledLogin, + 'isEnabledLogin', + false, // password가 아직 없으므로 false, + ), + ], + ); + + blocTest( + '잘못된 이메일 입력 이벤트 검사', + build: () => loginBloc, + act: (bloc) => bloc.add(LoginIdInputted(id: 'test')), + expect: () => [ + isA() + .having( + (state) => state.loginId, + 'loginId', + 'test', + ) + .having( + (state) => state.isValidId, + 'isValidId', + false, + ) + .having( + (state) => state.isEnabledLogin, + 'isEnabledLogin', + false, // password가 아직 없으므로 false, + ), + ], + ); + + blocTest( + '비밀번호 입력 이벤트 검사', + build: () => loginBloc, + act: (bloc) => bloc.add(LoginPasswordInputted(password: '123qwe!@')), + expect: () => [ + isA() + .having((state) => state.password, 'password', '123qwe!@') + .having((state) => state.isValidPassword, 'isValidPassword', true) + .having((state) => state.isEnabledLogin, 'isEnabledLogin', false), + ], + ); + + blocTest( + '8자리 미만 비밀번호 입력 이벤트 검사', + build: () => loginBloc, + act: (bloc) => bloc.add(LoginPasswordInputted(password: '12qw!@')), + expect: () => [ + isA() + .having((state) => state.password, 'password', '12qw!@') + .having((state) => state.isValidPassword, 'isValidPassword', false) + .having((state) => state.isEnabledLogin, 'isEnabledLogin', false), + ], + ); + + blocTest( + '아이디, 비밀번호 입력 시 isEnabledLogin 검사', + build: () => loginBloc, + act: (bloc) => [ + bloc.add(LoginIdInputted(id: 'test@test.com')), + bloc.add(LoginPasswordInputted(password: '123qwe!@')), + ], + expect: () => [ + /// 이메일 입력 후 상태 + isA() + .having((state) => state.loginId, 'loginId', 'test@test.com') + .having((state) => state.isValidId, 'isValidId', true) + .having((state) => state.isEnabledLogin, 'isEnabledLogin', false), + + /// 비밀번호도 입력 후 상태 + isA() + .having((state) => state.password, 'password', '123qwe!@') + .having((state) => state.isValidPassword, 'isValidPassword', true) + .having((state) => state.isEnabledLogin, 'isEnabledLogin', true), + ], + ); + + blocTest( + '로그인 버튼 클릭 시 성공 케이스', + build: () => loginBloc, + // Given + setUp: () { + when( + accountUseCase.login(entity: anyNamed('entity')), + ).thenAnswer( + (_) async => LoginResultEntity( + isLoggedIn: true, + message: '로그인 성공', + ), + ); + }, + // When + act: (bloc) => bloc.add(LoginBtnPressed()), + //Then + expect: () => [ + /// Loading 상태 + isA().having( + (state) => state.status, + 'status', + isA(), + ), + + /// Success 상태 + isA() + .having( + (state) => state.status, + 'status', + isA(), + ) + .having( + (state) => state.message, + 'message', + '로그인 성공', + ), + ], + verify: (_) { + verify(accountUseCase.login(entity: anyNamed('entity'))).called(1); + }, + ); + + blocTest( + '로그인 버튼 클릭 시 실패 케이스', + build: () => loginBloc, + // Given + setUp: () { + when( + accountUseCase.login(entity: anyNamed('entity')), + ).thenAnswer( + (_) async => LoginResultEntity( + isLoggedIn: false, + message: '중복된 아이디 입니다.', + ), + ); + }, + // When + act: (bloc) => bloc.add(LoginBtnPressed()), + //Then + expect: () => [ + /// Loading 상태 + isA().having( + (state) => state.status, + 'status', + isA(), + ), + + /// Failure 상태 + isA().having( + (state) => state.status, + 'status', + isA(), + ) + ], + verify: (_) { + verify(accountUseCase.login(entity: anyNamed('entity'))).called(1); + }, + ); + }); +} From 02dd01f858d36360965b5e4ce0ad9414e61e9571 Mon Sep 17 00:00:00 2001 From: Conner Date: Sun, 3 Nov 2024 02:07:22 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20Login=20Repository=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=8B=A4=ED=8C=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/data_source/repository/repository_impl_test.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/feature/account/data/data_source/repository/repository_impl_test.dart b/test/feature/account/data/data_source/repository/repository_impl_test.dart index 891a80b..e80a3c1 100644 --- a/test/feature/account/data/data_source/repository/repository_impl_test.dart +++ b/test/feature/account/data/data_source/repository/repository_impl_test.dart @@ -1,4 +1,3 @@ - import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -45,10 +44,8 @@ void main() { when( api.login(requestData: requestData), ).thenAnswer( - (_) async => ApiResponse.fail( - FailResponse.fromJson( - LoginResponseDtoMock.failure().toJson(), - ), + (_) async => ApiResponse.success( + LoginResponseDtoMock.failure(), ), ); From cc5241dddf188a0363619dcb65880d6593123e04 Mon Sep 17 00:00:00 2001 From: Conner Date: Sun, 3 Nov 2024 14:37:26 +0900 Subject: [PATCH 08/17] =?UTF-8?q?test:=20=EC=9C=84=EC=A0=AF=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Bloc이 위젯에 변화에 따라 실제 로직 테스트가 안되고 있는 상황이라 해결 방법 고민 중 --- .../presentation/page/login/login_page.dart | 12 +- lib/shared/widgets/base_input/base_input.dart | 4 + lib/shared/widgets/tab/widget/base_tabs.dart | 1 + .../bloc/bloc_test.dart | 0 .../presentation/page/login_page_test.dart | 136 ++++++++++++++++++ .../page/login_page_test_helper.dart | 45 ++++++ 6 files changed, 195 insertions(+), 3 deletions(-) rename test/feature/account/{presentatino => presentation}/bloc/bloc_test.dart (100%) create mode 100644 test/feature/account/presentation/page/login_page_test.dart create mode 100644 test/feature/account/presentation/page/login_page_test_helper.dart diff --git a/lib/feature/account/presentation/page/login/login_page.dart b/lib/feature/account/presentation/page/login/login_page.dart index 8b0af8f..b8fac0d 100644 --- a/lib/feature/account/presentation/page/login/login_page.dart +++ b/lib/feature/account/presentation/page/login/login_page.dart @@ -16,17 +16,19 @@ class LoginPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => getIt(), - child: _LoginPage(), + child: const LoginPageContent(), ); } } -class _LoginPage extends StatefulWidget { +class LoginPageContent extends StatefulWidget { + const LoginPageContent({super.key}); + @override State createState() => _LoginPageState(); } -class _LoginPageState extends State<_LoginPage> { +class _LoginPageState extends State { @override void initState() { super.initState(); @@ -69,6 +71,7 @@ class _LoginPageState extends State<_LoginPage> { ), const SizedBox(height: 26), BaseInput.email( + key: const Key('email_input'), textInputAction: TextInputAction.next, errorText: StringRes.pleaseEnterValidEmail.tr, errorVisible: !state.isValidId, @@ -78,6 +81,7 @@ class _LoginPageState extends State<_LoginPage> { ), const SizedBox(height: 30), BaseInput.password( + key: const Key('password_input'), errorText: StringRes.pleaseEnterValidPassword.tr, errorVisible: !state.isValidPassword, onChanged: (String text) { @@ -108,6 +112,7 @@ class _LoginPageState extends State<_LoginPage> { ), const SizedBox(height: 10), _LoginButton( + key: const Key('login_button'), enabled: state.isEnabledLogin, ), const SizedBox(height: 20), @@ -124,6 +129,7 @@ class _LoginButton extends StatelessWidget { final bool enabled; const _LoginButton({ + super.key, required this.enabled, }); diff --git a/lib/shared/widgets/base_input/base_input.dart b/lib/shared/widgets/base_input/base_input.dart index b99bc7e..ec4d86c 100644 --- a/lib/shared/widgets/base_input/base_input.dart +++ b/lib/shared/widgets/base_input/base_input.dart @@ -119,6 +119,7 @@ class BaseInput extends StatelessWidget { /// 이메일 형식 입력 factory BaseInput.email({ + Key? key, TextEditingController? controller, FocusNode? focusNode, TextInputAction? textInputAction, @@ -127,6 +128,7 @@ class BaseInput extends StatelessWidget { bool errorVisible = false, }) { return BaseInput( + key: key, controller: controller, focusNode: focusNode, keyboardType: TextInputType.emailAddress, @@ -140,6 +142,7 @@ class BaseInput extends StatelessWidget { /// 비밀번호 형식 입력 factory BaseInput.password({ + Key? key, TextEditingController? controller, FocusNode? focusNode, TextInputAction? textInputAction, @@ -148,6 +151,7 @@ class BaseInput extends StatelessWidget { bool errorVisible = false, }) { return BaseInput( + key: key, controller: controller, focusNode: focusNode, textInputAction: textInputAction, diff --git a/lib/shared/widgets/tab/widget/base_tabs.dart b/lib/shared/widgets/tab/widget/base_tabs.dart index 318602a..4dd653a 100644 --- a/lib/shared/widgets/tab/widget/base_tabs.dart +++ b/lib/shared/widgets/tab/widget/base_tabs.dart @@ -32,6 +32,7 @@ class BaseTabs extends StatelessWidget { .map( (tab) => Expanded( child: BaseTab( + key: Key('base_tab_${tab.value.toString()}'), data: tab, isSelected: selectedTab == tab, onTap: () => onTap(tab), diff --git a/test/feature/account/presentatino/bloc/bloc_test.dart b/test/feature/account/presentation/bloc/bloc_test.dart similarity index 100% rename from test/feature/account/presentatino/bloc/bloc_test.dart rename to test/feature/account/presentation/bloc/bloc_test.dart diff --git a/test/feature/account/presentation/page/login_page_test.dart b/test/feature/account/presentation/page/login_page_test.dart new file mode 100644 index 0000000..020d688 --- /dev/null +++ b/test/feature/account/presentation/page/login_page_test.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/feature/account/account.dart'; +import 'package:withu_app/shared/shared.dart'; + +import 'login_page_test.mocks.dart'; +import 'login_page_test_helper.dart'; + +@GenerateMocks([LoginBloc]) +void main() { + group('LoginPage Test', () { + late MockLoginBloc loginBloc; + late Widget testWidget; + + setUp(() { + loginBloc = MockLoginBloc(); + + final initialState = LoginState( + status: BaseBlocStatus.initial(), + selectedTab: AccountType.company, + ); + + when(loginBloc.state).thenReturn(initialState); + when(loginBloc.stream).thenAnswer( + (_) => Stream.fromIterable([ + initialState, + initialState.copyWith(selectedTab: AccountType.user), + ]), + ); + + testWidget = MaterialApp( + home: BlocProvider.value( + value: loginBloc, + child: const LoginPageContent(), // 로그인 페이지 위젯 + ), + ); + }); + + testWidgets('화면 로딩 후 초기화 상태 검사', (WidgetTester tester) async { + //Given + await tester.pumpWidget(testWidget); + final companyTab = LoginPageTestHelper.getCompanyTab(tester); + final userTab = LoginPageTestHelper.getUserTab(tester); + + // When + + // Then + expect(loginBloc.state.status.isInitial, true); + expect(loginBloc.state.selectedTab, AccountType.company); + expect(loginBloc.state.loginId, ''); + expect(loginBloc.state.password, ''); + expect(loginBloc.state.isValidId, true); + expect(loginBloc.state.isValidPassword, true); + expect(loginBloc.state.isEnabledLogin, false); + + // UI 요소 검증; + + expect(companyTab, isA()); + expect(companyTab.isSelected, true); + expect(userTab, isA()); + expect(userTab.isSelected, false); + expect(find.byType(BaseInput), findsNWidgets(2)); // ID, PW 입력 필드 + expect(find.byType(BaseButton), findsOneWidget); // 로그인 버튼 + }); + + testWidgets('새로운 일 찾기 탭 클릭 테스트', (WidgetTester tester) async { + //Given + await tester.pumpWidget(testWidget); + final companyTab = LoginPageTestHelper.getCompanyTab(tester); + final userTab = LoginPageTestHelper.getUserTab(tester); + + when( + loginBloc.add(argThat(isA())), + ).thenAnswer((_) { + when(loginBloc.state).thenReturn( + LoginState( + status: BaseBlocStatus.initial(), + selectedTab: AccountType.user, + ), + ); + }); + + // 초기 상태 검증 + expect(companyTab.isSelected, true); + expect(userTab.isSelected, false); + + // When + await tester.tap(LoginPageTestHelper.userTabFinder()); + await tester.pumpAndSettle(); + + // Then + verify(loginBloc.add(argThat(isA()))).called(1); + expect(loginBloc.state.selectedTab, equals(AccountType.user)); + + final updatedCompanyTab = LoginPageTestHelper.getCompanyTab(tester); + final updatedUserTab = LoginPageTestHelper.getUserTab(tester); + expect(updatedCompanyTab.isSelected, false); + expect(updatedUserTab.isSelected, true); + }); + + testWidgets('이메일 입력 테스트', (WidgetTester tester) async { + //Given + const loginId = 'test@test.com'; + when( + loginBloc.add(argThat(isA())), + ).thenAnswer((_) { + when(loginBloc.state).thenReturn( + loginBloc.state.copyWith( + loginId: loginId, + isValidId: true, + ), + ); + }); + + await tester.pumpWidget(testWidget); + + // 초기 상태 검증 + expect(loginBloc.state.loginId, ''); + + // When + await tester.enterText( + LoginPageTestHelper.idInputFinder(), + loginId, + ); + await tester.pumpAndSettle(); + + // Then + verify(loginBloc.add(argThat(isA()))).called(1); + expect(find.text(loginId), findsOneWidget); + }); + }); +} diff --git a/test/feature/account/presentation/page/login_page_test_helper.dart b/test/feature/account/presentation/page/login_page_test_helper.dart new file mode 100644 index 0000000..349474c --- /dev/null +++ b/test/feature/account/presentation/page/login_page_test_helper.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:withu_app/core/core.dart'; +import 'package:withu_app/shared/shared.dart'; + +class LoginPageTestHelper { + static Finder companyTabFinder() { + return find.byKey(Key('base_tab_${AccountType.company.toString()}')); + } + + static Finder userTabFinder() { + return find.byKey(Key('base_tab_${AccountType.user.toString()}')); + } + + static Finder idInputFinder() { + return find.byKey(const Key('email_input')); + } + + static Finder passwordInputFinder() { + return find.byKey(const Key('password_input')); + } + + static Finder loginButtonFinder() { + return find.byKey(const Key('login_button')); + } + + static BaseTab getCompanyTab(WidgetTester tester) { + return tester.widget(companyTabFinder()); + } + + static BaseTab getUserTab(WidgetTester tester) { + return tester.widget(userTabFinder()); + } + + static BaseInput getIdInput(WidgetTester tester) { + return tester.widget(idInputFinder()); + } + + static BaseInput getPasswordInput(WidgetTester tester) { + return tester.widget(passwordInputFinder()); + } + static BaseButton getLoginButton(WidgetTester tester) { + return tester.widget(loginButtonFinder()); + } +} From e3b6ead668e4f18110b60d1b1ab5c6028604a2ea Mon Sep 17 00:00:00 2001 From: Conner Date: Sun, 3 Nov 2024 15:00:40 +0900 Subject: [PATCH 09/17] =?UTF-8?q?test:=20=EC=9C=84=EC=A0=AF=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=97=90=EC=84=9C=20Bloc=EC=97=90=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/page/login_page_test.dart | 115 +++++++----------- 1 file changed, 42 insertions(+), 73 deletions(-) diff --git a/test/feature/account/presentation/page/login_page_test.dart b/test/feature/account/presentation/page/login_page_test.dart index 020d688..74455f6 100644 --- a/test/feature/account/presentation/page/login_page_test.dart +++ b/test/feature/account/presentation/page/login_page_test.dart @@ -1,40 +1,32 @@ +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; import 'package:withu_app/core/core.dart'; import 'package:withu_app/feature/account/account.dart'; import 'package:withu_app/shared/shared.dart'; -import 'login_page_test.mocks.dart'; import 'login_page_test_helper.dart'; -@GenerateMocks([LoginBloc]) +class MockLoginBloc extends MockBloc + implements LoginBloc {} + void main() { group('LoginPage Test', () { late MockLoginBloc loginBloc; late Widget testWidget; + late LoginState initialState; setUp(() { loginBloc = MockLoginBloc(); - final initialState = LoginState( + initialState = LoginState( status: BaseBlocStatus.initial(), - selectedTab: AccountType.company, - ); - - when(loginBloc.state).thenReturn(initialState); - when(loginBloc.stream).thenAnswer( - (_) => Stream.fromIterable([ - initialState, - initialState.copyWith(selectedTab: AccountType.user), - ]), ); testWidget = MaterialApp( - home: BlocProvider.value( - value: loginBloc, + home: BlocProvider( + create: (context) => loginBloc, child: const LoginPageContent(), // 로그인 페이지 위젯 ), ); @@ -42,23 +34,18 @@ void main() { testWidgets('화면 로딩 후 초기화 상태 검사', (WidgetTester tester) async { //Given + whenListen( + loginBloc, + Stream.fromIterable([initialState]), + initialState: initialState, + ); + + // When await tester.pumpWidget(testWidget); final companyTab = LoginPageTestHelper.getCompanyTab(tester); final userTab = LoginPageTestHelper.getUserTab(tester); - // When - - // Then - expect(loginBloc.state.status.isInitial, true); - expect(loginBloc.state.selectedTab, AccountType.company); - expect(loginBloc.state.loginId, ''); - expect(loginBloc.state.password, ''); - expect(loginBloc.state.isValidId, true); - expect(loginBloc.state.isValidPassword, true); - expect(loginBloc.state.isEnabledLogin, false); - // UI 요소 검증; - expect(companyTab, isA()); expect(companyTab.isSelected, true); expect(userTab, isA()); @@ -69,68 +56,50 @@ void main() { testWidgets('새로운 일 찾기 탭 클릭 테스트', (WidgetTester tester) async { //Given - await tester.pumpWidget(testWidget); - final companyTab = LoginPageTestHelper.getCompanyTab(tester); - final userTab = LoginPageTestHelper.getUserTab(tester); - - when( - loginBloc.add(argThat(isA())), - ).thenAnswer((_) { - when(loginBloc.state).thenReturn( - LoginState( - status: BaseBlocStatus.initial(), + whenListen( + loginBloc, + Stream.fromIterable([ + initialState, + initialState.copyWith( selectedTab: AccountType.user, - ), - ); - }); - - // 초기 상태 검증 - expect(companyTab.isSelected, true); - expect(userTab.isSelected, false); + ) + ]), + initialState: initialState, + ); // When + await tester.pumpWidget(testWidget); await tester.tap(LoginPageTestHelper.userTabFinder()); await tester.pumpAndSettle(); // Then - verify(loginBloc.add(argThat(isA()))).called(1); expect(loginBloc.state.selectedTab, equals(AccountType.user)); - - final updatedCompanyTab = LoginPageTestHelper.getCompanyTab(tester); - final updatedUserTab = LoginPageTestHelper.getUserTab(tester); - expect(updatedCompanyTab.isSelected, false); - expect(updatedUserTab.isSelected, true); + expect(LoginPageTestHelper.getCompanyTab(tester).isSelected, false); + expect(LoginPageTestHelper.getUserTab(tester).isSelected, true); }); testWidgets('이메일 입력 테스트', (WidgetTester tester) async { //Given - const loginId = 'test@test.com'; - when( - loginBloc.add(argThat(isA())), - ).thenAnswer((_) { - when(loginBloc.state).thenReturn( - loginBloc.state.copyWith( - loginId: loginId, - isValidId: true, - ), - ); - }); - - await tester.pumpWidget(testWidget); - - // 초기 상태 검증 - expect(loginBloc.state.loginId, ''); + whenListen( + loginBloc, + Stream.fromIterable([ + initialState, + initialState.copyWith( + selectedTab: AccountType.user, + ) + ]), + initialState: initialState, + ); // When - await tester.enterText( - LoginPageTestHelper.idInputFinder(), - loginId, - ); + await tester.pumpWidget(testWidget); + await tester.tap(LoginPageTestHelper.userTabFinder()); await tester.pumpAndSettle(); // Then - verify(loginBloc.add(argThat(isA()))).called(1); - expect(find.text(loginId), findsOneWidget); + expect(loginBloc.state.selectedTab, equals(AccountType.user)); + expect(LoginPageTestHelper.getCompanyTab(tester).isSelected, false); + expect(LoginPageTestHelper.getUserTab(tester).isSelected, true); }); }); } From 82946a527bf6ff40ee75ad293be95995fdb19429 Mon Sep 17 00:00:00 2001 From: Conner Date: Sun, 3 Nov 2024 15:02:46 +0900 Subject: [PATCH 10/17] =?UTF-8?q?chore:=20pre-commit=20hooks=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f1e81f..40876d0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,6 +15,13 @@ repos: type: [ dart ] files: lib/.*\.dart$ + + - id: flutter-test + name: flutter-test + entry: bash -c 'flutter test' + language: system + type: [ dart ] + # - id: dart-format # name: dart-format # entry: bash -c 'dart format "$@"' From 2da96ef61300c142057866f51aa37264d524cf44 Mon Sep 17 00:00:00 2001 From: Conner Date: Sun, 3 Nov 2024 15:03:46 +0900 Subject: [PATCH 11/17] =?UTF-8?q?chore:=20pre-commit=20hooks=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 40876d0..1ebe844 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,6 @@ repos: type: [ dart ] files: lib/.*\.dart$ - - id: flutter-test name: flutter-test entry: bash -c 'flutter test' From c6f54b64b2f044300a31a9142dd3bd0891a1e226 Mon Sep 17 00:00:00 2001 From: Conner Date: Sat, 9 Nov 2024 22:10:19 +0900 Subject: [PATCH 12/17] =?UTF-8?q?test:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Runner.xcodeproj/project.pbxproj | 130 ++++++------ .../presentation/bloc/login/login_bloc.dart | 1 + .../bloc/login/login_bloc.handler.dart | 10 + .../presentation/bloc/login/login_event.dart | 5 + .../presentation/bloc/login/login_state.dart | 4 +- .../presentation/page/login/login_page.dart | 6 + .../pages/job_posting_form_page.dart | 4 +- lib/shared/widgets/base_input/base_input.dart | 95 +++++---- pubspec.lock | 2 +- pubspec.yaml | 1 + .../account/presentation/bloc/bloc_test.dart | 2 +- .../presentation/page/login_page_test.dart | 194 +++++++++++++++++- .../page/login_page_test_helper.dart | 25 +++ 13 files changed, 359 insertions(+), 120 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1cf6d4d..0e68b3d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,14 +8,14 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 264CDBD38B8820AC135E6A23 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 18B43C5038E2A2612C67E2D2 /* Pods_Runner.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - DBC749BBCEE1813469E9EBE9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54FC5508885571A5F027815E /* Pods_Runner.framework */; }; - FC19169D40D3B30B1C8285AB /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E12ED764B4D86D43F555C363 /* Pods_RunnerTests.framework */; }; + F203E409ED254037D543D620 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF24C5B2EA1D5F65A1DA7AF3 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -42,18 +42,19 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0D82AA4422652D8004F2A305 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 18B43C5038E2A2612C67E2D2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 25F90361B8315567E32B6557 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 54FC5508885571A5F027815E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 5B89B99F570AD57467A5B205 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 64CD9938BCF89D57A5460C0C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 478C657C36A437DBDB38CF92 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 84885F91847426508F9CB6B4 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 8F35DFBFD2BD4E567EE24BCB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -61,10 +62,9 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A8B58E44B1EB758AA8A2DCCD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - CD45469538CEE843A45C33C2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - E12ED764B4D86D43F555C363 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F93BE9EC760304A94A11FACA /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + B24BC7EFF6D54A567817CBDF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + C9A2399CE8D6CA8FC8BE0996 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + FF24C5B2EA1D5F65A1DA7AF3 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,7 +72,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - FC19169D40D3B30B1C8285AB /* Pods_RunnerTests.framework in Frameworks */, + F203E409ED254037D543D620 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -80,7 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DBC749BBCEE1813469E9EBE9 /* Pods_Runner.framework in Frameworks */, + 264CDBD38B8820AC135E6A23 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -95,13 +95,18 @@ path = RunnerTests; sourceTree = ""; }; - 5940FA6FAF3401039AC3618C /* Frameworks */ = { + 33DD12C07618807508A402E4 /* Pods */ = { isa = PBXGroup; children = ( - 54FC5508885571A5F027815E /* Pods_Runner.framework */, - E12ED764B4D86D43F555C363 /* Pods_RunnerTests.framework */, + 478C657C36A437DBDB38CF92 /* Pods-Runner.debug.xcconfig */, + 8F35DFBFD2BD4E567EE24BCB /* Pods-Runner.release.xcconfig */, + C9A2399CE8D6CA8FC8BE0996 /* Pods-Runner.profile.xcconfig */, + 84885F91847426508F9CB6B4 /* Pods-RunnerTests.debug.xcconfig */, + 25F90361B8315567E32B6557 /* Pods-RunnerTests.release.xcconfig */, + B24BC7EFF6D54A567817CBDF /* Pods-RunnerTests.profile.xcconfig */, ); - name = Frameworks; + name = Pods; + path = Pods; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { @@ -122,8 +127,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, - B592DA797F244265F115B786 /* Pods */, - 5940FA6FAF3401039AC3618C /* Frameworks */, + 33DD12C07618807508A402E4 /* Pods */, + B73406FE005F9103253A70B4 /* Frameworks */, ); sourceTree = ""; }; @@ -151,18 +156,13 @@ path = Runner; sourceTree = ""; }; - B592DA797F244265F115B786 /* Pods */ = { + B73406FE005F9103253A70B4 /* Frameworks */ = { isa = PBXGroup; children = ( - CD45469538CEE843A45C33C2 /* Pods-Runner.debug.xcconfig */, - A8B58E44B1EB758AA8A2DCCD /* Pods-Runner.release.xcconfig */, - 0D82AA4422652D8004F2A305 /* Pods-Runner.profile.xcconfig */, - 64CD9938BCF89D57A5460C0C /* Pods-RunnerTests.debug.xcconfig */, - F93BE9EC760304A94A11FACA /* Pods-RunnerTests.release.xcconfig */, - 5B89B99F570AD57467A5B205 /* Pods-RunnerTests.profile.xcconfig */, + 18B43C5038E2A2612C67E2D2 /* Pods_Runner.framework */, + FF24C5B2EA1D5F65A1DA7AF3 /* Pods_RunnerTests.framework */, ); - name = Pods; - path = Pods; + name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ @@ -172,7 +172,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - BAC011BA047135336964E80D /* [CP] Check Pods Manifest.lock */, + 4916E87F44791A6691D4A007 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 93639A99A847B6F3892EE7F7 /* Frameworks */, @@ -191,14 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - DF2CA63EBF8244753EE19552 /* [CP] Check Pods Manifest.lock */, + 06A3A9522E174D3F1E78CB45 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - E235F37719081D02864E5419 /* [CP] Embed Pods Frameworks */, + 15283B1AA10175D9E2F3512A /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -270,60 +270,62 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 06A3A9522E174D3F1E78CB45 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + 15283B1AA10175D9E2F3512A /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputPaths = ( + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "Run Script"; - outputPaths = ( + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - BAC011BA047135336964E80D /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); + name = "Thin Binary"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - DF2CA63EBF8244753EE19552 /* [CP] Check Pods Manifest.lock */ = { + 4916E87F44791A6691D4A007 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -338,29 +340,27 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - E235F37719081D02864E5419 /* [CP] Embed Pods Frameworks */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + inputPaths = ( ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + name = "Run Script"; + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ @@ -488,7 +488,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 64CD9938BCF89D57A5460C0C /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 84885F91847426508F9CB6B4 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -506,7 +506,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F93BE9EC760304A94A11FACA /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 25F90361B8315567E32B6557 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -522,7 +522,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5B89B99F570AD57467A5B205 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = B24BC7EFF6D54A567817CBDF /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/lib/feature/account/presentation/bloc/login/login_bloc.dart b/lib/feature/account/presentation/bloc/login/login_bloc.dart index 3652071..c4ddd2b 100644 --- a/lib/feature/account/presentation/bloc/login/login_bloc.dart +++ b/lib/feature/account/presentation/bloc/login/login_bloc.dart @@ -25,5 +25,6 @@ class LoginBloc extends BaseBloc { on(_onPasswordInputted); on(_onBtnPressed); on(_onTabPressed); + on(_onVisiblePasswordToggled); } } diff --git a/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart b/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart index 84024f4..bdf7db0 100644 --- a/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart +++ b/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart @@ -51,4 +51,14 @@ extension LoginBlocHandler on LoginBloc { selectedTab: event.type, )); } + + /// 비밀번호 표시 토글 + void _onVisiblePasswordToggled( + LoginVisiblePasswordToggled event, + Emitter emit, + ) { + emit(state.copyWith( + isVisiblePassword: !state.isVisiblePassword, + )); + } } diff --git a/lib/feature/account/presentation/bloc/login/login_event.dart b/lib/feature/account/presentation/bloc/login/login_event.dart index b165fce..092614e 100644 --- a/lib/feature/account/presentation/bloc/login/login_event.dart +++ b/lib/feature/account/presentation/bloc/login/login_event.dart @@ -27,3 +27,8 @@ class LoginTabPressed extends LoginEvent { LoginTabPressed({required this.type}); } + +/// 비밀번호 표시 토클 이벤트 +class LoginVisiblePasswordToggled extends LoginEvent { + LoginVisiblePasswordToggled(); +} diff --git a/lib/feature/account/presentation/bloc/login/login_state.dart b/lib/feature/account/presentation/bloc/login/login_state.dart index 5763754..f24d9fc 100644 --- a/lib/feature/account/presentation/bloc/login/login_state.dart +++ b/lib/feature/account/presentation/bloc/login/login_state.dart @@ -21,6 +21,9 @@ class LoginState extends BaseBlocState with _$LoginState { /// password Valid 여부 @Default(true) bool isValidPassword, + /// password Visible 여부 + @Default(false) bool isVisiblePassword, + /// 로그인 버튼 enabled @Default(false) bool isEnabledLogin, @@ -30,7 +33,6 @@ class LoginState extends BaseBlocState with _$LoginState { } extension LoginStateExt on LoginState { - /// Id 유효성 검사 bool checkIdValid(String id) { return RegExUtil.emailPattern.hasMatch(id); diff --git a/lib/feature/account/presentation/page/login/login_page.dart b/lib/feature/account/presentation/page/login/login_page.dart index b8fac0d..de669e1 100644 --- a/lib/feature/account/presentation/page/login/login_page.dart +++ b/lib/feature/account/presentation/page/login/login_page.dart @@ -84,11 +84,17 @@ class _LoginPageState extends State { key: const Key('password_input'), errorText: StringRes.pleaseEnterValidPassword.tr, errorVisible: !state.isValidPassword, + obscureText: !state.isVisiblePassword, onChanged: (String text) { context .read() .add(LoginPasswordInputted(password: text)); }, + onSuffixPressed: () { + context + .read() + .add(LoginVisiblePasswordToggled()); + }, ), const SizedBox(height: 80), Row( diff --git a/lib/feature/job_posting/presentation/pages/job_posting_form_page.dart b/lib/feature/job_posting/presentation/pages/job_posting_form_page.dart index 3501083..3e8614f 100644 --- a/lib/feature/job_posting/presentation/pages/job_posting_form_page.dart +++ b/lib/feature/job_posting/presentation/pages/job_posting_form_page.dart @@ -548,7 +548,7 @@ class _ParticipantsState extends State<_Participants> { style: context.textTheme.bodyLarge, hintText: '0', hintTextStyle: context.textTheme.bodyLarge, - suffix: StringRes.numberOfPeopleUnit.tr, + suffixText: StringRes.numberOfPeopleUnit.tr, suffixStyle: context.textTheme.bodyLarge, keyboardType: TextInputType.number, maxLength: 3, @@ -620,7 +620,7 @@ class _PayTypeState extends State<_PayType> { style: context.textTheme.bodyLarge, hintText: '0', hintTextStyle: context.textTheme.bodyLarge, - suffix: StringRes.wonUnit.tr, + suffixText: StringRes.wonUnit.tr, suffixStyle: context.textTheme.bodyLarge, keyboardType: TextInputType.number, textAlign: TextAlign.end, diff --git a/lib/shared/widgets/base_input/base_input.dart b/lib/shared/widgets/base_input/base_input.dart index ec4d86c..1a7230b 100644 --- a/lib/shared/widgets/base_input/base_input.dart +++ b/lib/shared/widgets/base_input/base_input.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:withu_app/core/utils/utils.dart'; +import 'package:withu_app/gen/assets.gen.dart'; import 'package:withu_app/gen/colors.gen.dart'; class BaseInput extends StatelessWidget { @@ -18,7 +19,9 @@ class BaseInput extends StatelessWidget { final EdgeInsets? padding; - final String? suffix; + final Widget? suffix; + + final String? suffixText; final TextStyle? suffixStyle; @@ -51,6 +54,7 @@ class BaseInput extends StatelessWidget { this.padding, this.onChanged, this.suffix, + this.suffixText, this.suffixStyle, this.keyboardType, this.textInputAction, @@ -83,36 +87,44 @@ class BaseInput extends StatelessWidget { bottom: BorderSide(color: ColorName.teritary), ), ), - child: TextField( - controller: controller, - focusNode: focusNode, - style: style ?? defaultTextStyle, - keyboardType: keyboardType, - textInputAction: textInputAction ?? TextInputAction.done, - maxLength: maxLength, - cursorHeight: 16, - cursorColor: ColorName.primary80, - textAlign: textAlign, - inputFormatters: inputFormatters, - obscureText: obscureText, - decoration: InputDecoration( - hintText: hintText, - hintStyle: hintTextStyle ?? defaultHintStyle, - border: InputBorder.none, - isDense: true, - contentPadding: const EdgeInsets.all(0), - floatingLabelBehavior: FloatingLabelBehavior.always, - suffixText: suffix, - suffixStyle: suffixStyle, - counterText: '', - ), - onChanged: onChanged, + child: Row( + children: [ + Expanded( + child: TextField( + controller: controller, + focusNode: focusNode, + style: style ?? defaultTextStyle, + keyboardType: keyboardType, + textInputAction: textInputAction ?? TextInputAction.done, + maxLength: maxLength, + cursorHeight: 16, + cursorColor: ColorName.primary80, + textAlign: textAlign, + inputFormatters: inputFormatters, + obscureText: obscureText, + decoration: InputDecoration( + hintText: hintText, + hintStyle: hintTextStyle ?? defaultHintStyle, + border: InputBorder.none, + isDense: true, + contentPadding: const EdgeInsets.all(0), + floatingLabelBehavior: FloatingLabelBehavior.always, + suffixText: suffixText, + suffixStyle: suffixStyle, + counterText: '', + ), + onChanged: onChanged, + ), + ), + suffix ?? const SizedBox(), + ], ), ), - _ErrorText( - visible: errorVisible, - text: errorText, - ), + if (errorVisible == true) + _ErrorText( + key: Key('${(super.key as ValueKey).value}_error'), + text: errorText, + ), ], ); } @@ -149,41 +161,44 @@ class BaseInput extends StatelessWidget { Function(String)? onChanged, String errorText = '', bool errorVisible = false, + bool obscureText = true, + VoidCallback? onSuffixPressed, }) { return BaseInput( key: key, controller: controller, focusNode: focusNode, textInputAction: textInputAction, - obscureText: true, + obscureText: obscureText, hintText: StringRes.pleaseEnterPassword.tr, onChanged: onChanged, errorText: errorText, errorVisible: errorVisible, + suffix: InkWell( + key: const Key('password_visible_btn'), + splashColor: Colors.transparent, + onTap: onSuffixPressed, + child: Assets.images.eye.svg(), + ), ); } } /// 에러 문구 class _ErrorText extends StatelessWidget { - final bool visible; - final String text; const _ErrorText({ - required this.visible, + super.key, required this.text, }); @override Widget build(BuildContext context) { - return Visibility( - visible: visible, - child: Text( - text, - style: context.textTheme.bodySmall?.copyWith( - color: ColorName.annotations, - ), + return Text( + text, + style: context.textTheme.bodySmall?.copyWith( + color: ColorName.annotations, ), ); } diff --git a/pubspec.lock b/pubspec.lock index ce39ddf..793b93f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -683,7 +683,7 @@ packages: source: hosted version: "5.4.4" mocktail: - dependency: transitive + dependency: "direct main" description: name: mocktail sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" diff --git a/pubspec.yaml b/pubspec.yaml index 09488e8..c1195f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: calendar_date_picker2: ^1.1.7 flutter_keyboard_visibility: ^6.0.0 shared_preferences: ^2.3.2 + mocktail: ^1.0.4 dev_dependencies: diff --git a/test/feature/account/presentation/bloc/bloc_test.dart b/test/feature/account/presentation/bloc/bloc_test.dart index 4342740..d04d765 100644 --- a/test/feature/account/presentation/bloc/bloc_test.dart +++ b/test/feature/account/presentation/bloc/bloc_test.dart @@ -29,7 +29,7 @@ void main() { }); blocTest( - '새로운 일 찾기 탭 클릭 이벤트', + '사용자 유형 선택 - 사용자 찾기 옵션 선택', build: () => loginBloc, act: (bloc) => bloc.add(LoginTabPressed(type: AccountType.user)), expect: () => [ diff --git a/test/feature/account/presentation/page/login_page_test.dart b/test/feature/account/presentation/page/login_page_test.dart index 74455f6..93e9f8c 100644 --- a/test/feature/account/presentation/page/login_page_test.dart +++ b/test/feature/account/presentation/page/login_page_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -16,12 +17,19 @@ void main() { late MockLoginBloc loginBloc; late Widget testWidget; late LoginState initialState; + late StreamController controller; setUp(() { loginBloc = MockLoginBloc(); + controller = StreamController(); + initialState = LoginState(status: BaseBlocStatus.initial()); - initialState = LoginState( - status: BaseBlocStatus.initial(), + whenListen( + loginBloc, + controller.stream, + initialState: initialState.copyWith( + isVisiblePassword: true, + ), ); testWidget = MaterialApp( @@ -32,6 +40,10 @@ void main() { ); }); + tearDown(() { + controller.close(); + }); + testWidgets('화면 로딩 후 초기화 상태 검사', (WidgetTester tester) async { //Given whenListen( @@ -54,7 +66,7 @@ void main() { expect(find.byType(BaseButton), findsOneWidget); // 로그인 버튼 }); - testWidgets('새로운 일 찾기 탭 클릭 테스트', (WidgetTester tester) async { + testWidgets('[새로운 일 찾기] 탭 클릭 테스트', (WidgetTester tester) async { //Given whenListen( loginBloc, @@ -78,28 +90,190 @@ void main() { expect(LoginPageTestHelper.getUserTab(tester).isSelected, true); }); - testWidgets('이메일 입력 테스트', (WidgetTester tester) async { + testWidgets('이메일 유효성 검사 - 성공 케이스', (WidgetTester tester) async { //Given + const email = 'test@test.com'; whenListen( loginBloc, Stream.fromIterable([ initialState, initialState.copyWith( - selectedTab: AccountType.user, - ) + loginId: email, + isValidId: true, + ), ]), initialState: initialState, ); // When await tester.pumpWidget(testWidget); - await tester.tap(LoginPageTestHelper.userTabFinder()); + await tester.enterText(LoginPageTestHelper.idInputFinder(), email); await tester.pumpAndSettle(); // Then - expect(loginBloc.state.selectedTab, equals(AccountType.user)); - expect(LoginPageTestHelper.getCompanyTab(tester).isSelected, false); - expect(LoginPageTestHelper.getUserTab(tester).isSelected, true); + expect(loginBloc.state.loginId, equals(email)); + expect(loginBloc.state.isValidId, isTrue); + expect(find.text(email), findsOneWidget); + expect(LoginPageTestHelper.idErrorMessageFinder(), findsNothing); + }); + + testWidgets('이메일 유효성 검사 - 실패 케이스', (WidgetTester tester) async { + //Given + const email = 'test'; + whenListen( + loginBloc, + Stream.fromIterable([ + initialState, + initialState.copyWith( + loginId: email, + isValidId: false, + ), + ]), + initialState: initialState, + ); + + // When + await tester.pumpWidget(testWidget); + await tester.enterText(LoginPageTestHelper.idInputFinder(), email); + await tester.pumpAndSettle(); + + // Then + expect(loginBloc.state.loginId, equals(email)); + expect(loginBloc.state.isValidId, isFalse); + expect(find.text(email), findsOneWidget); + expect(LoginPageTestHelper.idErrorMessageFinder(), findsOneWidget); + expect(find.text(StringRes.pleaseEnterValidEmail.tr), findsOneWidget); + }); + + testWidgets('비밀번호 유효성 검사 - 성공 케이스', (WidgetTester tester) async { + //Given + const password = '123qwe!@'; + whenListen( + loginBloc, + Stream.fromIterable([ + initialState, + initialState.copyWith( + password: password, + isValidPassword: true, + ), + ]), + initialState: initialState, + ); + + // When + await tester.pumpWidget(testWidget); + await tester.enterText( + LoginPageTestHelper.passwordInputFinder(), password); + await tester.pumpAndSettle(); + + // Then + expect(loginBloc.state.password, equals(password)); + expect(loginBloc.state.isValidPassword, isTrue); + expect(find.text(password), findsOneWidget); + expect(LoginPageTestHelper.passwordErrorMessageFinder(), findsNothing); }); + + testWidgets('비밀번호 유효성 검사 - 실패 케이스', (WidgetTester tester) async { + //Given + const password = '123qwe'; + whenListen( + loginBloc, + Stream.fromIterable([ + initialState, + initialState.copyWith( + password: password, + isValidPassword: false, + ), + ]), + initialState: initialState, + ); + + // When + await tester.pumpWidget(testWidget); + await tester.enterText( + LoginPageTestHelper.passwordInputFinder(), + password, + ); + await tester.pumpAndSettle(); + + // Then + expect(loginBloc.state.password, equals(password)); + expect(loginBloc.state.isValidPassword, isFalse); + expect(find.text(password), findsOneWidget); + expect(LoginPageTestHelper.passwordErrorMessageFinder(), findsOneWidget); + expect(find.text(StringRes.pleaseEnterValidPassword.tr), findsOneWidget); + }); + + testWidgets('비밀번호 텍스트 표시 테스트', (WidgetTester tester) async { + /// Given + controller.add(initialState.copyWith(isVisiblePassword: false)); + + /// When + await tester.pumpWidget(testWidget); + await tester.pumpAndSettle(); + + /// 사전 검증 - 암호화 상태 확인 + expect(loginBloc.state.isVisiblePassword, isFalse); + expect(LoginPageTestHelper.getPasswordInput(tester).obscureText, true); + + controller.add(initialState.copyWith(isVisiblePassword: true)); + await tester.press(LoginPageTestHelper.passwordVisibleButton()); + await tester.pumpAndSettle(); + + /// Then + expect(loginBloc.state.isVisiblePassword, isTrue); + expect(LoginPageTestHelper.getPasswordInput(tester).obscureText, isFalse); + }); + + testWidgets('비밀번호 텍스트 숨김 테스트', (WidgetTester tester) async { + /// Given + + /// When + await tester.pumpWidget(testWidget); + await tester.pumpAndSettle(); + + /// 사전 검증 - 암호화 상태 확인 + expect(loginBloc.state.isVisiblePassword, isTrue); + expect(LoginPageTestHelper.getPasswordInput(tester).obscureText, isFalse); + + /// 클릭 이벤트 방출 + controller.add(initialState.copyWith(isVisiblePassword: false)); + await tester.press(LoginPageTestHelper.passwordVisibleButton()); + await tester.pumpAndSettle(); + + /// Then + expect(loginBloc.state.isVisiblePassword, isFalse); + expect(LoginPageTestHelper.getPasswordInput(tester).obscureText, isTrue); + }); + + // testWidgets('로그인 요청 - 성공 케이스 테스트', (WidgetTester tester) async { + // /// Given + // const loginId = 'test@test.com'; + // const password = '123qwe!@'; + // final state = initialState.copyWith( + // loginId: loginId, + // isValidId: true, + // password: password, + // isValidPassword: true, + // isEnabledLogin: true, + // ); + // controller.add(state); + // + // /// When + // await tester.pumpWidget(testWidget); + // await tester.pumpAndSettle(); + // + // /// 사전 검증 - 암호화 상태 확인 + // expect(loginBloc.state.isEnabledLogin, isTrue); + // + // /// 클릭 이벤트 방출 + // controller.add(state.copyWith(status: BaseBlocStatus.success())); + // await tester.press(LoginPageTestHelper.loginButtonFinder()); + // await tester.pumpAndSettle(); + // + // /// Then + // expect(loginBloc.state.status, isA()); + // // verify(() => getIt().replaceAll(const JobPostingRouter())) + // }); }); } diff --git a/test/feature/account/presentation/page/login_page_test_helper.dart b/test/feature/account/presentation/page/login_page_test_helper.dart index 349474c..90e6277 100644 --- a/test/feature/account/presentation/page/login_page_test_helper.dart +++ b/test/feature/account/presentation/page/login_page_test_helper.dart @@ -4,26 +4,46 @@ import 'package:withu_app/core/core.dart'; import 'package:withu_app/shared/shared.dart'; class LoginPageTestHelper { + /// Finder: 긱워커 찾기 탭 static Finder companyTabFinder() { return find.byKey(Key('base_tab_${AccountType.company.toString()}')); } + /// Finder: 새로운 일 찾기 탭 static Finder userTabFinder() { return find.byKey(Key('base_tab_${AccountType.user.toString()}')); } + /// Finder: 아이디 TextField static Finder idInputFinder() { return find.byKey(const Key('email_input')); } + /// Finder: 비밀번호 TextFied static Finder passwordInputFinder() { return find.byKey(const Key('password_input')); } + /// Finder: 아이디 TextField + static Finder idErrorMessageFinder() { + return find.byKey(const Key('email_input_error')); + } + + /// Finder: 비밀번호 TextField + static Finder passwordErrorMessageFinder() { + return find.byKey(const Key('password_input_error')); + } + + /// Finder: 로그인 버튼 static Finder loginButtonFinder() { return find.byKey(const Key('login_button')); } + // Finder: 비밀번호 표시 버튼 + static Finder passwordVisibleButton() { + return find.byKey(const Key('password_visible_btn')); + } + static BaseTab getCompanyTab(WidgetTester tester) { return tester.widget(companyTabFinder()); } @@ -39,7 +59,12 @@ class LoginPageTestHelper { static BaseInput getPasswordInput(WidgetTester tester) { return tester.widget(passwordInputFinder()); } + static BaseButton getLoginButton(WidgetTester tester) { return tester.widget(loginButtonFinder()); } + + static BaseButton getPasswordVisibleButton(WidgetTester tester) { + return tester.widget(passwordVisibleButton()); + } } From 18f0ae1612fddfd92b715e1fd295950f35ea2761 Mon Sep 17 00:00:00 2001 From: Conner Date: Sat, 9 Nov 2024 22:53:45 +0900 Subject: [PATCH 13/17] =?UTF-8?q?feat:=20AppRouter=EB=A5=BC=20DI=EB=A1=9C?= =?UTF-8?q?=20=EC=A3=BC=EC=9E=85=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Router 테스트 가능하게 주입 방식으로 변경 --- lib/core/utils/injections.dart | 7 ++ .../presentation/page/login/login_page.dart | 2 +- lib/main.dart | 7 +- .../presentation/page/login_page_test.dart | 106 ++++++++++++------ 4 files changed, 80 insertions(+), 42 deletions(-) diff --git a/lib/core/utils/injections.dart b/lib/core/utils/injections.dart index aa88c2f..2ec41fc 100644 --- a/lib/core/utils/injections.dart +++ b/lib/core/utils/injections.dart @@ -6,11 +6,18 @@ import 'package:withu_app/feature/splash/splash.dart'; final getIt = GetIt.instance; +AppRouter get getItAppRouter => getIt(); + void initCommonInjections() { getIt.registerSingleton(DioNetwork()); } +void initRouterInjections() { + getIt.registerSingleton(AppRouter()); +} + Future initInjections() async { + initRouterInjections(); initCommonInjections(); initAccountInjections(); initSplashInjections(); diff --git a/lib/feature/account/presentation/page/login/login_page.dart b/lib/feature/account/presentation/page/login/login_page.dart index de669e1..0e07f52 100644 --- a/lib/feature/account/presentation/page/login/login_page.dart +++ b/lib/feature/account/presentation/page/login/login_page.dart @@ -45,7 +45,7 @@ class _LoginPageState extends State { listener: (context, state) { /// 로그인 성공 if (state.status.isSuccess) { - context.router.replaceAll([const JobPostingsRoute()]); + getItAppRouter.replaceAll([const JobPostingsRoute()]); } }, builder: (context, state) { diff --git a/lib/main.dart b/lib/main.dart index 33faef2..c3d73fa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,15 +20,14 @@ void run({ fallbackLocale: const Locale('ko'), startLocale: const Locale('ko'), path: 'assets/translations', - child: App(), + child: const App(), ), ); } class App extends StatelessWidget { - final _appRouter = AppRouter(); - App({super.key}); + const App({super.key}); @override Widget build(BuildContext context) { @@ -37,7 +36,7 @@ class App extends StatelessWidget { supportedLocales: context.supportedLocales, locale: context.locale, theme: CustomTheme.theme, - routerConfig: _appRouter.config(), + routerConfig: getItAppRouter.config(), ); } } diff --git a/test/feature/account/presentation/page/login_page_test.dart b/test/feature/account/presentation/page/login_page_test.dart index 93e9f8c..671100e 100644 --- a/test/feature/account/presentation/page/login_page_test.dart +++ b/test/feature/account/presentation/page/login_page_test.dart @@ -1,9 +1,12 @@ import 'dart:async'; +import 'package:auto_route/auto_route.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:withu_app/core/core.dart'; +import 'package:withu_app/core/router/router.gr.dart'; import 'package:withu_app/feature/account/account.dart'; import 'package:withu_app/shared/shared.dart'; @@ -12,17 +15,28 @@ import 'login_page_test_helper.dart'; class MockLoginBloc extends MockBloc implements LoginBloc {} +class MockRouter extends Mock implements AppRouter {} + +class FakePageRouteInfo extends Mock implements PageRouteInfo {} + void main() { group('LoginPage Test', () { - late MockLoginBloc loginBloc; late Widget testWidget; + late MockLoginBloc loginBloc; late LoginState initialState; late StreamController controller; + late MockRouter mockRouter; + /// 테스트 시작 전 setUp(() { loginBloc = MockLoginBloc(); - controller = StreamController(); initialState = LoginState(status: BaseBlocStatus.initial()); + controller = StreamController(); + mockRouter = MockRouter(); + + if(!getIt.isRegistered()) { + getIt.registerSingleton(mockRouter); + } whenListen( loginBloc, @@ -32,6 +46,11 @@ void main() { ), ); + registerFallbackValue(FakePageRouteInfo()); + when(() => mockRouter.push(any())).thenAnswer((_) async => null); + when(() => mockRouter.replaceAll(any())) + .thenAnswer((_) async {}); + testWidget = MaterialApp( home: BlocProvider( create: (context) => loginBloc, @@ -40,6 +59,7 @@ void main() { ); }); + /// 테스트 종료 후 tearDown(() { controller.close(); }); @@ -86,8 +106,12 @@ void main() { // Then expect(loginBloc.state.selectedTab, equals(AccountType.user)); - expect(LoginPageTestHelper.getCompanyTab(tester).isSelected, false); - expect(LoginPageTestHelper.getUserTab(tester).isSelected, true); + expect(LoginPageTestHelper + .getCompanyTab(tester) + .isSelected, false); + expect(LoginPageTestHelper + .getUserTab(tester) + .isSelected, true); }); testWidgets('이메일 유효성 검사 - 성공 케이스', (WidgetTester tester) async { @@ -214,7 +238,9 @@ void main() { /// 사전 검증 - 암호화 상태 확인 expect(loginBloc.state.isVisiblePassword, isFalse); - expect(LoginPageTestHelper.getPasswordInput(tester).obscureText, true); + expect(LoginPageTestHelper + .getPasswordInput(tester) + .obscureText, true); controller.add(initialState.copyWith(isVisiblePassword: true)); await tester.press(LoginPageTestHelper.passwordVisibleButton()); @@ -222,7 +248,9 @@ void main() { /// Then expect(loginBloc.state.isVisiblePassword, isTrue); - expect(LoginPageTestHelper.getPasswordInput(tester).obscureText, isFalse); + expect(LoginPageTestHelper + .getPasswordInput(tester) + .obscureText, isFalse); }); testWidgets('비밀번호 텍스트 숨김 테스트', (WidgetTester tester) async { @@ -234,7 +262,9 @@ void main() { /// 사전 검증 - 암호화 상태 확인 expect(loginBloc.state.isVisiblePassword, isTrue); - expect(LoginPageTestHelper.getPasswordInput(tester).obscureText, isFalse); + expect(LoginPageTestHelper + .getPasswordInput(tester) + .obscureText, isFalse); /// 클릭 이벤트 방출 controller.add(initialState.copyWith(isVisiblePassword: false)); @@ -243,37 +273,39 @@ void main() { /// Then expect(loginBloc.state.isVisiblePassword, isFalse); - expect(LoginPageTestHelper.getPasswordInput(tester).obscureText, isTrue); + expect(LoginPageTestHelper + .getPasswordInput(tester) + .obscureText, isTrue); }); - // testWidgets('로그인 요청 - 성공 케이스 테스트', (WidgetTester tester) async { - // /// Given - // const loginId = 'test@test.com'; - // const password = '123qwe!@'; - // final state = initialState.copyWith( - // loginId: loginId, - // isValidId: true, - // password: password, - // isValidPassword: true, - // isEnabledLogin: true, - // ); - // controller.add(state); - // - // /// When - // await tester.pumpWidget(testWidget); - // await tester.pumpAndSettle(); - // - // /// 사전 검증 - 암호화 상태 확인 - // expect(loginBloc.state.isEnabledLogin, isTrue); - // - // /// 클릭 이벤트 방출 - // controller.add(state.copyWith(status: BaseBlocStatus.success())); - // await tester.press(LoginPageTestHelper.loginButtonFinder()); - // await tester.pumpAndSettle(); - // - // /// Then - // expect(loginBloc.state.status, isA()); - // // verify(() => getIt().replaceAll(const JobPostingRouter())) - // }); + testWidgets('로그인 요청 - 성공 케이스 테스트', (WidgetTester tester) async { + /// Given + const loginId = 'test@test.com'; + const password = '123qwe!@'; + final state = initialState.copyWith( + loginId: loginId, + isValidId: true, + password: password, + isValidPassword: true, + isEnabledLogin: true, + ); + controller.add(state); + + /// When + await tester.pumpWidget(testWidget); + await tester.pumpAndSettle(); + + /// 사전 검증 - 암호화 상태 확인 + expect(loginBloc.state.isEnabledLogin, isTrue); + + /// 클릭 이벤트 방출 + controller.add(state.copyWith(status: BaseBlocStatus.success())); + await tester.press(LoginPageTestHelper.loginButtonFinder()); + await tester.pumpAndSettle(); + + /// Then + expect(loginBloc.state.status, isA()); + verify(() => getItAppRouter.replaceAll([const JobPostingsRoute()])).called(1); + }); }); } From 033e36679a38b86271ace7de8a2c52edfe24d097 Mon Sep 17 00:00:00 2001 From: Conner Date: Sun, 10 Nov 2024 00:29:05 +0900 Subject: [PATCH 14/17] =?UTF-8?q?test:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/utils/bloc/base_bloc_state.dart | 2 + .../data/data_sources/mock/mock_api.dart | 2 +- .../account/domain/usecase/usecase_impl.dart | 4 +- lib/feature/account/init_injections.dart | 2 + .../presentation/bloc/login/login_bloc.dart | 1 + .../bloc/login/login_bloc.handler.dart | 8 +++ .../presentation/bloc/login/login_event.dart | 3 + .../presentation/page/login/login_page.dart | 13 ++++ lib/main.dart | 1 - .../account/domain/usecase/usecase_test.dart | 30 ++++++--- .../presentation/page/login_page_test.dart | 63 ++++++++++++------- 11 files changed, 94 insertions(+), 35 deletions(-) diff --git a/lib/core/utils/bloc/base_bloc_state.dart b/lib/core/utils/bloc/base_bloc_state.dart index 9e3f3d3..8405ac5 100644 --- a/lib/core/utils/bloc/base_bloc_state.dart +++ b/lib/core/utils/bloc/base_bloc_state.dart @@ -20,6 +20,8 @@ abstract class BaseBlocStatus { bool get isSuccess => this is BaseBlocStatusSuccess; + bool get isFailure => this is BaseBlocStatusFailure; + bool get isRefresh => this is BaseBlocStatusRefresh; } diff --git a/lib/feature/account/data/data_sources/mock/mock_api.dart b/lib/feature/account/data/data_sources/mock/mock_api.dart index 38d5683..497f7e4 100644 --- a/lib/feature/account/data/data_sources/mock/mock_api.dart +++ b/lib/feature/account/data/data_sources/mock/mock_api.dart @@ -20,7 +20,7 @@ class AccountMockApi extends AccountApiImpl { loginPath, (server) => server.reply( 200, - LoginResponseDtoMock.success().toJson(), + LoginResponseDtoMock.failure().toJson(), delay: const Duration(seconds: 1), ), data: requestData.toJson(), diff --git a/lib/feature/account/domain/usecase/usecase_impl.dart b/lib/feature/account/domain/usecase/usecase_impl.dart index d9bb900..5033e2a 100644 --- a/lib/feature/account/domain/usecase/usecase_impl.dart +++ b/lib/feature/account/domain/usecase/usecase_impl.dart @@ -15,13 +15,13 @@ class AccountUseCaseImpl implements AccountUseCase { requestData: entity.toDto(), ); - _storeSessionId(id: result.successData?.sessionId ?? ''); + storeSessionId(id: result.successData?.sessionId ?? ''); return LoginResultEntityConverter.fromDto(result: result); } /// 세션 Id 저장 - void _storeSessionId({ + void storeSessionId({ required String id, }) { if (id.isNotEmpty) { diff --git a/lib/feature/account/init_injections.dart b/lib/feature/account/init_injections.dart index 953171f..8818b74 100644 --- a/lib/feature/account/init_injections.dart +++ b/lib/feature/account/init_injections.dart @@ -1,6 +1,8 @@ import 'package:withu_app/core/core.dart'; import 'package:withu_app/feature/account/account.dart'; +LoginBloc get getItLoginBloc => getIt(); + void initAccountInjections() { getIt.registerSingleton( Environment.isProd diff --git a/lib/feature/account/presentation/bloc/login/login_bloc.dart b/lib/feature/account/presentation/bloc/login/login_bloc.dart index c4ddd2b..55ecca1 100644 --- a/lib/feature/account/presentation/bloc/login/login_bloc.dart +++ b/lib/feature/account/presentation/bloc/login/login_bloc.dart @@ -21,6 +21,7 @@ class LoginBloc extends BaseBloc { }) : super( LoginState(status: BaseBlocStatus.initial()), ) { + on(_onMessageCleared); on(_onIdInputted); on(_onPasswordInputted); on(_onBtnPressed); diff --git a/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart b/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart index bdf7db0..c7ab1cf 100644 --- a/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart +++ b/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart @@ -1,6 +1,14 @@ part of 'login_bloc.dart'; extension LoginBlocHandler on LoginBloc { + /// 메시지 초기화 + void _onMessageCleared( + LoginMessageCleared event, + Emitter emit, + ) { + emit(state.copyWith(message: '')); + } + /// 아이디 입력 void _onIdInputted( LoginIdInputted event, diff --git a/lib/feature/account/presentation/bloc/login/login_event.dart b/lib/feature/account/presentation/bloc/login/login_event.dart index 092614e..f3b38aa 100644 --- a/lib/feature/account/presentation/bloc/login/login_event.dart +++ b/lib/feature/account/presentation/bloc/login/login_event.dart @@ -2,6 +2,9 @@ part of 'login_bloc.dart'; sealed class LoginEvent extends BaseBlocEvent {} +/// 메시지 초기화 +class LoginMessageCleared extends LoginEvent {} + /// 아이디 입력 이벤트 class LoginIdInputted extends LoginEvent { final String id; diff --git a/lib/feature/account/presentation/page/login/login_page.dart b/lib/feature/account/presentation/page/login/login_page.dart index 0e07f52..4281fd4 100644 --- a/lib/feature/account/presentation/page/login/login_page.dart +++ b/lib/feature/account/presentation/page/login/login_page.dart @@ -47,6 +47,19 @@ class _LoginPageState extends State { if (state.status.isSuccess) { getItAppRouter.replaceAll([const JobPostingsRoute()]); } + + /// 로그인 실패 + if (state.status.isFailure) { + if (state.message.isNotEmpty) { + CustomAlertDialog.showContentAlert( + context: context, + content: state.message, + closeCallback: () { + getItLoginBloc.add(LoginMessageCleared()); + }, + ); + } + } }, builder: (context, state) { return PageRoot( diff --git a/lib/main.dart b/lib/main.dart index c3d73fa..4b12e6b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,7 +26,6 @@ void run({ } class App extends StatelessWidget { - const App({super.key}); @override diff --git a/test/feature/account/domain/usecase/usecase_test.dart b/test/feature/account/domain/usecase/usecase_test.dart index 2f3cafd..0e9bfa8 100644 --- a/test/feature/account/domain/usecase/usecase_test.dart +++ b/test/feature/account/domain/usecase/usecase_test.dart @@ -1,30 +1,38 @@ -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:withu_app/core/core.dart'; import 'package:withu_app/feature/account/account.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'usecase_test.mocks.dart'; -@GenerateMocks([AccountRepository]) +class MockAccountRepository extends Mock implements AccountRepository {} + void main() { late MockAccountRepository mockRepo; late AccountUseCase useCase; + setUpAll(() { + registerFallbackValue(LoginRequestDtoMock.mock()); + }); + setUp(() { mockRepo = MockAccountRepository(); useCase = AccountUseCaseImpl(accountRepo: mockRepo); }); group('Account UseCase 테스트', () { - test('로그인 성공', () async { + test('로그인 요청 - 성공 케이스 테스트', () async { // Given final successResponseDto = LoginResponseDtoMock.success(); when( - mockRepo.login(requestData: LoginRequestDtoMock.mock()), + () => mockRepo.login(requestData: LoginRequestDtoMock.mock()), ).thenAnswer( (_) async => ApiResponse.success(successResponseDto), ); + when( + () => mockRepo.storeSessionId(id: any(named: 'id')), + ).thenAnswer( + (_) async => {}, + ); // When final result = await useCase.login( @@ -32,13 +40,15 @@ void main() { ); // Then - expect(result.isLoggedIn, true); + expect(result.isLoggedIn, isTrue); + expect(result.isLoggedIn, isTrue); + verify(() => mockRepo.storeSessionId(id: any(named: 'id'))).called(1); }); test('서버 에러로 인한 로그인 실패', () async { // Given when( - mockRepo.login(requestData: LoginRequestDtoMock.mock()), + () => mockRepo.login(requestData: any(named: 'requestData')), ).thenAnswer( (_) async => ApiResponse.fail(FailResponse.error()), ); @@ -49,7 +59,9 @@ void main() { ); // Then - expect(result.isLoggedIn, false); + verify(() => mockRepo.login(requestData: any(named: 'requestData'))) + .called(1); + expect(result.isLoggedIn, isFalse); expect(result.message, StringRes.serverError.tr); }); }); diff --git a/test/feature/account/presentation/page/login_page_test.dart b/test/feature/account/presentation/page/login_page_test.dart index 671100e..6a0341a 100644 --- a/test/feature/account/presentation/page/login_page_test.dart +++ b/test/feature/account/presentation/page/login_page_test.dart @@ -34,7 +34,7 @@ void main() { controller = StreamController(); mockRouter = MockRouter(); - if(!getIt.isRegistered()) { + if (!getIt.isRegistered()) { getIt.registerSingleton(mockRouter); } @@ -48,8 +48,7 @@ void main() { registerFallbackValue(FakePageRouteInfo()); when(() => mockRouter.push(any())).thenAnswer((_) async => null); - when(() => mockRouter.replaceAll(any())) - .thenAnswer((_) async {}); + when(() => mockRouter.replaceAll(any())).thenAnswer((_) async {}); testWidget = MaterialApp( home: BlocProvider( @@ -106,12 +105,8 @@ void main() { // Then expect(loginBloc.state.selectedTab, equals(AccountType.user)); - expect(LoginPageTestHelper - .getCompanyTab(tester) - .isSelected, false); - expect(LoginPageTestHelper - .getUserTab(tester) - .isSelected, true); + expect(LoginPageTestHelper.getCompanyTab(tester).isSelected, false); + expect(LoginPageTestHelper.getUserTab(tester).isSelected, true); }); testWidgets('이메일 유효성 검사 - 성공 케이스', (WidgetTester tester) async { @@ -238,9 +233,7 @@ void main() { /// 사전 검증 - 암호화 상태 확인 expect(loginBloc.state.isVisiblePassword, isFalse); - expect(LoginPageTestHelper - .getPasswordInput(tester) - .obscureText, true); + expect(LoginPageTestHelper.getPasswordInput(tester).obscureText, true); controller.add(initialState.copyWith(isVisiblePassword: true)); await tester.press(LoginPageTestHelper.passwordVisibleButton()); @@ -248,9 +241,7 @@ void main() { /// Then expect(loginBloc.state.isVisiblePassword, isTrue); - expect(LoginPageTestHelper - .getPasswordInput(tester) - .obscureText, isFalse); + expect(LoginPageTestHelper.getPasswordInput(tester).obscureText, isFalse); }); testWidgets('비밀번호 텍스트 숨김 테스트', (WidgetTester tester) async { @@ -262,9 +253,7 @@ void main() { /// 사전 검증 - 암호화 상태 확인 expect(loginBloc.state.isVisiblePassword, isTrue); - expect(LoginPageTestHelper - .getPasswordInput(tester) - .obscureText, isFalse); + expect(LoginPageTestHelper.getPasswordInput(tester).obscureText, isFalse); /// 클릭 이벤트 방출 controller.add(initialState.copyWith(isVisiblePassword: false)); @@ -273,9 +262,7 @@ void main() { /// Then expect(loginBloc.state.isVisiblePassword, isFalse); - expect(LoginPageTestHelper - .getPasswordInput(tester) - .obscureText, isTrue); + expect(LoginPageTestHelper.getPasswordInput(tester).obscureText, isTrue); }); testWidgets('로그인 요청 - 성공 케이스 테스트', (WidgetTester tester) async { @@ -305,7 +292,39 @@ void main() { /// Then expect(loginBloc.state.status, isA()); - verify(() => getItAppRouter.replaceAll([const JobPostingsRoute()])).called(1); + verify(() => getItAppRouter.replaceAll([const JobPostingsRoute()])) + .called(1); + }); + + testWidgets('로그인 요청 - 실패 케이스 테스트', (WidgetTester tester) async { + /// Given + const loginId = 'test@test.com'; + const password = '123qwe!@'; + const failMessage = '존재하지 않는 계정입니다.'; + final state = initialState.copyWith( + loginId: loginId, + isValidId: true, + password: password, + isValidPassword: true, + isEnabledLogin: true, + ); + controller.add(state); + + /// When + await tester.pumpWidget(testWidget); + await tester.pumpAndSettle(); + + /// 클릭 이벤트 방출 + controller.add(state.copyWith( + status: BaseBlocStatus.failure(), + message: failMessage, + )); + await tester.press(LoginPageTestHelper.loginButtonFinder()); + await tester.pumpAndSettle(); + + /// Then + expect(loginBloc.state.status, isA()); + expect(loginBloc.state.message, failMessage); }); }); } From 37124f067c86a82e36947ecba90c925e8eff5e44 Mon Sep 17 00:00:00 2001 From: Conner Date: Sun, 10 Nov 2024 00:32:16 +0900 Subject: [PATCH 15/17] =?UTF-8?q?chore:=20pre-commit=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1ebe844..d278f5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,10 +20,3 @@ repos: entry: bash -c 'flutter test' language: system type: [ dart ] - -# - id: dart-format -# name: dart-format -# entry: bash -c 'dart format "$@"' -# language: system -# type: [ dart ] -# files: lib/.*\.dart$ From f739c80537f2b602d82c74351079f9185cc7e51d Mon Sep 17 00:00:00 2001 From: Conner Date: Sun, 10 Nov 2024 00:48:25 +0900 Subject: [PATCH 16/17] =?UTF-8?q?feat:=20getItLoginBloc=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. LoginBloc의 경우 싱글톤이 아닌 팩토리로 등록하기 때문에 getIt으로 조회하면 각각의 인스턴스로 취급되는 것 같음 --- lib/feature/account/init_injections.dart | 2 -- .../bloc/login/login_bloc.handler.dart | 5 ++++- .../presentation/page/login/login_page.dart | 20 +++++++++---------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/feature/account/init_injections.dart b/lib/feature/account/init_injections.dart index 8818b74..953171f 100644 --- a/lib/feature/account/init_injections.dart +++ b/lib/feature/account/init_injections.dart @@ -1,8 +1,6 @@ import 'package:withu_app/core/core.dart'; import 'package:withu_app/feature/account/account.dart'; -LoginBloc get getItLoginBloc => getIt(); - void initAccountInjections() { getIt.registerSingleton( Environment.isProd diff --git a/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart b/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart index c7ab1cf..af77a57 100644 --- a/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart +++ b/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart @@ -6,7 +6,10 @@ extension LoginBlocHandler on LoginBloc { LoginMessageCleared event, Emitter emit, ) { - emit(state.copyWith(message: '')); + emit(state.copyWith( + status: BaseBlocStatus.initial(), + message: '', + )); } /// 아이디 입력 diff --git a/lib/feature/account/presentation/page/login/login_page.dart b/lib/feature/account/presentation/page/login/login_page.dart index 4281fd4..0fdc0b6 100644 --- a/lib/feature/account/presentation/page/login/login_page.dart +++ b/lib/feature/account/presentation/page/login/login_page.dart @@ -42,23 +42,21 @@ class _LoginPageState extends State { @override Widget build(BuildContext context) { return BlocConsumer( - listener: (context, state) { + listener: (context, state) async { /// 로그인 성공 if (state.status.isSuccess) { getItAppRouter.replaceAll([const JobPostingsRoute()]); } /// 로그인 실패 - if (state.status.isFailure) { - if (state.message.isNotEmpty) { - CustomAlertDialog.showContentAlert( - context: context, - content: state.message, - closeCallback: () { - getItLoginBloc.add(LoginMessageCleared()); - }, - ); - } + if (state.status.isFailure && state.message.isNotEmpty) { + await CustomAlertDialog.showContentAlert( + context: context, + content: state.message, + closeCallback: () { + context.read().add(LoginMessageCleared()); + }, + ); } }, builder: (context, state) { From 15b6f169345426727e0095697ef4270346ada8b0 Mon Sep 17 00:00:00 2001 From: Conner Date: Wed, 13 Nov 2024 04:18:16 +0900 Subject: [PATCH 17/17] =?UTF-8?q?feat:=20=EB=B3=B5=EC=9E=A1=ED=95=9C=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=EB=AC=B8=EC=9D=84=20=EC=84=9C=EB=B8=8C?= =?UTF-8?q?=ED=95=A8=EC=88=98=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 복잡한 & 조건문을 hasFailMessage로 변경 2. 코드에서 정확한 의도를 반영할 수 있게 변경 --- .../entity/login/result/login_result_entity.converter.dart | 1 - lib/feature/account/presentation/bloc/login/login_state.dart | 3 +++ lib/feature/account/presentation/page/login/login_page.dart | 3 +-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/feature/account/domain/entity/login/result/login_result_entity.converter.dart b/lib/feature/account/domain/entity/login/result/login_result_entity.converter.dart index 0b20749..9e47efa 100644 --- a/lib/feature/account/domain/entity/login/result/login_result_entity.converter.dart +++ b/lib/feature/account/domain/entity/login/result/login_result_entity.converter.dart @@ -13,7 +13,6 @@ extension LoginResultEntityConverter on LoginResultEntity { success: (dto) { return LoginResultEntity( isLoggedIn: dto.status, - message: dto.message, ); }, orElse: () { diff --git a/lib/feature/account/presentation/bloc/login/login_state.dart b/lib/feature/account/presentation/bloc/login/login_state.dart index 7a5c992..2b4312e 100644 --- a/lib/feature/account/presentation/bloc/login/login_state.dart +++ b/lib/feature/account/presentation/bloc/login/login_state.dart @@ -31,4 +31,7 @@ extension LoginStateExt on LoginState { bool checkLoginEnabled() { return loginId.isValid && password.isValid; } + + /// 메시지가 있는지 검사. + bool get hasFailMessage => status.isFailure && message.isNotEmpty; } diff --git a/lib/feature/account/presentation/page/login/login_page.dart b/lib/feature/account/presentation/page/login/login_page.dart index 305c4eb..0a058c7 100644 --- a/lib/feature/account/presentation/page/login/login_page.dart +++ b/lib/feature/account/presentation/page/login/login_page.dart @@ -48,8 +48,7 @@ class _LoginPageState extends State { getItAppRouter.replaceAll([const JobPostingsRoute()]); } - /// 로그인 실패 - if (state.status.isFailure && state.message.isNotEmpty) { + if (state.hasFailMessage) { await CustomAlertDialog.showContentAlert( context: context, content: state.message,