diff --git a/.github/workflows/CI-iOS.yml b/.github/workflows/CI-iOS.yml index 87f2e93..e8429ee 100644 --- a/.github/workflows/CI-iOS.yml +++ b/.github/workflows/CI-iOS.yml @@ -21,11 +21,11 @@ jobs: - uses: actions/checkout@v4 - name: Select Xcode - run: sudo xcode-select -switch /Applications/Xcode_16.2.app + run: sudo xcode-select -switch /Applications/Xcode_16.0.app - name: Xcode version run: /usr/bin/xcodebuild -version - name: Build and Test - run: xcodebuild clean build test -workspace EssentialApp/EssentialApp.xcworkspace -scheme "CI_iOS" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2" ONLY_ACTIVE_ARCH=YES + run: xcodebuild clean build test -workspace EssentialApp/EssentialApp.xcworkspace -scheme "CI_iOS" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 16 Pro,OS=18.0" ONLY_ACTIVE_ARCH=YES \ No newline at end of file diff --git a/.github/workflows/CI-macOS.yml b/.github/workflows/CI-macOS.yml index da01ade..373d8ae 100644 --- a/.github/workflows/CI-macOS.yml +++ b/.github/workflows/CI-macOS.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 - name: Select Xcode - run: sudo xcode-select -switch /Applications/Xcode_16.2.app + run: sudo xcode-select -switch /Applications/Xcode_16.0.app - name: Xcode version run: /usr/bin/xcodebuild -version diff --git a/EssentialApp/CI_iOS.xctestplan b/EssentialApp/CI_iOS.xctestplan index a8e2931..b171d7e 100644 --- a/EssentialApp/CI_iOS.xctestplan +++ b/EssentialApp/CI_iOS.xctestplan @@ -2,7 +2,7 @@ "configurations" : [ { "id" : "91820604-8FC3-4344-8FDE-8E997DE399E6", - "name" : "Configuration 1", + "name" : "Test Scheme Action", "options" : { } @@ -32,39 +32,51 @@ }, "testTargets" : [ { + "parallelizable" : false, "target" : { "containerPath" : "container:..\/EssentialFeed\/EssentialFeed.xcodeproj", - "identifier" : "5B4BAE7C2CFBA0EE00CE079A", - "name" : "EssentialFeediOSTests" + "identifier" : "5B107E012BF5BB2100927709", + "name" : "EssentialFeedTests" } }, { + "parallelizable" : false, "target" : { "containerPath" : "container:..\/EssentialFeed\/EssentialFeed.xcodeproj", - "identifier" : "5BA598AF2CE18F9F007B1795", - "name" : "EssentialFeedCacheIntegrationTests" + "identifier" : "5B1C4FC52C057236003F0429", + "name" : "EssentialFeedAPIEndToEndTests" } }, { + "parallelizable" : false, "target" : { "containerPath" : "container:..\/EssentialFeed\/EssentialFeed.xcodeproj", - "identifier" : "5B1C4FC52C057236003F0429", - "name" : "EssentialFeedAPIEndToEndTests" + "identifier" : "5BA598AF2CE18F9F007B1795", + "name" : "EssentialFeedCacheIntegrationTests" } }, { + "parallelizable" : false, "target" : { "containerPath" : "container:..\/EssentialFeed\/EssentialFeed.xcodeproj", - "identifier" : "5B107E012BF5BB2100927709", - "name" : "EssentialFeedTests" + "identifier" : "5B4BAE7C2CFBA0EE00CE079A", + "name" : "EssentialFeediOSTests" } }, { + "parallelizable" : false, "target" : { "containerPath" : "container:EssentialApp.xcodeproj", "identifier" : "5BDE3C862D6D12BA005D520D", "name" : "EssentialAppTests" } + }, + { + "target" : { + "containerPath" : "container:EssentialApp.xcodeproj", + "identifier" : "5BBDA0822D72983C00D68DF0", + "name" : "EssentialAppUIAcceptanceTests" + } } ], "version" : 1 diff --git a/EssentialApp/EssentialApp.xcodeproj/project.pbxproj b/EssentialApp/EssentialApp.xcodeproj/project.pbxproj index 6b093cb..df4150f 100644 --- a/EssentialApp/EssentialApp.xcodeproj/project.pbxproj +++ b/EssentialApp/EssentialApp.xcodeproj/project.pbxproj @@ -15,6 +15,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 5BBDA0892D72983C00D68DF0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5BDE3C692D6D12B8005D520D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5BDE3C702D6D12B8005D520D; + remoteInfo = EssentialApp; + }; 5BDE3C882D6D12BA005D520D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 5BDE3C692D6D12B8005D520D /* Project object */; @@ -41,6 +48,7 @@ /* Begin PBXFileReference section */ 5BBD9FDF2D6EA59800D68DF0 /* CI_iOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CI_iOS.xctestplan; sourceTree = ""; }; + 5BBDA0832D72983C00D68DF0 /* EssentialAppUIAcceptanceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EssentialAppUIAcceptanceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5BDE3C712D6D12B8005D520D /* EssentialApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EssentialApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5BDE3C872D6D12BA005D520D /* EssentialAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EssentialAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5BDE3CDF2D6D186D005D520D /* EssentialFeed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = EssentialFeed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -59,6 +67,11 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + 5BBDA0842D72983C00D68DF0 /* EssentialAppUIAcceptanceTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = EssentialAppUIAcceptanceTests; + sourceTree = ""; + }; 5BDE3C732D6D12B8005D520D /* EssentialApp */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -75,6 +88,13 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + 5BBDA0802D72983C00D68DF0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5BDE3C6E2D6D12B8005D520D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -101,6 +121,7 @@ 5BDE3D0C2D6D6092005D520D /* EssentialApp.xctestplan */, 5BDE3C732D6D12B8005D520D /* EssentialApp */, 5BDE3C8A2D6D12BA005D520D /* EssentialAppTests */, + 5BBDA0842D72983C00D68DF0 /* EssentialAppUIAcceptanceTests */, 5BDE3CDE2D6D186D005D520D /* Frameworks */, 5BDE3C722D6D12B8005D520D /* Products */, ); @@ -111,6 +132,7 @@ children = ( 5BDE3C712D6D12B8005D520D /* EssentialApp.app */, 5BDE3C872D6D12BA005D520D /* EssentialAppTests.xctest */, + 5BBDA0832D72983C00D68DF0 /* EssentialAppUIAcceptanceTests.xctest */, ); name = Products; sourceTree = ""; @@ -127,6 +149,29 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 5BBDA0822D72983C00D68DF0 /* EssentialAppUIAcceptanceTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5BBDA08B2D72983C00D68DF0 /* Build configuration list for PBXNativeTarget "EssentialAppUIAcceptanceTests" */; + buildPhases = ( + 5BBDA07F2D72983C00D68DF0 /* Sources */, + 5BBDA0802D72983C00D68DF0 /* Frameworks */, + 5BBDA0812D72983C00D68DF0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5BBDA08A2D72983C00D68DF0 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 5BBDA0842D72983C00D68DF0 /* EssentialAppUIAcceptanceTests */, + ); + name = EssentialAppUIAcceptanceTests; + packageProductDependencies = ( + ); + productName = EssentialAppUIAcceptanceTests; + productReference = 5BBDA0832D72983C00D68DF0 /* EssentialAppUIAcceptanceTests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; 5BDE3C702D6D12B8005D520D /* EssentialApp */ = { isa = PBXNativeTarget; buildConfigurationList = 5BDE3C9A2D6D12BA005D520D /* Build configuration list for PBXNativeTarget "EssentialApp" */; @@ -183,6 +228,10 @@ LastSwiftUpdateCheck = 1620; LastUpgradeCheck = 1620; TargetAttributes = { + 5BBDA0822D72983C00D68DF0 = { + CreatedOnToolsVersion = 16.2; + TestTargetID = 5BDE3C702D6D12B8005D520D; + }; 5BDE3C702D6D12B8005D520D = { CreatedOnToolsVersion = 16.2; }; @@ -209,11 +258,19 @@ targets = ( 5BDE3C702D6D12B8005D520D /* EssentialApp */, 5BDE3C862D6D12BA005D520D /* EssentialAppTests */, + 5BBDA0822D72983C00D68DF0 /* EssentialAppUIAcceptanceTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 5BBDA0812D72983C00D68DF0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5BDE3C6F2D6D12B8005D520D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -232,6 +289,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 5BBDA07F2D72983C00D68DF0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5BDE3C6D2D6D12B8005D520D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -249,6 +313,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 5BBDA08A2D72983C00D68DF0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5BDE3C702D6D12B8005D520D /* EssentialApp */; + targetProxy = 5BBDA0892D72983C00D68DF0 /* PBXContainerItemProxy */; + }; 5BDE3C892D6D12BA005D520D /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 5BDE3C702D6D12B8005D520D /* EssentialApp */; @@ -257,6 +326,42 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 5BBDA08C2D72983C00D68DF0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4VK5P5723H; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = portocode.EssentialAppUIAcceptanceTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = EssentialApp; + }; + name = Debug; + }; + 5BBDA08D2D72983C00D68DF0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4VK5P5723H; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = portocode.EssentialAppUIAcceptanceTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = EssentialApp; + }; + name = Release; + }; 5BDE3C9B2D6D12BA005D520D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -272,6 +377,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -300,6 +406,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -441,7 +548,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4VK5P5723H; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = portocode.EssentialAppTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -462,7 +569,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4VK5P5723H; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = portocode.EssentialAppTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -476,6 +583,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 5BBDA08B2D72983C00D68DF0 /* Build configuration list for PBXNativeTarget "EssentialAppUIAcceptanceTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5BBDA08C2D72983C00D68DF0 /* Debug */, + 5BBDA08D2D72983C00D68DF0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 5BDE3C6C2D6D12B8005D520D /* Build configuration list for PBXProject "EssentialApp" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/EssentialApp/EssentialApp.xcworkspace/contents.xcworkspacedata b/EssentialApp/EssentialApp.xcworkspace/contents.xcworkspacedata index 75c7596..3a858a6 100644 --- a/EssentialApp/EssentialApp.xcworkspace/contents.xcworkspacedata +++ b/EssentialApp/EssentialApp.xcworkspace/contents.xcworkspacedata @@ -7,4 +7,7 @@ + + diff --git a/EssentialApp/EssentialApp.xcworkspace/xcshareddata/xcschemes/EssentialApp.xcscheme b/EssentialApp/EssentialApp.xcworkspace/xcshareddata/xcschemes/EssentialApp.xcscheme index a5c0df5..71a9fb6 100644 --- a/EssentialApp/EssentialApp.xcworkspace/xcshareddata/xcschemes/EssentialApp.xcscheme +++ b/EssentialApp/EssentialApp.xcworkspace/xcshareddata/xcschemes/EssentialApp.xcscheme @@ -46,6 +46,17 @@ ReferencedContainer = "container:EssentialApp.xcodeproj"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EssentialApp/EssentialApp/AppDelegate.swift b/EssentialApp/EssentialApp/AppDelegate.swift index 80a6f80..cbb2960 100644 --- a/EssentialApp/EssentialApp/AppDelegate.swift +++ b/EssentialApp/EssentialApp/AppDelegate.swift @@ -20,7 +20,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + + #if DEBUG + configuration.delegateClass = DebuggingSceneDelegate.self + #endif + + return configuration } func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { diff --git a/EssentialApp/EssentialApp/DebuggingSceneDelegate.swift b/EssentialApp/EssentialApp/DebuggingSceneDelegate.swift new file mode 100644 index 0000000..cee4daf --- /dev/null +++ b/EssentialApp/EssentialApp/DebuggingSceneDelegate.swift @@ -0,0 +1,83 @@ +// +// Created by Rodrigo Porto. +// Copyright © 2025 PortoCode. All Rights Reserved. +// + +#if DEBUG +import UIKit +import EssentialFeed + +class DebuggingSceneDelegate: SceneDelegate { + override func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let _ = (scene as? UIWindowScene) else { return } + + if CommandLine.arguments.contains("-reset") { + try? FileManager.default.removeItem(at: localStoreURL) + } + + super.scene(scene, willConnectTo: session, options: connectionOptions) + } + + override func makeRemoteClient() -> HTTPClient { + if let connectivity = UserDefaults.standard.string(forKey: "connectivity") { + return DebuggingHTTPClient(connectivity: connectivity) + } + return super.makeRemoteClient() + } +} + +private class DebuggingHTTPClient: HTTPClient { + private class Task: HTTPClientTask { + func cancel() {} + } + + private let connectivity: String + + init(connectivity: String) { + self.connectivity = connectivity + } + + func get(from url: URL, completion: @escaping (HTTPClient.Result) -> Void) -> HTTPClientTask { + switch connectivity { + case "online": + completion(.success(makeSuccessfulResponse(for: url))) + default: + completion(.failure(NSError(domain: "offline", code: 0))) + } + return Task() + } + + private func makeSuccessfulResponse(for url: URL) -> (Data, HTTPURLResponse) { + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! + return (makeData(for: url), response) + } + + private func makeData(for url: URL) -> Data { + switch url.absoluteString { + case "http://image.com": + return makeImageData() + + default: + return makeFeedData() + } + } + + private func makeImageData() -> Data { + let rect = CGRect(x: 0, y: 0, width: 1, height: 1) + UIGraphicsBeginImageContext(rect.size) + let context = UIGraphicsGetCurrentContext()! + context.setFillColor(UIColor.red.cgColor) + context.fill(rect) + let img = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return img!.pngData()! + } + + private func makeFeedData() -> Data { + return try! JSONSerialization.data(withJSONObject: ["items": [ + ["id": UUID().uuidString, "image": "http://image.com"], + ["id": UUID().uuidString, "image": "http://image.com"] + ]]) + } +} +#endif diff --git a/EssentialApp/EssentialApp/SceneDelegate.swift b/EssentialApp/EssentialApp/SceneDelegate.swift index e52f7d0..097e004 100644 --- a/EssentialApp/EssentialApp/SceneDelegate.swift +++ b/EssentialApp/EssentialApp/SceneDelegate.swift @@ -11,19 +11,19 @@ import EssentialFeediOS class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? + let localStoreURL = NSPersistentContainer + .defaultDirectoryURL() + .appendingPathComponent("feed-store.sqlite") + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let _ = (scene as? UIWindowScene) else { return } let remoteURL = URL(string: "https://ile-api.essentialdeveloper.com/essential-feed/v1/feed")! - let remoteClient = URLSessionHTTPClient(session: URLSession(configuration: .ephemeral)) + let remoteClient = makeRemoteClient() let remoteFeedLoader = RemoteFeedLoader(url: remoteURL, client: remoteClient) let remoteImageLoader = RemoteFeedImageDataLoader(client: remoteClient) - let localStoreURL = NSPersistentContainer - .defaultDirectoryURL() - .appendingPathComponent("feed-store.sqlite") - let localStore = try! CoreDataFeedStore(storeURL: localStoreURL) let localFeedLoader = LocalFeedLoader(store: localStore, currentDate: Date.init) let localImageLoader = LocalFeedImageDataLoader(store: localStore) @@ -40,5 +40,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { decoratee: remoteImageLoader, cache: localImageLoader))) } + + func makeRemoteClient() -> HTTPClient { + return URLSessionHTTPClient(session: URLSession(configuration: .ephemeral)) + } } - diff --git a/EssentialApp/EssentialApp/ViewController.swift b/EssentialApp/EssentialApp/ViewController.swift deleted file mode 100644 index 1d747ee..0000000 --- a/EssentialApp/EssentialApp/ViewController.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// Created by Rodrigo Porto. -// Copyright © 2025 PortoCode. All Rights Reserved. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - -} - diff --git a/EssentialApp/EssentialAppTests/EssentialApp.xctestplan b/EssentialApp/EssentialAppTests/EssentialApp.xctestplan index b1d7ac6..511dade 100644 --- a/EssentialApp/EssentialAppTests/EssentialApp.xctestplan +++ b/EssentialApp/EssentialAppTests/EssentialApp.xctestplan @@ -2,7 +2,7 @@ "configurations" : [ { "id" : "1C72DE09-1522-41C2-A2CD-1727C894E236", - "name" : "Configuration 1", + "name" : "Test Scheme Action", "options" : { } diff --git a/EssentialApp/EssentialAppUIAcceptanceTests/EssentialAppUIAcceptanceTests.swift b/EssentialApp/EssentialAppUIAcceptanceTests/EssentialAppUIAcceptanceTests.swift new file mode 100644 index 0000000..658afb0 --- /dev/null +++ b/EssentialApp/EssentialAppUIAcceptanceTests/EssentialAppUIAcceptanceTests.swift @@ -0,0 +1,47 @@ +// +// Created by Rodrigo Porto. +// Copyright © 2025 PortoCode. All Rights Reserved. +// + +import XCTest + +final class EssentialAppUIAcceptanceTests: XCTestCase { + + func test_onLaunch_displaysRemoteFeedWhenCustomerHasConnectivity() { + let app = XCUIApplication() + app.launchArguments = ["-reset", "-connectivity", "online"] + app.launch() + + let feedCells = app.cells.matching(identifier: "feed-image-cell") + XCTAssertEqual(feedCells.count, 2) + + let firstImage = app.images.matching(identifier: "feed-image-view").firstMatch + XCTAssertTrue(firstImage.exists) + } + + func test_onLaunch_displaysCachedRemoteFeedWhenCustomerHasNoConnectivity() { + let onlineApp = XCUIApplication() + onlineApp.launchArguments = ["-reset", "-connectivity", "online"] + onlineApp.launch() + + let offlineApp = XCUIApplication() + offlineApp.launchArguments = ["-connectivity", "offline"] + offlineApp.launch() + + let cachedFeedCells = offlineApp.cells.matching(identifier: "feed-image-cell") + XCTAssertEqual(cachedFeedCells.count, 2) + + let firstCachedImage = offlineApp.images.matching(identifier: "feed-image-view").firstMatch + XCTAssertTrue(firstCachedImage.exists) + } + + func test_onLaunch_displaysEmptyFeedWhenCustomerHasNoConnectivityAndNoCache() { + let app = XCUIApplication() + app.launchArguments = ["-reset", "-connectivity", "offline"] + app.launch() + + let feedCells = app.cells.matching(identifier: "feed-image-cell") + XCTAssertEqual(feedCells.count, 0) + } + +} diff --git a/EssentialApp/EssentialAppUIAcceptanceTests/EssentialAppUIAcceptanceTests.xctestplan b/EssentialApp/EssentialAppUIAcceptanceTests/EssentialAppUIAcceptanceTests.xctestplan new file mode 100644 index 0000000..794f4e9 --- /dev/null +++ b/EssentialApp/EssentialAppUIAcceptanceTests/EssentialAppUIAcceptanceTests.xctestplan @@ -0,0 +1,33 @@ +{ + "configurations" : [ + { + "id" : "BEB595B4-9364-4FB0-9A36-B6EAFD0D066A", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:EssentialApp.xcodeproj", + "identifier" : "5BDE3C702D6D12B8005D520D", + "name" : "EssentialApp" + } + ] + }, + "testExecutionOrdering" : "random" + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:EssentialApp.xcodeproj", + "identifier" : "5BBDA0822D72983C00D68DF0", + "name" : "EssentialAppUIAcceptanceTests" + } + } + ], + "version" : 1 +} diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 349cbd9..dcfbc84 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -1210,6 +1210,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -1246,6 +1247,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -1273,6 +1275,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 4VK5P5723H; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.portocode.EssentialFeedTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1292,6 +1295,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 4VK5P5723H; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.portocode.EssentialFeedTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1309,6 +1313,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 4VK5P5723H; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MACOSX_DEPLOYMENT_TARGET = 14.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.portocode.EssentialFeedAPIEndToEndTests; @@ -1327,6 +1332,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 4VK5P5723H; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MACOSX_DEPLOYMENT_TARGET = 14.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.portocode.EssentialFeedAPIEndToEndTests; @@ -1354,7 +1360,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 18.1; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1392,7 +1398,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 18.1; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1421,7 +1427,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4VK5P5723H; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.1; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.portocode.EssentialFeediOSTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1441,7 +1447,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4VK5P5723H; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.1; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.portocode.EssentialFeediOSTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1460,6 +1466,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4VK5P5723H; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MACOSX_DEPLOYMENT_TARGET = 14.5; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.portocode.EssentialFeedCacheIntegrationTests; @@ -1477,6 +1484,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4VK5P5723H; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MACOSX_DEPLOYMENT_TARGET = 14.5; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.portocode.EssentialFeedCacheIntegrationTests; diff --git a/EssentialFeed/EssentialFeediOS/Feed UI/Views/Feed.storyboard b/EssentialFeed/EssentialFeediOS/Feed UI/Views/Feed.storyboard index d46af52..5e5270a 100644 --- a/EssentialFeed/EssentialFeediOS/Feed UI/Views/Feed.storyboard +++ b/EssentialFeed/EssentialFeediOS/Feed UI/Views/Feed.storyboard @@ -91,6 +91,9 @@ + + +