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/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f1e81f..d278f5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,9 +15,8 @@ repos: type: [ dart ] files: lib/.*\.dart$ -# - id: dart-format -# name: dart-format -# entry: bash -c 'dart format "$@"' -# language: system -# type: [ dart ] -# files: lib/.*\.dart$ + - id: flutter-test + name: flutter-test + entry: bash -c 'flutter test' + language: system + type: [ dart ] 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/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/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/bloc/base_bloc_state.dart b/lib/core/utils/bloc/base_bloc_state.dart index 2228113..8405ac5 100644 --- a/lib/core/utils/bloc/base_bloc_state.dart +++ b/lib/core/utils/bloc/base_bloc_state.dart @@ -14,10 +14,14 @@ 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; + bool get isFailure => this is BaseBlocStatusFailure; + bool get isRefresh => this is BaseBlocStatusRefresh; } diff --git a/lib/core/utils/injections.dart b/lib/core/utils/injections.dart index 9b0cdcd..2ec41fc 100644 --- a/lib/core/utils/injections.dart +++ b/lib/core/utils/injections.dart @@ -1,11 +1,24 @@ 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; +AppRouter get getItAppRouter => getIt(); + +void initCommonInjections() { + getIt.registerSingleton(DioNetwork()); +} + +void initRouterInjections() { + getIt.registerSingleton(AppRouter()); +} + Future initInjections() async { + initRouterInjections(); + initCommonInjections(); initAccountInjections(); initSplashInjections(); initJobPostingInjections(); 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/mock/mock_api.dart b/lib/feature/account/data/data_sources/mock/mock_api.dart index 01b864a..497f7e4 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.network}) { + dioAdapter = DioAdapter(dio: network.dio); + } + /// 로그인 API @override FutureOr> login({ @@ -14,7 +20,7 @@ class AccountMockApi extends AccountApiImpl with MockAPI { 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/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..c0eac95 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 DioNetwork network; + + AccountApiImpl({required this.network}); + /// 로그인 API @override FutureOr> login({ required LoginRequestDto requestData, }) async { - return dio + return network.dio .post( loginPath, data: requestData.toJson(), @@ -17,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/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/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/login_result_entity.freezed.dart b/lib/feature/account/domain/entity/login/login_result_entity.freezed.dart deleted file mode 100644 index 9461f40..0000000 --- a/lib/feature/account/domain/entity/login/login_result_entity.freezed.dart +++ /dev/null @@ -1,165 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'login_result_entity.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -/// @nodoc -mixin _$LoginResultEntity { - bool get isLoggedIn => throw _privateConstructorUsedError; // 로그인 성공 엽 - String get message => throw _privateConstructorUsedError; - - /// Create a copy of LoginResultEntity - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $LoginResultEntityCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $LoginResultEntityCopyWith<$Res> { - factory $LoginResultEntityCopyWith( - LoginResultEntity value, $Res Function(LoginResultEntity) then) = - _$LoginResultEntityCopyWithImpl<$Res, LoginResultEntity>; - @useResult - $Res call({bool isLoggedIn, String message}); -} - -/// @nodoc -class _$LoginResultEntityCopyWithImpl<$Res, $Val extends LoginResultEntity> - implements $LoginResultEntityCopyWith<$Res> { - _$LoginResultEntityCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of LoginResultEntity - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? isLoggedIn = null, - Object? message = null, - }) { - return _then(_value.copyWith( - isLoggedIn: null == isLoggedIn - ? _value.isLoggedIn - : isLoggedIn // ignore: cast_nullable_to_non_nullable - as bool, - message: null == message - ? _value.message - : message // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$LoginResultEntityImplCopyWith<$Res> - implements $LoginResultEntityCopyWith<$Res> { - factory _$$LoginResultEntityImplCopyWith(_$LoginResultEntityImpl value, - $Res Function(_$LoginResultEntityImpl) then) = - __$$LoginResultEntityImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({bool isLoggedIn, String message}); -} - -/// @nodoc -class __$$LoginResultEntityImplCopyWithImpl<$Res> - extends _$LoginResultEntityCopyWithImpl<$Res, _$LoginResultEntityImpl> - implements _$$LoginResultEntityImplCopyWith<$Res> { - __$$LoginResultEntityImplCopyWithImpl(_$LoginResultEntityImpl _value, - $Res Function(_$LoginResultEntityImpl) _then) - : super(_value, _then); - - /// Create a copy of LoginResultEntity - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? isLoggedIn = null, - Object? message = null, - }) { - return _then(_$LoginResultEntityImpl( - isLoggedIn: null == isLoggedIn - ? _value.isLoggedIn - : isLoggedIn // ignore: cast_nullable_to_non_nullable - as bool, - message: null == message - ? _value.message - : message // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc - -class _$LoginResultEntityImpl implements _LoginResultEntity { - _$LoginResultEntityImpl({required this.isLoggedIn, this.message = ''}); - - @override - final bool isLoggedIn; -// 로그인 성공 엽 - @override - @JsonKey() - final String message; - - @override - String toString() { - return 'LoginResultEntity(isLoggedIn: $isLoggedIn, message: $message)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$LoginResultEntityImpl && - (identical(other.isLoggedIn, isLoggedIn) || - other.isLoggedIn == isLoggedIn) && - (identical(other.message, message) || other.message == message)); - } - - @override - int get hashCode => Object.hash(runtimeType, isLoggedIn, message); - - /// Create a copy of LoginResultEntity - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$LoginResultEntityImplCopyWith<_$LoginResultEntityImpl> get copyWith => - __$$LoginResultEntityImplCopyWithImpl<_$LoginResultEntityImpl>( - this, _$identity); -} - -abstract class _LoginResultEntity implements LoginResultEntity { - factory _LoginResultEntity( - {required final bool isLoggedIn, - final String message}) = _$LoginResultEntityImpl; - - @override - bool get isLoggedIn; // 로그인 성공 엽 - @override - String get message; - - /// Create a copy of LoginResultEntity - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$LoginResultEntityImplCopyWith<_$LoginResultEntityImpl> get copyWith => - throw _privateConstructorUsedError; -} 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 86% 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..9e47efa 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(); @@ -13,7 +13,6 @@ extension LoginResultEntityParser on LoginResultEntity { success: (dto) { return LoginResultEntity( isLoggedIn: dto.status, - message: dto.message, ); }, orElse: () { 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 c53560f..5033e2a 100644 --- a/lib/feature/account/domain/usecase/usecase_impl.dart +++ b/lib/feature/account/domain/usecase/usecase_impl.dart @@ -9,27 +9,19 @@ 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 ?? ''); + storeSessionId(id: result.successData?.sessionId ?? ''); - return LoginResultEntityParser.fromDto(result: result); + 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 16f01b7..953171f 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(network: getIt()) + : AccountMockApi(network: getIt()), ); getIt.registerSingleton( AccountStorageImpl(), diff --git a/lib/feature/account/presentation/bloc/login/login_bloc.dart b/lib/feature/account/presentation/bloc/login/login_bloc.dart index 9781424..55ecca1 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; @@ -19,9 +21,11 @@ class LoginBloc extends BaseBloc { }) : super( LoginState(status: BaseBlocStatus.initial()), ) { + on(_onMessageCleared); on(_onIdInputted); on(_onPasswordInputted); on(_onBtnPressed); on(_onTabPressed); + on(_onVisiblePasswordToggled); } } diff --git a/lib/feature/account/presentation/bloc/login/login_bloc.freezed.dart b/lib/feature/account/presentation/bloc/login/login_bloc.freezed.dart index 7581b1c..40e0021 100644 --- a/lib/feature/account/presentation/bloc/login/login_bloc.freezed.dart +++ b/lib/feature/account/presentation/bloc/login/login_bloc.freezed.dart @@ -28,6 +28,9 @@ mixin _$LoginState { /// password Password get password => throw _privateConstructorUsedError; + /// password Visible 여부 + bool get isVisiblePassword => throw _privateConstructorUsedError; + /// 로그인 버튼 enabled bool get isEnabledLogin => throw _privateConstructorUsedError; @@ -52,6 +55,7 @@ abstract class $LoginStateCopyWith<$Res> { String message, LoginId loginId, Password password, + bool isVisiblePassword, bool isEnabledLogin, AccountType selectedTab}); } @@ -75,6 +79,7 @@ class _$LoginStateCopyWithImpl<$Res, $Val extends LoginState> Object? message = null, Object? loginId = null, Object? password = null, + Object? isVisiblePassword = null, Object? isEnabledLogin = null, Object? selectedTab = null, }) { @@ -95,6 +100,10 @@ class _$LoginStateCopyWithImpl<$Res, $Val extends LoginState> ? _value.password : password // ignore: cast_nullable_to_non_nullable as Password, + isVisiblePassword: null == isVisiblePassword + ? _value.isVisiblePassword + : isVisiblePassword // ignore: cast_nullable_to_non_nullable + as bool, isEnabledLogin: null == isEnabledLogin ? _value.isEnabledLogin : isEnabledLogin // ignore: cast_nullable_to_non_nullable @@ -120,6 +129,7 @@ abstract class _$$LoginStateImplCopyWith<$Res> String message, LoginId loginId, Password password, + bool isVisiblePassword, bool isEnabledLogin, AccountType selectedTab}); } @@ -141,6 +151,7 @@ class __$$LoginStateImplCopyWithImpl<$Res> Object? message = null, Object? loginId = null, Object? password = null, + Object? isVisiblePassword = null, Object? isEnabledLogin = null, Object? selectedTab = null, }) { @@ -161,6 +172,10 @@ class __$$LoginStateImplCopyWithImpl<$Res> ? _value.password : password // ignore: cast_nullable_to_non_nullable as Password, + isVisiblePassword: null == isVisiblePassword + ? _value.isVisiblePassword + : isVisiblePassword // ignore: cast_nullable_to_non_nullable + as bool, isEnabledLogin: null == isEnabledLogin ? _value.isEnabledLogin : isEnabledLogin // ignore: cast_nullable_to_non_nullable @@ -181,6 +196,7 @@ class _$LoginStateImpl implements _LoginState { this.message = '', this.loginId = Email.empty, this.password = Password.empty, + this.isVisiblePassword = false, this.isEnabledLogin = false, this.selectedTab = AccountType.company}); @@ -203,6 +219,11 @@ class _$LoginStateImpl implements _LoginState { @JsonKey() final Password password; + /// password Visible 여부 + @override + @JsonKey() + final bool isVisiblePassword; + /// 로그인 버튼 enabled @override @JsonKey() @@ -215,7 +236,7 @@ class _$LoginStateImpl implements _LoginState { @override String toString() { - return 'LoginState(status: $status, message: $message, loginId: $loginId, password: $password, isEnabledLogin: $isEnabledLogin, selectedTab: $selectedTab)'; + return 'LoginState(status: $status, message: $message, loginId: $loginId, password: $password, isVisiblePassword: $isVisiblePassword, isEnabledLogin: $isEnabledLogin, selectedTab: $selectedTab)'; } @override @@ -228,6 +249,8 @@ class _$LoginStateImpl implements _LoginState { (identical(other.loginId, loginId) || other.loginId == loginId) && (identical(other.password, password) || other.password == password) && + (identical(other.isVisiblePassword, isVisiblePassword) || + other.isVisiblePassword == isVisiblePassword) && (identical(other.isEnabledLogin, isEnabledLogin) || other.isEnabledLogin == isEnabledLogin) && (identical(other.selectedTab, selectedTab) || @@ -236,7 +259,7 @@ class _$LoginStateImpl implements _LoginState { @override int get hashCode => Object.hash(runtimeType, status, message, loginId, - password, isEnabledLogin, selectedTab); + password, isVisiblePassword, isEnabledLogin, selectedTab); /// Create a copy of LoginState /// with the given fields replaced by the non-null parameter values. @@ -253,6 +276,7 @@ abstract class _LoginState implements LoginState { final String message, final LoginId loginId, final Password password, + final bool isVisiblePassword, final bool isEnabledLogin, final AccountType selectedTab}) = _$LoginStateImpl; @@ -272,6 +296,10 @@ abstract class _LoginState implements LoginState { @override Password get password; + /// password Visible 여부 + @override + bool get isVisiblePassword; + /// 로그인 버튼 enabled @override bool get isEnabledLogin; 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 335c3bf..72089e6 100644 --- a/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart +++ b/lib/feature/account/presentation/bloc/login/login_bloc.handler.dart @@ -1,15 +1,33 @@ part of 'login_bloc.dart'; extension LoginBlocHandler on LoginBloc { + /// 메시지 초기화 + void _onMessageCleared( + LoginMessageCleared event, + Emitter emit, + ) { + emit(state.copyWith( + status: BaseBlocStatus.initial(), + message: '', + )); + } + + /// 로그인 버튼 활성화 상태 체크 및 emit + void _checkLoginEnabled(Emitter emit) { + emit(state.copyWith( + isEnabledLogin: state.checkLoginEnabled( + loginId: state.loginId, + password: state.password, + ))); + } + /// 아이디 입력 void _onIdInputted( LoginIdInputted event, Emitter emit, ) { - emit(state.copyWith( - loginId: event.loginId, - isEnabledLogin: state.checkLoginEnabled(loginId: event.loginId), - )); + emit(state.copyWith(loginId: event.loginId)); + _checkLoginEnabled(emit); } /// 비밀번호 입력 @@ -17,10 +35,8 @@ extension LoginBlocHandler on LoginBloc { LoginPasswordInputted event, Emitter emit, ) { - emit(state.copyWith( - password: event.password, - isEnabledLogin: state.checkLoginEnabled(password: event.password), - )); + emit(state.copyWith(password: event.password)); + _checkLoginEnabled(emit); } /// 로그인 버튼 클릭 @@ -31,10 +47,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.value, - password: state.password.value, + entity: toEntity(), ); emit(state.copyWith( @@ -52,4 +65,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_bloc_converter.dart b/lib/feature/account/presentation/bloc/login/login_bloc_converter.dart new file mode 100644 index 0000000..c77c523 --- /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.value, + password: state.password.value, + ); + } +} diff --git a/lib/feature/account/presentation/bloc/login/login_event.dart b/lib/feature/account/presentation/bloc/login/login_event.dart index 2378ce8..28d6b41 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 value; @@ -31,3 +34,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 4c67f94..777ade1 100644 --- a/lib/feature/account/presentation/bloc/login/login_state.dart +++ b/lib/feature/account/presentation/bloc/login/login_state.dart @@ -15,6 +15,9 @@ class LoginState extends BaseBlocState with _$LoginState { /// password @Default(Password.empty) Password password, + /// password Visible 여부 + @Default(false) bool isVisiblePassword, + /// 로그인 버튼 enabled @Default(false) bool isEnabledLogin, @@ -29,7 +32,9 @@ extension LoginStateExt on LoginState { LoginId loginId = Email.empty, Password password = Password.empty, }) { - logger.i(loginId.isValid); 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 df6d482..0a058c7 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(); @@ -40,10 +42,20 @@ class _LoginPageState extends State<_LoginPage> { @override Widget build(BuildContext context) { return BlocConsumer( - listener: (context, state) { + listener: (context, state) async { /// 로그인 성공 if (state.status.isSuccess) { - context.router.replaceAll([const JobPostingsRoute()]); + getItAppRouter.replaceAll([const JobPostingsRoute()]); + } + + if (state.hasFailMessage) { + await CustomAlertDialog.showContentAlert( + context: context, + content: state.message, + closeCallback: () { + context.read().add(LoginMessageCleared()); + }, + ); } }, builder: (context, state) { @@ -69,6 +81,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.loginId.isValid, @@ -78,13 +91,20 @@ class _LoginPageState extends State<_LoginPage> { ), const SizedBox(height: 30), BaseInput.password( + key: const Key('password_input'), errorText: StringRes.pleaseEnterValidPassword.tr, + obscureText: !state.isVisiblePassword, errorVisible: !state.password.isValid, onChanged: (String text) { context .read() .add(LoginPasswordInputted(value: text)); }, + onSuffixPressed: () { + context + .read() + .add(LoginVisiblePasswordToggled()); + }, ), const SizedBox(height: 80), Row( @@ -108,6 +128,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 +145,7 @@ class _LoginButton extends StatelessWidget { final bool enabled; const _LoginButton({ + super.key, required this.enabled, }); 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 f134cb1..092259c 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'; /// 공고 목록 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 2cf9408..b7565b8 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/feature/splash/presentation/bloc/splash_bloc.dart b/lib/feature/splash/presentation/bloc/splash_bloc.dart index cfa3af4..8ef6e9b 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'; import 'package:withu_app/feature/splash/type/type.dart'; @@ -29,8 +30,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(), nextPage: SplashNextPageTypeExt.getNextPage(isLoggedIn), diff --git a/lib/main.dart b/lib/main.dart index 33faef2..4b12e6b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,15 +20,13 @@ 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 +35,7 @@ class App extends StatelessWidget { supportedLocales: context.supportedLocales, locale: context.locale, theme: CustomTheme.theme, - routerConfig: _appRouter.config(), + routerConfig: getItAppRouter.config(), ); } } diff --git a/lib/shared/widgets/base_input/base_input.dart b/lib/shared/widgets/base_input/base_input.dart index b99bc7e..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,42 +87,51 @@ 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, + ), ], ); } /// 이메일 형식 입력 factory BaseInput.email({ + Key? key, TextEditingController? controller, FocusNode? focusNode, TextInputAction? textInputAction, @@ -127,6 +140,7 @@ class BaseInput extends StatelessWidget { bool errorVisible = false, }) { return BaseInput( + key: key, controller: controller, focusNode: focusNode, keyboardType: TextInputType.emailAddress, @@ -140,46 +154,51 @@ class BaseInput extends StatelessWidget { /// 비밀번호 형식 입력 factory BaseInput.password({ + Key? key, TextEditingController? controller, FocusNode? focusNode, TextInputAction? textInputAction, 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/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/pubspec.lock b/pubspec.lock index 413c8fe..793b93f 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: @@ -483,7 +507,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 +674,22 @@ 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" + mocktail: + dependency: "direct main" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" nested: dependency: transitive description: @@ -658,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: @@ -834,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: @@ -871,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: @@ -919,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: @@ -927,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: @@ -1023,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 c051c59..c1195f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,11 +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 + mocktail: ^1.0.4 + dev_dependencies: flutter_test: @@ -68,12 +76,13 @@ dev_dependencies: # Resource manager flutter_gen_runner: - # api mocking - http_mock_adapter: ^0.6.1 - # 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/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..e80a3c1 --- /dev/null +++ b/test/feature/account/data/data_source/repository/repository_impl_test.dart @@ -0,0 +1,78 @@ +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.success( + LoginResponseDtoMock.failure(), + ), + ); + + // 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..0e9bfa8 --- /dev/null +++ b/test/feature/account/domain/usecase/usecase_test.dart @@ -0,0 +1,68 @@ +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'; + +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 { + // Given + final successResponseDto = LoginResponseDtoMock.success(); + + when( + () => 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( + entity: LoginRequestEntityMock.mock(), + ); + + // Then + expect(result.isLoggedIn, isTrue); + expect(result.isLoggedIn, isTrue); + verify(() => mockRepo.storeSessionId(id: any(named: 'id'))).called(1); + }); + + test('서버 에러로 인한 로그인 실패', () async { + // Given + when( + () => mockRepo.login(requestData: any(named: 'requestData')), + ).thenAnswer( + (_) async => ApiResponse.fail(FailResponse.error()), + ); + + // When + final result = await useCase.login( + entity: LoginRequestEntityMock.mock(), + ); + + // Then + 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/bloc/bloc_test.dart b/test/feature/account/presentation/bloc/bloc_test.dart new file mode 100644 index 0000000..75df512 --- /dev/null +++ b/test/feature/account/presentation/bloc/bloc_test.dart @@ -0,0 +1,247 @@ +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, Email.empty); + expect(loginBloc.state.password, Password.empty); + expect(loginBloc.state.isVisiblePassword, false); + 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(value: 'test@test.com')), + expect: () => [ + isA() + .having( + (state) => state.loginId, + 'loginId', + const Email(value: 'test@test.com'), + ) + .having( + (state) => state.loginId.isValid, + 'isValidId', + true, + ) + .having( + (state) => state.isEnabledLogin, + 'isEnabledLogin', + false, // password가 아직 없으므로 false, + ), + ], + ); + + blocTest( + '잘못된 이메일 입력 이벤트 검사', + build: () => loginBloc, + act: (bloc) => bloc.add(LoginIdInputted(value: 'test')), + expect: () => [ + isA() + .having( + (state) => state.loginId, + 'loginId', + const Email(value: 'test'), + ) + .having( + (state) => state.loginId.isValid, + 'isValid', + false, + ) + .having( + (state) => state.isEnabledLogin, + 'isEnabledLogin', + false, // password가 아직 없으므로 false, + ), + ], + ); + + blocTest( + '비밀번호 입력 이벤트 검사', + build: () => loginBloc, + act: (bloc) => bloc.add(LoginPasswordInputted(value: '123qwe!@')), + expect: () => [ + isA() + .having( + (state) => state.password, + 'password', + const Password('123qwe!@'), + ) + .having( + (state) => state.password.isValid, + 'isValid', + true, + ) + .having( + (state) => state.isEnabledLogin, + 'isEnabledLogin', + false, + ), + ], + ); + + blocTest( + '8자리 미만 비밀번호 입력 이벤트 검사', + build: () => loginBloc, + act: (bloc) => bloc.add(LoginPasswordInputted(value: '12qw!@')), + expect: () => [ + isA() + .having( + (state) => state.password, + 'password', + const Password('12qw!@'), + ) + .having( + (state) => state.password.isValid, + 'isValidPassword', + false, + ) + .having( + (state) => state.isEnabledLogin, + 'isEnabledLogin', + false, + ), + ], + ); + + blocTest( + '아이디, 비밀번호 입력 시 isEnabledLogin 검사', + build: () => loginBloc, + act: (bloc) => [ + bloc.add(LoginIdInputted(value: 'test@test.com')), + bloc.add(LoginPasswordInputted(value: '123qwe!@')), + ], + expect: () { + const loginId = Email(value: 'test@test.com'); + const password = Password('123qwe!@'); + return [ + /// 이메일 입력 후 상태 + isA() + .having((state) => state.loginId, 'loginId', loginId) + .having((state) => state.loginId.isValid, 'isValid', true), + + /// 비밀번호도 입력 후 상태 + isA() + .having((state) => state.password, 'password', password) + .having((state) => state.password.isValid, 'isValid', true), + + /// 비밀번호도 입력 후 상태 + isA() + .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); + }, + ); + }); +} 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..bab5b49 --- /dev/null +++ b/test/feature/account/presentation/page/login_page_test.dart @@ -0,0 +1,322 @@ +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'; + +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 Widget testWidget; + late MockLoginBloc loginBloc; + late LoginState initialState; + late StreamController controller; + late MockRouter mockRouter; + + /// 테스트 시작 전 + setUp(() { + loginBloc = MockLoginBloc(); + initialState = LoginState(status: BaseBlocStatus.initial()); + controller = StreamController(); + mockRouter = MockRouter(); + + if (!getIt.isRegistered()) { + getIt.registerSingleton(mockRouter); + } + + whenListen( + loginBloc, + controller.stream, + initialState: initialState.copyWith( + isVisiblePassword: true, + ), + ); + + registerFallbackValue(FakePageRouteInfo()); + when(() => mockRouter.push(any())).thenAnswer((_) async => null); + when(() => mockRouter.replaceAll(any())).thenAnswer((_) async {}); + + testWidget = MaterialApp( + home: BlocProvider( + create: (context) => loginBloc, + child: const LoginPageContent(), // 로그인 페이지 위젯 + ), + ); + }); + + /// 테스트 종료 후 + tearDown(() { + controller.close(); + }); + + 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); + + // 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 + whenListen( + loginBloc, + Stream.fromIterable([ + initialState, + initialState.copyWith( + selectedTab: AccountType.user, + ) + ]), + initialState: initialState, + ); + + // When + await tester.pumpWidget(testWidget); + await tester.tap(LoginPageTestHelper.userTabFinder()); + await tester.pumpAndSettle(); + + // Then + expect(loginBloc.state.selectedTab, equals(AccountType.user)); + expect(LoginPageTestHelper.getCompanyTab(tester).isSelected, false); + expect(LoginPageTestHelper.getUserTab(tester).isSelected, true); + }); + + testWidgets('이메일 유효성 검사 - 성공 케이스', (WidgetTester tester) async { + //Given + const email = 'test@test.com'; + whenListen( + loginBloc, + Stream.fromIterable([ + initialState, + initialState.copyWith( + loginId: const Email(value: email), + ), + ]), + initialState: initialState, + ); + + // When + await tester.pumpWidget(testWidget); + await tester.enterText(LoginPageTestHelper.idInputFinder(), email); + await tester.pumpAndSettle(); + + // Then + expect(loginBloc.state.loginId, equals(const Email(value:email))); + expect(loginBloc.state.loginId.isValid, 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: const Email(value:email), + ), + ]), + initialState: initialState, + ); + + // When + await tester.pumpWidget(testWidget); + await tester.enterText(LoginPageTestHelper.idInputFinder(), email); + await tester.pumpAndSettle(); + + // Then + expect(loginBloc.state.loginId, equals(const Email(value:email))); + expect(loginBloc.state.loginId.isValid, 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: const Password(password), + ), + ]), + initialState: initialState, + ); + + // When + await tester.pumpWidget(testWidget); + await tester.enterText( + LoginPageTestHelper.passwordInputFinder(), password); + await tester.pumpAndSettle(); + + // Then + expect(loginBloc.state.password, equals(const Password(password),)); + expect(loginBloc.state.password.isValid, 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: const Password(password), + ), + ]), + initialState: initialState, + ); + + // When + await tester.pumpWidget(testWidget); + await tester.enterText( + LoginPageTestHelper.passwordInputFinder(), + password, + ); + await tester.pumpAndSettle(); + + // Then + expect(loginBloc.state.password, equals(const Password(password))); + expect(loginBloc.state.password.isValid, 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: const Email(value:loginId), + password: const Password(password), + 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); + }); + + testWidgets('로그인 요청 - 실패 케이스 테스트', (WidgetTester tester) async { + /// Given + const loginId = 'test@test.com'; + const password = '123qwe!@'; + const failMessage = '존재하지 않는 계정입니다.'; + final state = initialState.copyWith( + loginId: const Email(value:loginId), + password: const Password(password), + 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); + }); + }); +} 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..90e6277 --- /dev/null +++ b/test/feature/account/presentation/page/login_page_test_helper.dart @@ -0,0 +1,70 @@ +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 { + /// 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()); + } + + 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()); + } + + static BaseButton getPasswordVisibleButton(WidgetTester tester) { + return tester.widget(passwordVisibleButton()); + } +}