From 69fd9e0a55937601528239dca2123ac4d86c3b26 Mon Sep 17 00:00:00 2001 From: grinevich Date: Thu, 15 Aug 2024 14:23:45 +0300 Subject: [PATCH] sprint13 is done --- ImageFeed.xcodeproj/project.pbxproj | 262 ++++++++++++++++++ ImageFeed/Auth/AuthHelper.swift | 54 ++++ ImageFeed/Auth/AuthViewController.swift | 4 + ImageFeed/Auth/WebViewPresenter.swift | 48 ++++ ImageFeed/Auth/WebViewViewController.swift | 52 ++-- ImageFeed/Helpers/Constants.swift | 27 ++ ImageFeed/ImagesList/ImagesListCell.swift | 3 +- .../ImagesList/ImagesListPresenter.swift | 80 ++++++ .../ImagesList/ImagesListViewController.swift | 77 ++--- ImageFeed/Profile/ProfileViewController.swift | 57 ++-- ImageFeed/Profile/ProfileViewPresenter.swift | 57 ++++ ImageFeed/Services/ImagesListService.swift | 4 +- .../SingleImageViewController.swift | 1 + .../TabBarViewController.swift | 4 + ImageFeedTests/ImagesListTests.swift | 47 ++++ ImageFeedTests/ProfileViewTests.swift | 44 +++ ImageFeedTests/WebViewTests.swift | 115 ++++++++ ImageFeedUITests/ImageFeedUITests.swift | 94 +++++++ .../ImageFeedUITestsLaunchTests.swift | 32 +++ 19 files changed, 942 insertions(+), 120 deletions(-) create mode 100644 ImageFeed/Auth/AuthHelper.swift create mode 100644 ImageFeed/Auth/WebViewPresenter.swift create mode 100644 ImageFeed/ImagesList/ImagesListPresenter.swift create mode 100644 ImageFeed/Profile/ProfileViewPresenter.swift create mode 100644 ImageFeedTests/ImagesListTests.swift create mode 100644 ImageFeedTests/ProfileViewTests.swift create mode 100644 ImageFeedTests/WebViewTests.swift create mode 100644 ImageFeedUITests/ImageFeedUITests.swift create mode 100644 ImageFeedUITests/ImageFeedUITestsLaunchTests.swift diff --git a/ImageFeed.xcodeproj/project.pbxproj b/ImageFeed.xcodeproj/project.pbxproj index 73879d2..8239afb 100644 --- a/ImageFeed.xcodeproj/project.pbxproj +++ b/ImageFeed.xcodeproj/project.pbxproj @@ -16,6 +16,12 @@ 5B196CE22C30604D008C34EF /* OAuth2TokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B196CE12C30604D008C34EF /* OAuth2TokenStorage.swift */; }; 5B196CE42C306C17008C34EF /* SplashViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B196CE32C306C17008C34EF /* SplashViewController.swift */; }; 5B54A1792C1B348E000F7302 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B54A1782C1B348E000F7302 /* ProfileViewController.swift */; }; + 5B621F752C6620D60098B477 /* WebViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B621F742C6620D60098B477 /* WebViewPresenter.swift */; }; + 5B621F772C6760610098B477 /* AuthHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B621F762C6760610098B477 /* AuthHelper.swift */; }; + 5B621F7F2C6762A90098B477 /* WebViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B621F7E2C6762A90098B477 /* WebViewTests.swift */; }; + 5B621F862C677B740098B477 /* ProfileViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B621F852C677B740098B477 /* ProfileViewPresenter.swift */; }; + 5B621F882C6D37750098B477 /* ImagesListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B621F872C6D37750098B477 /* ImagesListPresenter.swift */; }; + 5B621F8A2C6D46700098B477 /* ImagesListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B621F892C6D46700098B477 /* ImagesListTests.swift */; }; 5B69A3092C0B626E0057123F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B69A3082C0B626E0057123F /* AppDelegate.swift */; }; 5B69A30B2C0B626E0057123F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B69A30A2C0B626E0057123F /* SceneDelegate.swift */; }; 5B69A30D2C0B626E0057123F /* ImagesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B69A30C2C0B626E0057123F /* ImagesListViewController.swift */; }; @@ -31,11 +37,31 @@ 5B7CFFCD2C43032100A43C4F /* ProfileImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B7CFFCC2C43032100A43C4F /* ProfileImageService.swift */; }; 5B9E6DD12C59507E008F101E /* ProfileLogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9E6DD02C59507E008F101E /* ProfileLogoutService.swift */; }; 5BB462F52C403AB2003672F9 /* ProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 5BB462F42C403AB2003672F9 /* ProgressHUD */; }; + 5BB4C7E02C6D4FA800B8E948 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB4C7DF2C6D4FA800B8E948 /* ProfileViewTests.swift */; }; + 5BB4C7E82C6DFE4200B8E948 /* ImageFeedUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB4C7E72C6DFE4200B8E948 /* ImageFeedUITests.swift */; }; + 5BB4C7EA2C6DFE4200B8E948 /* ImageFeedUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB4C7E92C6DFE4200B8E948 /* ImageFeedUITestsLaunchTests.swift */; }; 5BC5A08A2C4437DB009697F1 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 5BC5A0892C4437DB009697F1 /* Kingfisher */; }; 5BCA62312C1E178A007850E6 /* UIView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BCA62302C1E178A007850E6 /* UIView+Extension.swift */; }; 5BFB30A52C44F62500E80746 /* SwiftKeychainWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = 5BFB30A42C44F62500E80746 /* SwiftKeychainWrapper */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 5B621F802C6762A90098B477 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5B69A2FD2C0B626E0057123F /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5B69A3042C0B626E0057123F; + remoteInfo = ImageFeed; + }; + 5BB4C7EB2C6DFE4200B8E948 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5B69A2FD2C0B626E0057123F /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5B69A3042C0B626E0057123F; + remoteInfo = ImageFeed; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 5B0AB6422C5375D80080F11F /* ImagesListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesListService.swift; sourceTree = ""; }; 5B196CD52C301C52008C34EF /* AuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewController.swift; sourceTree = ""; }; @@ -46,6 +72,13 @@ 5B196CE12C30604D008C34EF /* OAuth2TokenStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2TokenStorage.swift; sourceTree = ""; }; 5B196CE32C306C17008C34EF /* SplashViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewController.swift; sourceTree = ""; }; 5B54A1782C1B348E000F7302 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; + 5B621F742C6620D60098B477 /* WebViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewPresenter.swift; sourceTree = ""; }; + 5B621F762C6760610098B477 /* AuthHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthHelper.swift; sourceTree = ""; }; + 5B621F7C2C6762A90098B477 /* ImageFeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImageFeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 5B621F7E2C6762A90098B477 /* WebViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewTests.swift; sourceTree = ""; }; + 5B621F852C677B740098B477 /* ProfileViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewPresenter.swift; sourceTree = ""; }; + 5B621F872C6D37750098B477 /* ImagesListPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesListPresenter.swift; sourceTree = ""; }; + 5B621F892C6D46700098B477 /* ImagesListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesListTests.swift; sourceTree = ""; }; 5B69A3052C0B626E0057123F /* ImageFeed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ImageFeed.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5B69A3082C0B626E0057123F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 5B69A30A2C0B626E0057123F /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -62,10 +95,21 @@ 5B7CFFCA2C42A6D400A43C4F /* ProfileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileService.swift; sourceTree = ""; }; 5B7CFFCC2C43032100A43C4F /* ProfileImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImageService.swift; sourceTree = ""; }; 5B9E6DD02C59507E008F101E /* ProfileLogoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileLogoutService.swift; sourceTree = ""; }; + 5BB4C7DF2C6D4FA800B8E948 /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = ""; }; + 5BB4C7E52C6DFE4200B8E948 /* ImageFeedUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImageFeedUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 5BB4C7E72C6DFE4200B8E948 /* ImageFeedUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFeedUITests.swift; sourceTree = ""; }; + 5BB4C7E92C6DFE4200B8E948 /* ImageFeedUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFeedUITestsLaunchTests.swift; sourceTree = ""; }; 5BCA62302C1E178A007850E6 /* UIView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extension.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 5B621F792C6762A90098B477 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5B69A3022C0B626E0057123F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -76,6 +120,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5BB4C7E22C6DFE4200B8E948 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -92,6 +143,8 @@ children = ( 5B196CD52C301C52008C34EF /* AuthViewController.swift */, 5B196CD82C302408008C34EF /* WebViewViewController.swift */, + 5B621F742C6620D60098B477 /* WebViewPresenter.swift */, + 5B621F762C6760610098B477 /* AuthHelper.swift */, ); path = Auth; sourceTree = ""; @@ -110,10 +163,22 @@ path = Services; sourceTree = ""; }; + 5B621F7D2C6762A90098B477 /* ImageFeedTests */ = { + isa = PBXGroup; + children = ( + 5B621F7E2C6762A90098B477 /* WebViewTests.swift */, + 5B621F892C6D46700098B477 /* ImagesListTests.swift */, + 5BB4C7DF2C6D4FA800B8E948 /* ProfileViewTests.swift */, + ); + path = ImageFeedTests; + sourceTree = ""; + }; 5B69A2FC2C0B626E0057123F = { isa = PBXGroup; children = ( 5B69A3072C0B626E0057123F /* ImageFeed */, + 5B621F7D2C6762A90098B477 /* ImageFeedTests */, + 5BB4C7E62C6DFE4200B8E948 /* ImageFeedUITests */, 5B69A3062C0B626E0057123F /* Products */, ); sourceTree = ""; @@ -122,6 +187,8 @@ isa = PBXGroup; children = ( 5B69A3052C0B626E0057123F /* ImageFeed.app */, + 5B621F7C2C6762A90098B477 /* ImageFeedTests.xctest */, + 5BB4C7E52C6DFE4200B8E948 /* ImageFeedUITests.xctest */, ); name = Products; sourceTree = ""; @@ -158,6 +225,7 @@ children = ( 5B69A31D2C0B7E0A0057123F /* ImagesListCell.swift */, 5B69A30C2C0B626E0057123F /* ImagesListViewController.swift */, + 5B621F872C6D37750098B477 /* ImagesListPresenter.swift */, ); path = ImagesList; sourceTree = ""; @@ -183,10 +251,20 @@ path = App; sourceTree = ""; }; + 5BB4C7E62C6DFE4200B8E948 /* ImageFeedUITests */ = { + isa = PBXGroup; + children = ( + 5BB4C7E72C6DFE4200B8E948 /* ImageFeedUITests.swift */, + 5BB4C7E92C6DFE4200B8E948 /* ImageFeedUITestsLaunchTests.swift */, + ); + path = ImageFeedUITests; + sourceTree = ""; + }; 5BCA622E2C1E131F007850E6 /* Profile */ = { isa = PBXGroup; children = ( 5B54A1782C1B348E000F7302 /* ProfileViewController.swift */, + 5B621F852C677B740098B477 /* ProfileViewPresenter.swift */, ); path = Profile; sourceTree = ""; @@ -210,6 +288,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 5B621F7B2C6762A90098B477 /* ImageFeedTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5B621F822C6762A90098B477 /* Build configuration list for PBXNativeTarget "ImageFeedTests" */; + buildPhases = ( + 5B621F782C6762A90098B477 /* Sources */, + 5B621F792C6762A90098B477 /* Frameworks */, + 5B621F7A2C6762A90098B477 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5B621F812C6762A90098B477 /* PBXTargetDependency */, + ); + name = ImageFeedTests; + productName = ImageFeedTests; + productReference = 5B621F7C2C6762A90098B477 /* ImageFeedTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 5B69A3042C0B626E0057123F /* ImageFeed */ = { isa = PBXNativeTarget; buildConfigurationList = 5B69A3192C0B626F0057123F /* Build configuration list for PBXNativeTarget "ImageFeed" */; @@ -232,6 +328,24 @@ productReference = 5B69A3052C0B626E0057123F /* ImageFeed.app */; productType = "com.apple.product-type.application"; }; + 5BB4C7E42C6DFE4200B8E948 /* ImageFeedUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5BB4C7ED2C6DFE4200B8E948 /* Build configuration list for PBXNativeTarget "ImageFeedUITests" */; + buildPhases = ( + 5BB4C7E12C6DFE4200B8E948 /* Sources */, + 5BB4C7E22C6DFE4200B8E948 /* Frameworks */, + 5BB4C7E32C6DFE4200B8E948 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5BB4C7EC2C6DFE4200B8E948 /* PBXTargetDependency */, + ); + name = ImageFeedUITests; + productName = ImageFeedUITests; + productReference = 5BB4C7E52C6DFE4200B8E948 /* ImageFeedUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -242,9 +356,17 @@ LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 1540; TargetAttributes = { + 5B621F7B2C6762A90098B477 = { + CreatedOnToolsVersion = 15.4; + TestTargetID = 5B69A3042C0B626E0057123F; + }; 5B69A3042C0B626E0057123F = { CreatedOnToolsVersion = 15.4; }; + 5BB4C7E42C6DFE4200B8E948 = { + CreatedOnToolsVersion = 15.4; + TestTargetID = 5B69A3042C0B626E0057123F; + }; }; }; buildConfigurationList = 5B69A3002C0B626E0057123F /* Build configuration list for PBXProject "ImageFeed" */; @@ -266,11 +388,20 @@ projectRoot = ""; targets = ( 5B69A3042C0B626E0057123F /* ImageFeed */, + 5B621F7B2C6762A90098B477 /* ImageFeedTests */, + 5BB4C7E42C6DFE4200B8E948 /* ImageFeedUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 5B621F7A2C6762A90098B477 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5B69A3032C0B626E0057123F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -280,9 +411,26 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5BB4C7E32C6DFE4200B8E948 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 5B621F782C6762A90098B477 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B621F7F2C6762A90098B477 /* WebViewTests.swift in Sources */, + 5BB4C7E02C6D4FA800B8E948 /* ProfileViewTests.swift in Sources */, + 5B621F8A2C6D46700098B477 /* ImagesListTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5B69A3012C0B626E0057123F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -291,11 +439,15 @@ 5B196CD62C301C52008C34EF /* AuthViewController.swift in Sources */, 5B7B29462C1CC5C9005533D2 /* TabBarViewController.swift in Sources */, 5B196CE42C306C17008C34EF /* SplashViewController.swift in Sources */, + 5B621F882C6D37750098B477 /* ImagesListPresenter.swift in Sources */, + 5B621F752C6620D60098B477 /* WebViewPresenter.swift in Sources */, + 5B621F772C6760610098B477 /* AuthHelper.swift in Sources */, 5B0AB6432C5375D80080F11F /* ImagesListService.swift in Sources */, 5B9E6DD12C59507E008F101E /* ProfileLogoutService.swift in Sources */, 5B196CDB2C304588008C34EF /* OAuth2Service.swift in Sources */, 5B7B29482C1CCEAE005533D2 /* SingleImageViewController.swift in Sources */, 5B69A3212C0B93400057123F /* Constants.swift in Sources */, + 5B621F862C677B740098B477 /* ProfileViewPresenter.swift in Sources */, 5B69A30D2C0B626E0057123F /* ImagesListViewController.swift in Sources */, 5B69A31E2C0B7E0A0057123F /* ImagesListCell.swift in Sources */, 5B69A3232C0B96490057123F /* UIColor+Extension.swift in Sources */, @@ -312,8 +464,30 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5BB4C7E12C6DFE4200B8E948 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BB4C7EA2C6DFE4200B8E948 /* ImageFeedUITestsLaunchTests.swift in Sources */, + 5BB4C7E82C6DFE4200B8E948 /* ImageFeedUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 5B621F812C6762A90098B477 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5B69A3042C0B626E0057123F /* ImageFeed */; + targetProxy = 5B621F802C6762A90098B477 /* PBXContainerItemProxy */; + }; + 5BB4C7EC2C6DFE4200B8E948 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5B69A3042C0B626E0057123F /* ImageFeed */; + targetProxy = 5BB4C7EB2C6DFE4200B8E948 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 5B69A3132C0B626F0057123F /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; @@ -326,6 +500,42 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 5B621F832C6762A90098B477 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4Z6GQTMC99; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.grinevich.ImageFeedTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ImageFeed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ImageFeed"; + }; + name = Debug; + }; + 5B621F842C6762A90098B477 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4Z6GQTMC99; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.grinevich.ImageFeedTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ImageFeed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ImageFeed"; + }; + name = Release; + }; 5B69A3172C0B626F0057123F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -507,9 +717,52 @@ }; name = Release; }; + 5BB4C7EE2C6DFE4200B8E948 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4Z6GQTMC99; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.grinevich.ImageFeedUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = ImageFeed; + }; + name = Debug; + }; + 5BB4C7EF2C6DFE4200B8E948 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4Z6GQTMC99; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.grinevich.ImageFeedUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = ImageFeed; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 5B621F822C6762A90098B477 /* Build configuration list for PBXNativeTarget "ImageFeedTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5B621F832C6762A90098B477 /* Debug */, + 5B621F842C6762A90098B477 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 5B69A3002C0B626E0057123F /* Build configuration list for PBXProject "ImageFeed" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -528,6 +781,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 5BB4C7ED2C6DFE4200B8E948 /* Build configuration list for PBXNativeTarget "ImageFeedUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5BB4C7EE2C6DFE4200B8E948 /* Debug */, + 5BB4C7EF2C6DFE4200B8E948 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/ImageFeed/Auth/AuthHelper.swift b/ImageFeed/Auth/AuthHelper.swift new file mode 100644 index 0000000..d08e5f9 --- /dev/null +++ b/ImageFeed/Auth/AuthHelper.swift @@ -0,0 +1,54 @@ +// +// AuthHelper.swift +// ImageFeed +// +// Created by Юрий Гриневич on 10.08.2024. +// + +import Foundation + +protocol AuthHelperProtocol { + func authRequest() -> URLRequest? + func code(from url: URL) -> String? +} + +final class AuthHelper: AuthHelperProtocol { + let configuration: AuthConfiguration + + init(configuration: AuthConfiguration = .standard) { + self.configuration = configuration + } + + func authRequest() -> URLRequest? { + guard let url = authURL() else { return nil } + + return URLRequest(url: url) + } + + func authURL() -> URL? { + guard var urlComponents = URLComponents(string: configuration.authURLString) else { + return nil + } + + urlComponents.queryItems = [ + URLQueryItem(name: "client_id", value: configuration.accessKey), + URLQueryItem(name: "redirect_uri", value: configuration.redirectURI), + URLQueryItem(name: "response_type", value: "code"), + URLQueryItem(name: "scope", value: configuration.accessScope) + ] + + return urlComponents.url + } + + func code(from url: URL) -> String? { + if let urlComponents = URLComponents(string: url.absoluteString), + urlComponents.path == "/oauth/authorize/native", + let items = urlComponents.queryItems, + let codeItem = items.first(where: { $0.name == "code" }) + { + return codeItem.value + } else { + return nil + } + } +} diff --git a/ImageFeed/Auth/AuthViewController.swift b/ImageFeed/Auth/AuthViewController.swift index a46913f..5fd74f1 100644 --- a/ImageFeed/Auth/AuthViewController.swift +++ b/ImageFeed/Auth/AuthViewController.swift @@ -48,6 +48,10 @@ final class AuthViewController: UIViewController { @objc private func didTapLoginButton() { let vc = WebViewViewController() + let authHelper = AuthHelper() + let webViewPresenter = WebViewPresenter(authHelper: authHelper) + vc.presenter = webViewPresenter + webViewPresenter.view = vc vc.delegate = self navigationController?.pushViewController(vc, animated: true) } diff --git a/ImageFeed/Auth/WebViewPresenter.swift b/ImageFeed/Auth/WebViewPresenter.swift new file mode 100644 index 0000000..0f61fa0 --- /dev/null +++ b/ImageFeed/Auth/WebViewPresenter.swift @@ -0,0 +1,48 @@ +// +// WebViewPresenter.swift +// ImageFeed +// +// Created by Юрий Гриневич on 09.08.2024. +// + +import Foundation + +public protocol WebViewPresenterProtocol { + var view: WebViewViewControllerProtocol? { get set } + func viewDidLoad() + func didUpdateProgressValue(_ newValue: Double) + func code(from url: URL) -> String? +} + +final class WebViewPresenter: WebViewPresenterProtocol { + + weak var view: WebViewViewControllerProtocol? + var authHelper: AuthHelperProtocol + + init(authHelper: AuthHelperProtocol) { + self.authHelper = authHelper + } + + func viewDidLoad() { + guard let request = authHelper.authRequest() else { return } + + view?.load(request: request) + didUpdateProgressValue(0) + } + + func didUpdateProgressValue(_ newValue: Double) { + let newProgressValue = Float(newValue) + view?.setProgressValue(newProgressValue) + + let shouldHideProgress = shouldHideProgress(for: newProgressValue) + view?.setProgressHidden(shouldHideProgress) + } + + func shouldHideProgress(for value: Float) -> Bool { + abs(value - 1.0) <= 0.0001 + } + + func code(from url: URL) -> String? { + authHelper.code(from: url) + } +} diff --git a/ImageFeed/Auth/WebViewViewController.swift b/ImageFeed/Auth/WebViewViewController.swift index 526c298..5dd9e02 100644 --- a/ImageFeed/Auth/WebViewViewController.swift +++ b/ImageFeed/Auth/WebViewViewController.swift @@ -8,14 +8,22 @@ import UIKit import WebKit +public protocol WebViewViewControllerProtocol: AnyObject { + var presenter: WebViewPresenterProtocol? { get set } + func load(request: URLRequest) + func setProgressValue(_ newValue: Float) + func setProgressHidden(_ isHidden: Bool) +} + protocol WebViewViewControllerDelegate: AnyObject { func webViewViewController(_ vc: WebViewViewController, didAuthenticateWithCode code: String) func webViewViewControllerDidCancel(_ vc: WebViewViewController) } -final class WebViewViewController: UIViewController { +final class WebViewViewController: UIViewController, WebViewViewControllerProtocol { weak var delegate: WebViewViewControllerDelegate? + var presenter: WebViewPresenterProtocol? private let progressBar: UIProgressView = { let bar = UIProgressView() @@ -26,6 +34,7 @@ final class WebViewViewController: UIViewController { private let webView: WKWebView = { let web = WKWebView() + web.accessibilityIdentifier = "UnsplashWebView" web.translatesAutoresizingMaskIntoConstraints = false return web }() @@ -35,41 +44,27 @@ final class WebViewViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() configureUI() - loadAuthView() + presenter?.viewDidLoad() estimatedProgressObservation = webView.observe( \.estimatedProgress, options: [], changeHandler: { [weak self] _, _ in guard let self = self else { return } - self.updateProgress() + self.presenter?.didUpdateProgressValue(webView.estimatedProgress) }) } - private func loadAuthView() { - guard var urlComponents = URLComponents(string: Constants.unsplashAuthorizeURLString) else { - return - } - - urlComponents.queryItems = [ - URLQueryItem(name: "client_id", value: Constants.accessKey), - URLQueryItem(name: "redirect_uri", value: Constants.redirectURI), - URLQueryItem(name: "response_type", value: "code"), - URLQueryItem(name: "scope", value: Constants.accessScope) - ] - - guard let url = urlComponents.url else { - print("Function: \(#function), line \(#line) Failed to get URL") - return - } - - let request = URLRequest(url: url) + func load(request: URLRequest) { webView.load(request) } - private func updateProgress() { - progressBar.setProgress(Float(webView.estimatedProgress), animated: true) - progressBar.isHidden = fabs(webView.estimatedProgress - 1.0) <= 0.0001 + func setProgressValue(_ newValue: Float) { + progressBar.setProgress(newValue, animated: true) + } + + func setProgressHidden(_ isHidden: Bool) { + progressBar.isHidden = isHidden } @@ -105,13 +100,8 @@ extension WebViewViewController: WKNavigationDelegate { } private func code(from navigationAction: WKNavigationAction) -> String? { - if let url = navigationAction.request.url, - let urlComponents = URLComponents(string: url.absoluteString), - urlComponents.path == "/oauth/authorize/native", - let items = urlComponents.queryItems, - let codeItem = items.first(where: { $0.name == "code" }) { - - return codeItem.value + if let url = navigationAction.request.url { + return presenter?.code(from: url) } else { return nil diff --git a/ImageFeed/Helpers/Constants.swift b/ImageFeed/Helpers/Constants.swift index b8a5835..122d4c0 100644 --- a/ImageFeed/Helpers/Constants.swift +++ b/ImageFeed/Helpers/Constants.swift @@ -19,3 +19,30 @@ enum Constants { static let unsplashAuthorizeURLString = "https://unsplash.com/oauth/authorize" static let tokenURL = "https://unsplash.com/oauth/token" } + +struct AuthConfiguration { + let accessKey: String + let secretKey: String + let redirectURI: String + let accessScope: String + let defaultBaseURL: URL + let authURLString: String + + init(accessKey: String, secretKey: String, redirectURI: String, accessScope: String, authURLString: String, defaultBaseURL: URL) { + self.accessKey = accessKey + self.secretKey = secretKey + self.redirectURI = redirectURI + self.accessScope = accessScope + self.defaultBaseURL = defaultBaseURL + self.authURLString = authURLString + } + + static var standard: AuthConfiguration { + return AuthConfiguration(accessKey: Constants.accessKey, + secretKey: Constants.secretKey, + redirectURI: Constants.redirectURI, + accessScope: Constants.accessScope, + authURLString: Constants.unsplashAuthorizeURLString, + defaultBaseURL: Constants.defaultBaseURL) + } +} diff --git a/ImageFeed/ImagesList/ImagesListCell.swift b/ImageFeed/ImagesList/ImagesListCell.swift index a92f455..121e91d 100644 --- a/ImageFeed/ImagesList/ImagesListCell.swift +++ b/ImageFeed/ImagesList/ImagesListCell.swift @@ -30,6 +30,7 @@ final class ImagesListCell: UITableViewCell { private let likeButton: UIButton = { let button = UIButton() + button.accessibilityIdentifier = "LikeButton" button.translatesAutoresizingMaskIntoConstraints = false return button }() @@ -64,7 +65,7 @@ final class ImagesListCell: UITableViewCell { configureUI() } - override func prepareForReuse() { + override func prepareForReuse() { super.prepareForReuse() imageFeed.kf.cancelDownloadTask() } diff --git a/ImageFeed/ImagesList/ImagesListPresenter.swift b/ImageFeed/ImagesList/ImagesListPresenter.swift new file mode 100644 index 0000000..8ab287e --- /dev/null +++ b/ImageFeed/ImagesList/ImagesListPresenter.swift @@ -0,0 +1,80 @@ +// +// ImageListPresenter.swift +// ImageFeed +// +// Created by Юрий Гриневич on 14.08.2024. +// + +import Foundation + +protocol ImagesListPresenterProtocol { + var view: ImagesListViewControllerProtocol? { get set } + var photos: [Photo] { get set } + func viewDidLoad() + func willDisplay(for indexPath: IndexPath) + func imageListCellDidTapLike(_ cell: ImagesListCell, indexPath: IndexPath) + func updateTableViewAnimated() +} + +final class ImagesListPresenter: ImagesListPresenterProtocol { + + weak var view: ImagesListViewControllerProtocol? + + private let imagesListService = ImagesListService.shared + private var imagesListServiceObserver: NSObjectProtocol? + + var photos: [Photo] = [] + + func viewDidLoad() { + imagesListService.fetchPhotosNextPage() + setupNotifications() + } + + func updateTableViewAnimated() { + let oldCount = photos.count + let newCount = imagesListService.photos.count + photos = imagesListService.photos + if oldCount != newCount { + let indexPaths = (oldCount.. Int { - return photos.count + return presenter?.photos.count ?? 0 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: ImagesListCell.reuseIdentifier, for: indexPath) as? ImagesListCell else { return UITableViewCell() } cell.delegate = self - let photo = photos[indexPath.row] + guard let photo = presenter?.photos[indexPath.row] else { return UITableViewCell() } cell.setCell(photo: photo) return cell } @@ -87,19 +82,17 @@ extension ImagesListViewController: UITableViewDelegate { let vc = SingleImageViewController() vc.modalPresentationStyle = .fullScreen - let imageURL = photos[indexPath.row].largeImageURL + guard let imageURL = presenter?.photos[indexPath.row].largeImageURL else { return } vc.image = URL(string: imageURL) present(vc, animated: true) } func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if indexPath.row == photos.count - 1 { - imagesListService.fetchPhotosNextPage() - } + presenter?.willDisplay(for: indexPath) } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let photo = photos[indexPath.row].size + guard let photo = presenter?.photos[indexPath.row].size else { return CGFloat() } let imageInsets = UIEdgeInsets(top: 4, left: 16, bottom: 4, right: 16) let imageViewWidth = tableView.bounds.width - imageInsets.left - imageInsets.right @@ -113,20 +106,6 @@ extension ImagesListViewController: UITableViewDelegate { extension ImagesListViewController: ImagesListCellDelegate { func imageListCellDidTapLike(_ cell: ImagesListCell) { guard let indexPath = tableView.indexPath(for: cell) else { return } - let photo = photos[indexPath.row] - - UIBlockingProgressHUD.show() - imagesListService.changeLike(photoId: photo.id, isLike: !photo.isLiked) { [weak self] result in - guard let self else { return } - switch result { - case .success: - self.photos = self.imagesListService.photos - cell.setIsLiked(isLiked: self.photos[indexPath.row].isLiked) - UIBlockingProgressHUD.dismiss() - case .failure: - print("Function: \(#function), line \(#line) Failed to change Like") - UIBlockingProgressHUD.dismiss() - } - } + presenter?.imageListCellDidTapLike(cell, indexPath: indexPath) } } diff --git a/ImageFeed/Profile/ProfileViewController.swift b/ImageFeed/Profile/ProfileViewController.swift index c3cf4c0..aa4c3ca 100644 --- a/ImageFeed/Profile/ProfileViewController.swift +++ b/ImageFeed/Profile/ProfileViewController.swift @@ -6,11 +6,18 @@ // import UIKit -import Kingfisher -final class ProfileViewController: UIViewController { +protocol ProfileViewControllerProtocol: AnyObject { + var presenter: ProfileViewPresenterProtocol? { get set } + var profileImage: UIImageView { get } + var nameLabel: UILabel { get } + var bioLabel: UILabel { get } + var loginLabel: UILabel { get } +} + +final class ProfileViewController: UIViewController, ProfileViewControllerProtocol { - private let profileImage: UIImageView = { + let profileImage: UIImageView = { let image = UIImageView() image.contentMode = .scaleAspectFit image.layer.cornerRadius = 35 @@ -20,7 +27,7 @@ final class ProfileViewController: UIViewController { return image }() - private let nameLabel: UILabel = { + let nameLabel: UILabel = { let label = UILabel() label.text = "Екатерина Новикова" label.font = UIFont.systemFont(ofSize: 23, weight: .semibold) @@ -29,7 +36,7 @@ final class ProfileViewController: UIViewController { return label }() - private let loginLabel: UILabel = { + let loginLabel: UILabel = { let label = UILabel() label.text = "@ekaterina_nov" label.font = UIFont.systemFont(ofSize: 13) @@ -38,7 +45,7 @@ final class ProfileViewController: UIViewController { return label }() - private let bioLabel: UILabel = { + let bioLabel: UILabel = { let label = UILabel() label.text = "Hello, world!" label.numberOfLines = 0 @@ -50,51 +57,25 @@ final class ProfileViewController: UIViewController { private let logoffButton: UIButton = { let button = UIButton() + button.accessibilityIdentifier = "logoffButton" button.setImage(UIImage(named: "logoffButton"), for: .normal) button.translatesAutoresizingMaskIntoConstraints = false return button }() - private let profileService = ProfileService.shared - private var profileImageServiceObserver: NSObjectProtocol? + var presenter: ProfileViewPresenterProtocol? override func viewDidLoad() { super.viewDidLoad() configureUI() - updateProfileDetails(profile: profileService.profile) - - profileImageServiceObserver = NotificationCenter.default - .addObserver( - forName: ProfileImageService.didChangeNotification, - object: nil, - queue: .main - ) { [weak self] _ in - guard let self = self else { return } - self.updateAvatar() - } - updateAvatar() - } - - private func updateAvatar() { - guard let profileImageURL = ProfileImageService.shared.avatarURL, - let url = URL(string: profileImageURL) else { return } - let processor = RoundCornerImageProcessor(cornerRadius: 35) - profileImage.kf.setImage(with: url, - placeholder: UIImage(named: "placeholder.jpeg"), - options: [.processor(processor)]) - } - - private func updateProfileDetails(profile: Profile?) { - guard let profile else { return } - nameLabel.text = profile.name - bioLabel.text = profile.bio - loginLabel.text = profile.loginName + presenter?.updateProfileDetails() + presenter?.observe(placeholder: UIImage(named: "placeholder.jpeg") ?? UIImage()) } @objc private func didTapLogoffButton() { let alertController = UIAlertController(title: "Пока, пока!", message: "Уверены что хотите выйти?", preferredStyle: .alert) let alertYes = UIAlertAction(title: "Да", style: .default) { [weak self] _ in - ProfileLogoutService.shared.logout() + self?.presenter?.didTapLogoffButton() let vc = SplashViewController() self?.navigationController?.pushViewController(vc, animated: true) } @@ -107,6 +88,8 @@ final class ProfileViewController: UIViewController { } private func configureUI() { + presenter?.view = self + view.backgroundColor = .ypBlack view.addSubviews(profileImage, nameLabel, loginLabel, bioLabel, logoffButton) diff --git a/ImageFeed/Profile/ProfileViewPresenter.swift b/ImageFeed/Profile/ProfileViewPresenter.swift new file mode 100644 index 0000000..c087476 --- /dev/null +++ b/ImageFeed/Profile/ProfileViewPresenter.swift @@ -0,0 +1,57 @@ +// +// ProfileViewPresenter.swift +// ImageFeed +// +// Created by Юрий Гриневич on 10.08.2024. +// + +import Foundation +import Kingfisher + +protocol ProfileViewPresenterProtocol { + var view: ProfileViewControllerProtocol? { get set } + func didTapLogoffButton() + func updateProfileDetails() + func observe(placeholder: Placeholder) +} + +final class ProfileViewPresenter: ProfileViewPresenterProtocol { + + weak var view: ProfileViewControllerProtocol? + private let logoutService = ProfileLogoutService.shared + private let profileService = ProfileService.shared + private var profileImageServiceObserver: NSObjectProtocol? + + func didTapLogoffButton() { + logoutService.logout() + } + + private func updateAvatar(placeholder: Placeholder) { + guard let profileImageURL = ProfileImageService.shared.avatarURL, + let url = URL(string: profileImageURL) else { return } + let processor = RoundCornerImageProcessor(cornerRadius: 35) + view?.profileImage.kf.setImage(with: url, + placeholder: placeholder, + options: [.processor(processor)]) + } + + func updateProfileDetails() { + guard let profile = profileService.profile else { return } + view?.nameLabel.text = profile.name + view?.bioLabel.text = profile.bio + view?.loginLabel.text = profile.loginName + } + + func observe(placeholder: Placeholder) { + profileImageServiceObserver = NotificationCenter.default + .addObserver( + forName: ProfileImageService.didChangeNotification, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self = self else { return } + self.updateAvatar(placeholder: placeholder) + } + updateAvatar(placeholder: placeholder) + } +} diff --git a/ImageFeed/Services/ImagesListService.swift b/ImageFeed/Services/ImagesListService.swift index 75215d8..e22c00c 100644 --- a/ImageFeed/Services/ImagesListService.swift +++ b/ImageFeed/Services/ImagesListService.swift @@ -7,7 +7,7 @@ import Foundation -struct Photo { +public struct Photo { let id: String let size: CGSize let createdAt: Date? @@ -119,7 +119,7 @@ final class ImagesListService { guard let self else { return } DispatchQueue.main.async { switch result { - case .success(let photoLike): + case .success(_): if let index = self.photos.firstIndex(where: { $0.id == photoId }) { self.photos[index].isLiked.toggle() completion(.success(())) diff --git a/ImageFeed/SingleImageView/SingleImageViewController.swift b/ImageFeed/SingleImageView/SingleImageViewController.swift index f3a5863..368d1d5 100644 --- a/ImageFeed/SingleImageView/SingleImageViewController.swift +++ b/ImageFeed/SingleImageView/SingleImageViewController.swift @@ -20,6 +20,7 @@ final class SingleImageViewController: UIViewController { private let backButton: UIButton = { let button = UIButton() + button.accessibilityIdentifier = "backButton" button.setImage(UIImage(named: "chevron.backward"), for: .normal) button.tintColor = .ypWhite button.translatesAutoresizingMaskIntoConstraints = false diff --git a/ImageFeed/TabBarController/TabBarViewController.swift b/ImageFeed/TabBarController/TabBarViewController.swift index b437466..65ce891 100644 --- a/ImageFeed/TabBarController/TabBarViewController.swift +++ b/ImageFeed/TabBarController/TabBarViewController.swift @@ -20,7 +20,11 @@ final class TabBarViewController: UITabBarController { tabBar.tintColor = .ypWhite let imagesListVC = ImagesListViewController() + let imagesListPresenter = ImagesListPresenter() + imagesListVC.presenter = imagesListPresenter let profileVC = ProfileViewController() + let profileViewPresenter = ProfileViewPresenter() + profileVC.presenter = profileViewPresenter let imagesListNVC = UINavigationController(rootViewController: imagesListVC) diff --git a/ImageFeedTests/ImagesListTests.swift b/ImageFeedTests/ImagesListTests.swift new file mode 100644 index 0000000..0527f83 --- /dev/null +++ b/ImageFeedTests/ImagesListTests.swift @@ -0,0 +1,47 @@ +// +// ImagesListTests.swift +// ImageFeedTests +// +// Created by Юрий Гриневич on 14.08.2024. +// + +@testable import ImageFeed +import XCTest + +final class ImagesListTests: XCTestCase { + + func testViewControllerCallsViewDidLoad() { + + let imagesListVC = ImagesListViewController() + let presenter = ImagesListPresenterSpy() + imagesListVC.presenter = presenter + presenter.view = imagesListVC + + _ = imagesListVC.view + + XCTAssertTrue(presenter.viewDidLoadCalled) + } +} + +final class ImagesListPresenterSpy: ImagesListPresenterProtocol { + var viewDidLoadCalled: Bool = false + var view: ImagesListViewControllerProtocol? + + var photos: [Photo] = [] + + func viewDidLoad() { + viewDidLoadCalled = true + } + + func willDisplay(for indexPath: IndexPath) { + + } + + func imageListCellDidTapLike(_ cell: ImageFeed.ImagesListCell, indexPath: IndexPath) { + + } + + func updateTableViewAnimated() { + + } +} diff --git a/ImageFeedTests/ProfileViewTests.swift b/ImageFeedTests/ProfileViewTests.swift new file mode 100644 index 0000000..b0a41e8 --- /dev/null +++ b/ImageFeedTests/ProfileViewTests.swift @@ -0,0 +1,44 @@ +// +// ProfileViewTests.swift +// ImageFeedTests +// +// Created by Юрий Гриневич on 14.08.2024. +// + +@testable import ImageFeed +import XCTest +import Kingfisher + +final class ProfileTests: XCTestCase { + + func testViewControllerCallsUpdateProfileDetailsCalled() { + let profileViewVC = ProfileViewController() + let presenter = ProfileViewPresenterSpy() + profileViewVC.presenter = presenter + presenter.view = profileViewVC + + _ = profileViewVC.view + + XCTAssertTrue(presenter.updateProfileDetailsCalled) + XCTAssertTrue(presenter.observeCalled) + } +} + +final class ProfileViewPresenterSpy: ProfileViewPresenterProtocol { + + var updateProfileDetailsCalled: Bool = false + var observeCalled: Bool = false + var view: ProfileViewControllerProtocol? + + func didTapLogoffButton() { + + } + + func updateProfileDetails() { + updateProfileDetailsCalled = true + } + + func observe(placeholder: any Kingfisher.Placeholder) { + observeCalled = true + } +} diff --git a/ImageFeedTests/WebViewTests.swift b/ImageFeedTests/WebViewTests.swift new file mode 100644 index 0000000..38f4928 --- /dev/null +++ b/ImageFeedTests/WebViewTests.swift @@ -0,0 +1,115 @@ +// +// WebViewTests.swift +// WebViewTests +// +// Created by Юрий Гриневич on 10.08.2024. +// + +@testable import ImageFeed +import XCTest + +final class WebViewTests: XCTestCase { + + func testViewControllerCallsViewDidLoad() { + let webViewVC = WebViewViewController() + let presenter = WebViewPresenterSpy() + webViewVC.presenter = presenter + presenter.view = webViewVC + + _ = webViewVC.view + + XCTAssertTrue(presenter.viewDidLoadCalled) + } + + func testPresenterCallsLoadRequest() { + let webViewVC = WebViewViewControllerSpy() + let authHelper = AuthHelper() + let presenter = WebViewPresenter(authHelper: authHelper) + webViewVC.presenter = presenter + presenter.view = webViewVC + + presenter.viewDidLoad() + + XCTAssertTrue(webViewVC.loadRequestCalled) + } + + func testProgressVisibleWhenLessThenOne() { + let authHelper = AuthHelper() + let presenter = WebViewPresenter(authHelper: authHelper) + let progress: Float = 0.6 + + let shouldHideProgress = presenter.shouldHideProgress(for: progress) + + XCTAssertFalse(shouldHideProgress) + } + + func testProgressHiddenWhenOne() { + let authHelper = AuthHelper() + let presenter = WebViewPresenter(authHelper: authHelper) + let progress: Float = 1 + + let shouldHideProgress = presenter.shouldHideProgress(for: progress) + + XCTAssertTrue(shouldHideProgress) + } + + func testAuthHelperAuthURL() { + let configuration = AuthConfiguration.standard + let authHelper = AuthHelper(configuration: configuration) + + guard let url = authHelper.authURL() else { return } + let urlString = url.absoluteString + + XCTAssertTrue(urlString.contains(configuration.authURLString)) + XCTAssertTrue(urlString.contains(configuration.accessKey)) + XCTAssertTrue(urlString.contains(configuration.redirectURI)) + XCTAssertTrue(urlString.contains("code")) + XCTAssertTrue(urlString.contains(configuration.accessScope)) + } + + func testCodeFromURL() { + let authHelper = AuthHelper() + + var urlComponents = URLComponents(string: "https://unsplash.com/oauth/authorize/native") + urlComponents?.queryItems = [URLQueryItem(name: "code", value: "test code")] + guard let url = urlComponents?.url else { return } + let code = authHelper.code(from: url) + + XCTAssertEqual(code, "test code") + } +} + +final class WebViewPresenterSpy: WebViewPresenterProtocol { + var viewDidLoadCalled: Bool = false + var view: WebViewViewControllerProtocol? + + func viewDidLoad() { + viewDidLoadCalled = true + } + + func didUpdateProgressValue(_ newValue: Double) { + + } + + func code(from url: URL) -> String? { + return nil + } +} + +final class WebViewViewControllerSpy: WebViewViewControllerProtocol { + var presenter: ImageFeed.WebViewPresenterProtocol? + + var loadRequestCalled: Bool = false + + func load(request: URLRequest) { + loadRequestCalled = true + } + + func setProgressValue(_ newValue: Float) { + + } + + func setProgressHidden(_ isHidden: Bool) { + + } +} diff --git a/ImageFeedUITests/ImageFeedUITests.swift b/ImageFeedUITests/ImageFeedUITests.swift new file mode 100644 index 0000000..b88ae55 --- /dev/null +++ b/ImageFeedUITests/ImageFeedUITests.swift @@ -0,0 +1,94 @@ +// +// ImageFeedUITests.swift +// ImageFeedUITests +// +// Created by Юрий Гриневич on 15.08.2024. +// + +import XCTest + +final class ImageFeedUITests: XCTestCase { + private let app = XCUIApplication() + + override func setUpWithError() throws { + continueAfterFailure = false + + app.launch() + } + + func testAuth() throws { + app.buttons["Войти"].tap() + + let webView = app.webViews["UnsplashWebView"] + + XCTAssertTrue(webView.waitForExistence(timeout: 5)) + + let loginTextField = webView.descendants(matching: .textField).element + XCTAssertTrue(loginTextField.waitForExistence(timeout: 5)) + + + webView/*@START_MENU_TOKEN@*/.textFields["Email address"]/*[[".otherElements[\"Connect ImageFeed + Unsplash | Unsplash\"].textFields[\"Email address\"]",".textFields[\"Email address\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() + loginTextField.typeText("ENTER YOUR EMAIL HERE...") + webView.swipeUp() + sleep(3) + + let passwordTextField = webView.descendants(matching: .secureTextField).element + XCTAssertTrue(passwordTextField.waitForExistence(timeout: 5)) + + + webView/*@START_MENU_TOKEN@*/.secureTextFields["Password"]/*[[".otherElements[\"Connect ImageFeed + Unsplash | Unsplash\"].secureTextFields[\"Password\"]",".secureTextFields[\"Password\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() + passwordTextField.typeText("ENTER YOUR PASSWORD HERE...") + webView.swipeUp() + + sleep(3) + + webView.buttons["Login"].tap() + + let tablesQuery = app.tables + let cell = tablesQuery.children(matching: .cell).element(boundBy: 0) + + XCTAssertTrue(cell.waitForExistence(timeout: 5)) + } + + func testFeed() throws { + let tablesQuery = app.tables + + let cell = tablesQuery.children(matching: .cell).element(boundBy: 0) + cell.swipeUp() + + sleep(2) + + let cellToLike = tablesQuery.children(matching: .cell).element(boundBy: 1) + + cellToLike.buttons["LikeButton"].tap() + sleep(2) + cellToLike.buttons["LikeButton"].tap() + + sleep(2) + + cellToLike.tap() + + sleep(3) + + let image = app.scrollViews.images.element(boundBy: 0) + // Zoom in + image.pinch(withScale: 3, velocity: 1) // zoom in + // Zoom out + image.pinch(withScale: 0.5, velocity: -1) + + let navBackButtonWhiteButton = app.buttons["backButton"] + navBackButtonWhiteButton.tap() + } + + func testProfile() throws { + sleep(3) + app.tabBars.buttons.element(boundBy: 1).tap() + + XCTAssertTrue(app.staticTexts["siphons rollmop"].exists) + XCTAssertTrue(app.staticTexts["@siphons0m"].exists) + + app.buttons["logoffButton"].tap() + + app.alerts["Пока, пока!"].scrollViews.otherElements.buttons["Да"].tap() + } +} diff --git a/ImageFeedUITests/ImageFeedUITestsLaunchTests.swift b/ImageFeedUITests/ImageFeedUITestsLaunchTests.swift new file mode 100644 index 0000000..324dcbd --- /dev/null +++ b/ImageFeedUITests/ImageFeedUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// ImageFeedUITestsLaunchTests.swift +// ImageFeedUITests +// +// Created by Юрий Гриневич on 15.08.2024. +// + +import XCTest + +final class ImageFeedUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +}