From e00a0d6a60a08ce8892085736c9b31b259018d43 Mon Sep 17 00:00:00 2001 From: gogolakovaa Date: Sun, 15 Sep 2024 15:14:58 +0200 Subject: [PATCH 01/21] Creates Login Feature first draft --- .../iOS/RocketApp.xcodeproj/project.pbxproj | 7 +++ Solution/iOS/RocketApp/Features/Package.swift | 10 ++++ .../Features/Sources/Login/LoginCore.swift | 49 +++++++++++++++ .../Features/Sources/Login/LoginView.swift | 60 +++++++++++++++++++ Solution/iOS/RocketApp/RocketApp.swift | 15 +++-- 5 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 Solution/iOS/RocketApp/Features/Sources/Login/LoginCore.swift create mode 100644 Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift diff --git a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj index 30a84697..912c30ad 100644 --- a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj +++ b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 165B20552999449B0047F70A /* Infrastructure in Resources */ = {isa = PBXBuildFile; fileRef = 165B20532999449B0047F70A /* Infrastructure */; }; 16BC7FFC29CC9AE5002AAE15 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 16BC7FFB29CC9AE5002AAE15 /* Assets.xcassets */; }; 635FE7B12A20CDD40018A585 /* RocketList in Frameworks */ = {isa = PBXBuildFile; productRef = 635FE7B02A20CDD40018A585 /* RocketList */; }; + 6EA6ACB52C96F40A00EA0B8A /* Login in Frameworks */ = {isa = PBXBuildFile; productRef = 6EA6ACB42C96F40A00EA0B8A /* Login */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -31,6 +32,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6EA6ACB52C96F40A00EA0B8A /* Login in Frameworks */, 635FE7B12A20CDD40018A585 /* RocketList in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -103,6 +105,7 @@ name = RocketApp; packageProductDependencies = ( 635FE7B02A20CDD40018A585 /* RocketList */, + 6EA6ACB42C96F40A00EA0B8A /* Login */, ); productName = RocketApp; productReference = 160CA40D28A506E4004A274A /* RocketApp.app */; @@ -407,6 +410,10 @@ isa = XCSwiftPackageProductDependency; productName = RocketList; }; + 6EA6ACB42C96F40A00EA0B8A /* Login */ = { + isa = XCSwiftPackageProductDependency; + productName = Login; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 160CA40528A506E4004A274A /* Project object */; diff --git a/Solution/iOS/RocketApp/Features/Package.swift b/Solution/iOS/RocketApp/Features/Package.swift index fb40ae48..7690916a 100644 --- a/Solution/iOS/RocketApp/Features/Package.swift +++ b/Solution/iOS/RocketApp/Features/Package.swift @@ -8,6 +8,10 @@ let package = Package( platforms: [.iOS(.v16), .macOS(.v12)], products: [ + .library( + name: "Login", + targets: ["Login"] + ), .library( name: "RocketDetail", targets: ["RocketDetail"] @@ -35,6 +39,12 @@ let package = Package( ], targets: [ + .target( + name: "Login", + dependencies: [ + .product(name: "ComposableArchitecture", package: "swift-composable-architecture") + ] + ), .target( name: "RocketDetail", dependencies: [ diff --git a/Solution/iOS/RocketApp/Features/Sources/Login/LoginCore.swift b/Solution/iOS/RocketApp/Features/Sources/Login/LoginCore.swift new file mode 100644 index 00000000..3822b23c --- /dev/null +++ b/Solution/iOS/RocketApp/Features/Sources/Login/LoginCore.swift @@ -0,0 +1,49 @@ +import ComposableArchitecture +import Foundation + +public struct LoginCore: ReducerProtocol { + public struct State: Equatable { + @BindingState public var username: String + @BindingState public var password: String + public var isAuthFailedMessageDisplayed: Bool + + public init(username: String = "", password: String = "", isAuthFailedMessageDisplayed: Bool = false) { + self.username = username + self.password = password + self.isAuthFailedMessageDisplayed = isAuthFailedMessageDisplayed + } + } + + public enum Action: BindableAction { + case binding(BindingAction) + case loginTapped + case loginSucceeded + } + + public init() {} + + public var body: some ReducerProtocol { + BindingReducer() + + Reduce { state, action in + switch action { + case .loginTapped: + guard isValid(username: state.username, password: state.password) else { + state.isAuthFailedMessageDisplayed = true + return .none + } + + return EffectTask(value: .loginSucceeded) + case .binding: + state.isAuthFailedMessageDisplayed = false + return .none + case .loginSucceeded: + return .none + } + } + } +} + +func isValid(username: String, password: String) -> Bool { + return false +} diff --git a/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift b/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift new file mode 100644 index 00000000..96aee272 --- /dev/null +++ b/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift @@ -0,0 +1,60 @@ +import ComposableArchitecture +import SwiftUI + +public struct LoginView: View { + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store) { viewStore in + VStack { + Text("Shiiiiiiiiiit, your credentials are not valid") + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color.pink) + .transition(.opacity) + .opacity(viewStore.isAuthFailedMessageDisplayed ? 1 : 0) + .accessibilityHidden(!viewStore.isAuthFailedMessageDisplayed) + .animation(.interpolatingSpring, value: viewStore.isAuthFailedMessageDisplayed) + + content + } + } + } + + var content: some View { + WithViewStore(self.store) { viewStore in + VStack { + Text("Login") + .font(.largeTitle) + .fontWeight(.bold) + .padding(.top, 20) + + Spacer() + + TextField("Username", text: viewStore.binding(\.$username)) + .textFieldStyle(.roundedBorder) + + SecureField("Password", text: viewStore.binding(\.$password)) + .textFieldStyle(.roundedBorder) + + Spacer() + + Button("Login") { + viewStore.send(.loginTapped) + } + .buttonStyle(.borderedProminent) + .tint(.pink) + } + .padding(.horizontal, 20) + } + } +} + +//#Preview { +// LoginView() +//} diff --git a/Solution/iOS/RocketApp/RocketApp.swift b/Solution/iOS/RocketApp/RocketApp.swift index ab9c94a4..d15b82b3 100644 --- a/Solution/iOS/RocketApp/RocketApp.swift +++ b/Solution/iOS/RocketApp/RocketApp.swift @@ -1,15 +1,22 @@ import ComposableArchitecture -import RocketList +//import RocketList +import Login import SwiftUI @main struct RocketApp: App { var body: some Scene { WindowGroup { - RocketListView( +// RocketListView( +// store: Store( +// initialState: RocketListCore.State(), +// reducer: RocketListCore()._printChanges() +// ) +// ) + LoginView( store: Store( - initialState: RocketListCore.State(), - reducer: RocketListCore()._printChanges() + initialState: LoginCore.State(), + reducer: LoginCore() ) ) } From 47ca89d6012ee6237737ed803de7a2cc73d26c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikol=20Duchanova=CC=81?= Date: Sun, 15 Sep 2024 16:12:05 +0200 Subject: [PATCH 02/21] Add UI Tests setup --- .../iOS/RocketApp.xcodeproj/project.pbxproj | 134 +++++++++++++++++- .../xcshareddata/xcschemes/RocketApp.xcscheme | 13 ++ .../Sources/UIToolkit/AccessibilityKeys.swift | 1 + .../RocketAppUITests/Utils/BaseScreen.swift | 5 + .../RocketAppUITests/Utils/BaseTestCase.swift | 18 +++ 5 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/AccessibilityKeys.swift create mode 100644 Solution/iOS/RocketAppUITests/Utils/BaseScreen.swift create mode 100644 Solution/iOS/RocketAppUITests/Utils/BaseTestCase.swift diff --git a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj index 30a84697..8459553e 100644 --- a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj +++ b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -15,6 +15,16 @@ 635FE7B12A20CDD40018A585 /* RocketList in Frameworks */ = {isa = PBXBuildFile; productRef = 635FE7B02A20CDD40018A585 /* RocketList */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + E5A6B6FA2C97216000376970 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 160CA40528A506E4004A274A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 160CA40C28A506E4004A274A; + remoteInfo = RocketApp; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 1607AECC28D21C240030F1DF /* AllTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AllTests.xctestplan; sourceTree = ""; }; 1607AECD28D21D040030F1DF /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -24,8 +34,13 @@ 165B20522999449B0047F70A /* Features */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Features; sourceTree = ""; }; 165B20532999449B0047F70A /* Infrastructure */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Infrastructure; sourceTree = ""; }; 16BC7FFB29CC9AE5002AAE15 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + E5A6B6F42C97216000376970 /* RocketAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RocketAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + E5A6B6F52C97216000376970 /* RocketAppUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = RocketAppUITests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 160CA40A28A506E4004A274A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -35,6 +50,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E5A6B6F12C97216000376970 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -52,6 +74,7 @@ children = ( 1607AECD28D21D040030F1DF /* README.md */, 163E0EBC28C7988100CECFD8 /* RocketApp */, + E5A6B6F52C97216000376970 /* RocketAppUITests */, 160CA40E28A506E4004A274A /* Products */, 163E0F2D28D093BD00CECFD8 /* Frameworks */, ); @@ -61,6 +84,7 @@ isa = PBXGroup; children = ( 160CA40D28A506E4004A274A /* RocketApp.app */, + E5A6B6F42C97216000376970 /* RocketAppUITests.xctest */, ); name = Products; sourceTree = ""; @@ -108,6 +132,29 @@ productReference = 160CA40D28A506E4004A274A /* RocketApp.app */; productType = "com.apple.product-type.application"; }; + E5A6B6F32C97216000376970 /* RocketAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E5A6B6FE2C97216000376970 /* Build configuration list for PBXNativeTarget "RocketAppUITests" */; + buildPhases = ( + E5A6B6F02C97216000376970 /* Sources */, + E5A6B6F12C97216000376970 /* Frameworks */, + E5A6B6F22C97216000376970 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E5A6B6FB2C97216000376970 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + E5A6B6F52C97216000376970 /* RocketAppUITests */, + ); + name = RocketAppUITests; + packageProductDependencies = ( + ); + productName = RocketAppUITests; + productReference = E5A6B6F42C97216000376970 /* RocketAppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -115,13 +162,17 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1340; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1420; TargetAttributes = { 160CA40C28A506E4004A274A = { CreatedOnToolsVersion = 13.4.1; LastSwiftMigration = 1340; }; + E5A6B6F32C97216000376970 = { + CreatedOnToolsVersion = 16.0; + TestTargetID = 160CA40C28A506E4004A274A; + }; }; }; buildConfigurationList = 160CA40828A506E4004A274A /* Build configuration list for PBXProject "RocketApp" */; @@ -142,6 +193,7 @@ projectRoot = ""; targets = ( 160CA40C28A506E4004A274A /* RocketApp */, + E5A6B6F32C97216000376970 /* RocketAppUITests */, ); }; /* End PBXProject section */ @@ -158,6 +210,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E5A6B6F22C97216000376970 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -191,8 +250,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E5A6B6F02C97216000376970 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + E5A6B6FB2C97216000376970 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 160CA40C28A506E4004A274A /* RocketApp */; + targetProxy = E5A6B6FA2C97216000376970 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 160CA42F28A506E6004A274A /* Debug */ = { isa = XCBuildConfiguration; @@ -379,6 +453,53 @@ }; name = Release; }; + E5A6B6FC2C97216000376970 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L4GD77D338; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cz.quanti.RocketAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = RocketApp; + }; + name = Debug; + }; + E5A6B6FD2C97216000376970 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L4GD77D338; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cz.quanti.RocketAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = RocketApp; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -400,6 +521,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + E5A6B6FE2C97216000376970 /* Build configuration list for PBXNativeTarget "RocketAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E5A6B6FC2C97216000376970 /* Debug */, + E5A6B6FD2C97216000376970 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ diff --git a/Solution/iOS/RocketApp.xcodeproj/xcshareddata/xcschemes/RocketApp.xcscheme b/Solution/iOS/RocketApp.xcodeproj/xcshareddata/xcschemes/RocketApp.xcscheme index f567a104..fd95eab3 100644 --- a/Solution/iOS/RocketApp.xcodeproj/xcshareddata/xcschemes/RocketApp.xcscheme +++ b/Solution/iOS/RocketApp.xcodeproj/xcshareddata/xcschemes/RocketApp.xcscheme @@ -51,6 +51,19 @@ default = "YES"> + + + + + + Date: Sun, 15 Sep 2024 17:58:16 +0200 Subject: [PATCH 03/21] Adds AppCore and integrates login and logout --- .../iOS/RocketApp.xcodeproj/project.pbxproj | 7 +++ Solution/iOS/RocketApp/Features/Package.swift | 13 ++++++ .../Features/Sources/App/AppCore.swift | 43 +++++++++++++++++++ .../Features/Sources/App/AppView.swift | 36 ++++++++++++++++ .../Features/Sources/Login/LoginCore.swift | 7 ++- .../Features/Sources/Login/LoginView.swift | 17 +++++--- .../RocketDetail/RocketDetailView.swift | 2 +- .../Sources/RocketList/RocketListCore.swift | 3 +- .../Sources/RocketList/RocketListView.swift | 26 +++++++---- .../quanti.colorset/Contents.json | 20 +++++++++ .../UIToolkit/Resources/Color+quanti.swift | 5 +++ Solution/iOS/RocketApp/RocketApp.swift | 15 ++----- 12 files changed, 167 insertions(+), 27 deletions(-) create mode 100644 Solution/iOS/RocketApp/Features/Sources/App/AppCore.swift create mode 100644 Solution/iOS/RocketApp/Features/Sources/App/AppView.swift create mode 100644 Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/Resources/Assets.xcassets/quanti.colorset/Contents.json create mode 100644 Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/Resources/Color+quanti.swift diff --git a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj index 912c30ad..7ce634ed 100644 --- a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj +++ b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 165B20552999449B0047F70A /* Infrastructure in Resources */ = {isa = PBXBuildFile; fileRef = 165B20532999449B0047F70A /* Infrastructure */; }; 16BC7FFC29CC9AE5002AAE15 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 16BC7FFB29CC9AE5002AAE15 /* Assets.xcassets */; }; 635FE7B12A20CDD40018A585 /* RocketList in Frameworks */ = {isa = PBXBuildFile; productRef = 635FE7B02A20CDD40018A585 /* RocketList */; }; + 6E3F7D202C972CFD00554BA3 /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3F7D1F2C972CFD00554BA3 /* App */; }; 6EA6ACB52C96F40A00EA0B8A /* Login in Frameworks */ = {isa = PBXBuildFile; productRef = 6EA6ACB42C96F40A00EA0B8A /* Login */; }; /* End PBXBuildFile section */ @@ -32,6 +33,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6E3F7D202C972CFD00554BA3 /* App in Frameworks */, 6EA6ACB52C96F40A00EA0B8A /* Login in Frameworks */, 635FE7B12A20CDD40018A585 /* RocketList in Frameworks */, ); @@ -106,6 +108,7 @@ packageProductDependencies = ( 635FE7B02A20CDD40018A585 /* RocketList */, 6EA6ACB42C96F40A00EA0B8A /* Login */, + 6E3F7D1F2C972CFD00554BA3 /* App */, ); productName = RocketApp; productReference = 160CA40D28A506E4004A274A /* RocketApp.app */; @@ -410,6 +413,10 @@ isa = XCSwiftPackageProductDependency; productName = RocketList; }; + 6E3F7D1F2C972CFD00554BA3 /* App */ = { + isa = XCSwiftPackageProductDependency; + productName = App; + }; 6EA6ACB42C96F40A00EA0B8A /* Login */ = { isa = XCSwiftPackageProductDependency; productName = Login; diff --git a/Solution/iOS/RocketApp/Features/Package.swift b/Solution/iOS/RocketApp/Features/Package.swift index 7690916a..9670a663 100644 --- a/Solution/iOS/RocketApp/Features/Package.swift +++ b/Solution/iOS/RocketApp/Features/Package.swift @@ -8,6 +8,10 @@ let package = Package( platforms: [.iOS(.v16), .macOS(.v12)], products: [ + .library( + name: "App", + targets: ["App"] + ), .library( name: "Login", targets: ["Login"] @@ -39,9 +43,18 @@ let package = Package( ], targets: [ + .target( + name: "App", + dependencies: [ + "Login", + "RocketList", + .product(name: "ComposableArchitecture", package: "swift-composable-architecture") + ] + ), .target( name: "Login", dependencies: [ + .product(name: "UIToolkit", package: "Infrastructure"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture") ] ), diff --git a/Solution/iOS/RocketApp/Features/Sources/App/AppCore.swift b/Solution/iOS/RocketApp/Features/Sources/App/AppCore.swift new file mode 100644 index 00000000..4c156a60 --- /dev/null +++ b/Solution/iOS/RocketApp/Features/Sources/App/AppCore.swift @@ -0,0 +1,43 @@ +import ComposableArchitecture +import Foundation +import Login +import RocketList + +public struct AppCore: ReducerProtocol { + public enum State: Equatable { + case login(LoginCore.State) + case rocketList(RocketListCore.State) + + public init() { + self = .login(LoginCore.State()) + } + } + + public enum Action { + case login(LoginCore.Action) + case rocketList(RocketListCore.Action) + } + + public init() {} + + public var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .login(.loginSucceeded): + state = .rocketList(RocketListCore.State()) + return .none + case .rocketList(.logoutTapped): + state = .login(LoginCore.State()) + return .none + default: + return .none + } + } + .ifCaseLet(/State.login, action: /Action.login) { + LoginCore() + } + .ifCaseLet(/State.rocketList, action: /Action.rocketList) { + RocketListCore() + } + } +} diff --git a/Solution/iOS/RocketApp/Features/Sources/App/AppView.swift b/Solution/iOS/RocketApp/Features/Sources/App/AppView.swift new file mode 100644 index 00000000..150e07c3 --- /dev/null +++ b/Solution/iOS/RocketApp/Features/Sources/App/AppView.swift @@ -0,0 +1,36 @@ +import ComposableArchitecture +import Login +import RocketList +import SwiftUI + +public struct AppView: View { + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + SwitchStore(store) { + CaseLet( + state: /AppCore.State.login, + action: AppCore.Action.login, + then: LoginView.init + ) + CaseLet( + state: /AppCore.State.rocketList, + action: AppCore.Action.rocketList, + then: RocketListView.init + ) + } + } +} + +#Preview { + AppView( + store: Store( + initialState: AppCore.State(), + reducer: AppCore() + ) + ) +} diff --git a/Solution/iOS/RocketApp/Features/Sources/Login/LoginCore.swift b/Solution/iOS/RocketApp/Features/Sources/Login/LoginCore.swift index 3822b23c..4b483b5a 100644 --- a/Solution/iOS/RocketApp/Features/Sources/Login/LoginCore.swift +++ b/Solution/iOS/RocketApp/Features/Sources/Login/LoginCore.swift @@ -45,5 +45,10 @@ public struct LoginCore: ReducerProtocol { } func isValid(username: String, password: String) -> Bool { - return false + let validPairs: [String: String] = [ + "username1": "test1234", + "astronaut1": "space" + ] + + return validPairs[username] == password } diff --git a/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift b/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift index 96aee272..dc37d02c 100644 --- a/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift @@ -1,5 +1,6 @@ import ComposableArchitecture import SwiftUI +import UIToolkit public struct LoginView: View { let store: StoreOf @@ -15,7 +16,7 @@ public struct LoginView: View { .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding() - .background(Color.pink) + .background(Color.quanti) .transition(.opacity) .opacity(viewStore.isAuthFailedMessageDisplayed ? 1 : 0) .accessibilityHidden(!viewStore.isAuthFailedMessageDisplayed) @@ -38,6 +39,7 @@ public struct LoginView: View { TextField("Username", text: viewStore.binding(\.$username)) .textFieldStyle(.roundedBorder) + .autocapitalization(.none) SecureField("Password", text: viewStore.binding(\.$password)) .textFieldStyle(.roundedBorder) @@ -48,13 +50,18 @@ public struct LoginView: View { viewStore.send(.loginTapped) } .buttonStyle(.borderedProminent) - .tint(.pink) + .tint(.quanti) } .padding(.horizontal, 20) } } } -//#Preview { -// LoginView() -//} +#Preview { + LoginView( + store: Store( + initialState: LoginCore.State(), + reducer: LoginCore() + ) + ) +} diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketDetail/RocketDetailView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketDetail/RocketDetailView.swift index d6ebd2b7..603b2d4b 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketDetail/RocketDetailView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketDetail/RocketDetailView.swift @@ -130,7 +130,7 @@ public struct RocketDetailView: View { } } - private func paramWindow(type: RocketDetail.RocketParameters, backgroundColor: Color = .pink) -> some View { + private func paramWindow(type: RocketDetail.RocketParameters, backgroundColor: Color = .quanti) -> some View { VStack(spacing: 4) { Text( type.detail( diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListCore.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListCore.swift index c3bd49bb..c64da79b 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListCore.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListCore.swift @@ -19,6 +19,7 @@ public struct RocketListCore: ReducerProtocol { case fetchData case dataFetched(TaskResult<[RocketDetail]>) case rocketDetail(PresentationAction) + case logoutTapped } public init() {} @@ -58,7 +59,7 @@ public struct RocketListCore: ReducerProtocol { state.loadingStatus = .failure(RocketsClientAsyncError(from: error)) return .none - case .rocketDetail: + case .rocketDetail, .logoutTapped: return .none } } diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift index 7e9b6e08..09edc206 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift @@ -26,15 +26,25 @@ public struct RocketListView: View { public var body: some View { NavigationStack { - Group { - switch viewStore.loadingStatus { - case let .success(data): - rocketsListView(rocketData: data) - case let .failure(error): - errorView(error: error) - default: - loadingView + VStack { + Group { + switch viewStore.loadingStatus { + case let .success(data): + rocketsListView(rocketData: data) + case let .failure(error): + errorView(error: error) + default: + loadingView + } } + + Spacer() + + Button("Logout") { + viewStore.send(.logoutTapped) + } + .buttonStyle(.borderedProminent) + .tint(.quanti) } .navigationTitle(.rockets) } diff --git a/Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/Resources/Assets.xcassets/quanti.colorset/Contents.json b/Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/Resources/Assets.xcassets/quanti.colorset/Contents.json new file mode 100644 index 00000000..2dd2fb5c --- /dev/null +++ b/Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/Resources/Assets.xcassets/quanti.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x87", + "green" : "0x51", + "red" : "0xF1" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/Resources/Color+quanti.swift b/Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/Resources/Color+quanti.swift new file mode 100644 index 00000000..583ae48d --- /dev/null +++ b/Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/Resources/Color+quanti.swift @@ -0,0 +1,5 @@ +import SwiftUI + +public extension Color { + static let quanti = Color("quanti", bundle: .module) +} diff --git a/Solution/iOS/RocketApp/RocketApp.swift b/Solution/iOS/RocketApp/RocketApp.swift index d15b82b3..95100762 100644 --- a/Solution/iOS/RocketApp/RocketApp.swift +++ b/Solution/iOS/RocketApp/RocketApp.swift @@ -1,22 +1,15 @@ import ComposableArchitecture -//import RocketList -import Login +import App import SwiftUI @main struct RocketApp: App { var body: some Scene { WindowGroup { -// RocketListView( -// store: Store( -// initialState: RocketListCore.State(), -// reducer: RocketListCore()._printChanges() -// ) -// ) - LoginView( + AppView( store: Store( - initialState: LoginCore.State(), - reducer: LoginCore() + initialState: AppCore.State(), + reducer: AppCore() ) ) } From 6cda78830ff248a995cba97be02b80b0a0f0b0ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikol=20Duchanova=CC=81?= Date: Sun, 15 Sep 2024 18:10:45 +0200 Subject: [PATCH 04/21] Add UI Tests scheme and format code --- .../iOS/RocketApp.xcodeproj/project.pbxproj | 7 ++ .../xcschemes/RocketAppUITests.xcscheme | 89 +++++++++++++++++++ .../RocketAppUITests/Utils/BaseScreen.swift | 2 +- .../RocketAppUITests/Utils/BaseTestCase.swift | 26 +++--- 4 files changed, 110 insertions(+), 14 deletions(-) create mode 100644 Solution/iOS/RocketApp.xcodeproj/xcshareddata/xcschemes/RocketAppUITests.xcscheme diff --git a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj index 8459553e..3105228a 100644 --- a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj +++ b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 165B20552999449B0047F70A /* Infrastructure in Resources */ = {isa = PBXBuildFile; fileRef = 165B20532999449B0047F70A /* Infrastructure */; }; 16BC7FFC29CC9AE5002AAE15 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 16BC7FFB29CC9AE5002AAE15 /* Assets.xcassets */; }; 635FE7B12A20CDD40018A585 /* RocketList in Frameworks */ = {isa = PBXBuildFile; productRef = 635FE7B02A20CDD40018A585 /* RocketList */; }; + E55944B42C97328A00594002 /* UIToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = E55944B32C97328A00594002 /* UIToolkit */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,6 +55,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E55944B42C97328A00594002 /* UIToolkit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -150,6 +152,7 @@ ); name = RocketAppUITests; packageProductDependencies = ( + E55944B32C97328A00594002 /* UIToolkit */, ); productName = RocketAppUITests; productReference = E5A6B6F42C97216000376970 /* RocketAppUITests.xctest */; @@ -537,6 +540,10 @@ isa = XCSwiftPackageProductDependency; productName = RocketList; }; + E55944B32C97328A00594002 /* UIToolkit */ = { + isa = XCSwiftPackageProductDependency; + productName = UIToolkit; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 160CA40528A506E4004A274A /* Project object */; diff --git a/Solution/iOS/RocketApp.xcodeproj/xcshareddata/xcschemes/RocketAppUITests.xcscheme b/Solution/iOS/RocketApp.xcodeproj/xcshareddata/xcschemes/RocketAppUITests.xcscheme new file mode 100644 index 00000000..06cb4e4b --- /dev/null +++ b/Solution/iOS/RocketApp.xcodeproj/xcshareddata/xcschemes/RocketAppUITests.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Solution/iOS/RocketAppUITests/Utils/BaseScreen.swift b/Solution/iOS/RocketAppUITests/Utils/BaseScreen.swift index 010d2599..b4ba4411 100644 --- a/Solution/iOS/RocketAppUITests/Utils/BaseScreen.swift +++ b/Solution/iOS/RocketAppUITests/Utils/BaseScreen.swift @@ -1,5 +1,5 @@ import XCTest protocol Screen { - var app: XCUIApplication { get } + var app: XCUIApplication { get } } diff --git a/Solution/iOS/RocketAppUITests/Utils/BaseTestCase.swift b/Solution/iOS/RocketAppUITests/Utils/BaseTestCase.swift index 137331e4..902659b4 100644 --- a/Solution/iOS/RocketAppUITests/Utils/BaseTestCase.swift +++ b/Solution/iOS/RocketAppUITests/Utils/BaseTestCase.swift @@ -1,18 +1,18 @@ import XCTest class BaseTestCase: XCTestCase { - var app: XCUIApplication! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + continueAfterFailure = false - override func setUp() { - super.setUp() - continueAfterFailure = false - - app = XCUIApplication() - app.launchArguments = ["UITestMode", "testsFreshRun"] - app.launch() - } - - override func tearDown() { - super.tearDown() - } + app = XCUIApplication() + app.launchArguments = ["UITestMode", "testsFreshRun"] + app.launch() + } + + override func tearDown() { + super.tearDown() + } } From 7f38fdba34aeade700d888ff14a4cc0c9783892a Mon Sep 17 00:00:00 2001 From: gogolakovaa Date: Sun, 15 Sep 2024 18:57:35 +0200 Subject: [PATCH 05/21] Creates custom button style --- .../Features/Sources/Login/LoginView.swift | 3 +-- .../Sources/RocketList/RocketListView.swift | 3 +-- .../UIToolkit/Components/QuantiButton.swift | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/Components/QuantiButton.swift diff --git a/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift b/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift index dc37d02c..f70bae6f 100644 --- a/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift @@ -49,8 +49,7 @@ public struct LoginView: View { Button("Login") { viewStore.send(.loginTapped) } - .buttonStyle(.borderedProminent) - .tint(.quanti) + .buttonStyle(QuantiButtonStyle()) } .padding(.horizontal, 20) } diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift index 09edc206..d59d328d 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift @@ -43,8 +43,7 @@ public struct RocketListView: View { Button("Logout") { viewStore.send(.logoutTapped) } - .buttonStyle(.borderedProminent) - .tint(.quanti) + .buttonStyle(QuantiButtonStyle()) } .navigationTitle(.rockets) } diff --git a/Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/Components/QuantiButton.swift b/Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/Components/QuantiButton.swift new file mode 100644 index 00000000..1097dd0a --- /dev/null +++ b/Solution/iOS/RocketApp/Infrastructure/Sources/UIToolkit/Components/QuantiButton.swift @@ -0,0 +1,15 @@ +import SwiftUI + +public struct QuantiButtonStyle: ButtonStyle { + + public init() {} + + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .frame(maxWidth: .infinity) + .padding() + .background(configuration.isPressed ? Color.quanti.opacity(0.3) : Color.quanti) + .foregroundColor(.white) + .clipShape(RoundedRectangle(cornerRadius: 24)) + } +} From f9e8089d51af9726f0ba81ff7ebbd02f50a3c5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikol=20Duchanova=CC=81?= Date: Sun, 15 Sep 2024 19:41:32 +0200 Subject: [PATCH 06/21] Add setup testing --- .../iOS/RocketApp.xcodeproj/project.pbxproj | 11 ++++++++-- .../AccessibilityKeys+RocketList.swift | 7 +++++++ .../Sources/RocketList/RocketListView.swift | 1 + .../Screens/RocketListScreen.swift | 20 +++++++++++++++++++ .../iOS/RocketAppUITests/Tests/Test.swift | 8 ++++++++ 5 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+RocketList.swift create mode 100644 Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift create mode 100644 Solution/iOS/RocketAppUITests/Tests/Test.swift diff --git a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj index 3105228a..e28fd276 100644 --- a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj +++ b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 165B20552999449B0047F70A /* Infrastructure in Resources */ = {isa = PBXBuildFile; fileRef = 165B20532999449B0047F70A /* Infrastructure */; }; 16BC7FFC29CC9AE5002AAE15 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 16BC7FFB29CC9AE5002AAE15 /* Assets.xcassets */; }; 635FE7B12A20CDD40018A585 /* RocketList in Frameworks */ = {isa = PBXBuildFile; productRef = 635FE7B02A20CDD40018A585 /* RocketList */; }; + E50DF9542C9751A700BE1B5D /* RocketList in Frameworks */ = {isa = PBXBuildFile; productRef = E50DF9532C9751A700BE1B5D /* RocketList */; }; E55944B42C97328A00594002 /* UIToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = E55944B32C97328A00594002 /* UIToolkit */; }; /* End PBXBuildFile section */ @@ -55,6 +56,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E50DF9542C9751A700BE1B5D /* RocketList in Frameworks */, E55944B42C97328A00594002 /* UIToolkit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -153,6 +155,7 @@ name = RocketAppUITests; packageProductDependencies = ( E55944B32C97328A00594002 /* UIToolkit */, + E50DF9532C9751A700BE1B5D /* RocketList */, ); productName = RocketAppUITests; productReference = E5A6B6F42C97216000376970 /* RocketAppUITests.xctest */; @@ -467,7 +470,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = cz.quanti.RocketAppUITests; @@ -491,7 +494,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = cz.quanti.RocketAppUITests; @@ -540,6 +543,10 @@ isa = XCSwiftPackageProductDependency; productName = RocketList; }; + E50DF9532C9751A700BE1B5D /* RocketList */ = { + isa = XCSwiftPackageProductDependency; + productName = RocketList; + }; E55944B32C97328A00594002 /* UIToolkit */ = { isa = XCSwiftPackageProductDependency; productName = UIToolkit; diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+RocketList.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+RocketList.swift new file mode 100644 index 00000000..38826640 --- /dev/null +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+RocketList.swift @@ -0,0 +1,7 @@ +import UIToolkit + +extension AccessibilityKeys { + public enum RocketList { + public static let list = "rocketListID" + } +} diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift index 7e9b6e08..5139e315 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift @@ -53,6 +53,7 @@ public struct RocketListView: View { RocketListCellView(store: $0) } } + .accessibilityIdentifier(AccessibilityKeys.RocketList.list) .listStyle(.sidebar) .navigationDestination( store: self.store.scope( diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift new file mode 100644 index 00000000..da614dd0 --- /dev/null +++ b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift @@ -0,0 +1,20 @@ +import UIToolkit +import RocketList +import XCTest + +struct RocketListScreen: Screen { + let app: XCUIApplication + + private let rocketList: XCUIElement + + init(app: XCUIApplication) { + self.app = app + rocketList = app.descendants(matching: .any)[AccessibilityKeys.RocketList.list] + } + + @discardableResult + func checkRocketList() -> Self { + XCTAssert(rocketList.waitForExistence(timeout: 5)) + return self + } +} diff --git a/Solution/iOS/RocketAppUITests/Tests/Test.swift b/Solution/iOS/RocketAppUITests/Tests/Test.swift new file mode 100644 index 00000000..5e3c3b60 --- /dev/null +++ b/Solution/iOS/RocketAppUITests/Tests/Test.swift @@ -0,0 +1,8 @@ +import XCTest + +final class Test: BaseTestCase { + func testTest() { + RocketListScreen(app: app) + .checkRocketList() + } +} From 61873705c30985192b58b795d17a588cf4856531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikol=20Duchanova=CC=81?= Date: Mon, 11 Nov 2024 20:49:59 +0100 Subject: [PATCH 07/21] hihi --- Solution/iOS/RocketApp.xcodeproj/project.pbxproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj index 65e35428..69c38def 100644 --- a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj +++ b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj @@ -556,6 +556,7 @@ E55944B32C97328A00594002 /* UIToolkit */ = { isa = XCSwiftPackageProductDependency; productName = UIToolkit; + }; 6E3F7D1F2C972CFD00554BA3 /* App */ = { isa = XCSwiftPackageProductDependency; productName = App; From 3fab0ad1dfb55c67008f16e386a2de771012d81a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikol=20Duchanova=CC=81?= Date: Mon, 11 Nov 2024 20:50:42 +0100 Subject: [PATCH 08/21] Change wrong credentials message --- Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift b/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift index f70bae6f..c02e70a4 100644 --- a/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift @@ -12,7 +12,7 @@ public struct LoginView: View { public var body: some View { WithViewStore(self.store) { viewStore in VStack { - Text("Shiiiiiiiiiit, your credentials are not valid") + Text("Sorry, your credentials are not valid") .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding() From 41707e244df5daa5b5568310035500160000b1cb Mon Sep 17 00:00:00 2001 From: nikolduchanova Date: Mon, 11 Nov 2024 21:46:40 +0100 Subject: [PATCH 09/21] Change example test --- .../iOS/RocketApp.xcodeproj/project.pbxproj | 27 ++++++++++++------- .../Login/AccessibilityKeys+Login.swift | 7 +++++ .../Features/Sources/Login/LoginView.swift | 1 + .../AccessibilityKeys+RocketList.swift | 7 ----- .../Sources/RocketList/RocketListView.swift | 5 ++-- .../Screens/LoginScreen.swift | 21 +++++++++++++++ .../Screens/RocketListScreen.swift | 20 -------------- .../iOS/RocketAppUITests/Tests/Test.swift | 4 +-- 8 files changed, 50 insertions(+), 42 deletions(-) create mode 100644 Solution/iOS/RocketApp/Features/Sources/Login/AccessibilityKeys+Login.swift delete mode 100644 Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+RocketList.swift create mode 100644 Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift delete mode 100644 Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift diff --git a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj index 69c38def..d8f92497 100644 --- a/Solution/iOS/RocketApp.xcodeproj/project.pbxproj +++ b/Solution/iOS/RocketApp.xcodeproj/project.pbxproj @@ -13,10 +13,11 @@ 165B20552999449B0047F70A /* Infrastructure in Resources */ = {isa = PBXBuildFile; fileRef = 165B20532999449B0047F70A /* Infrastructure */; }; 16BC7FFC29CC9AE5002AAE15 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 16BC7FFB29CC9AE5002AAE15 /* Assets.xcassets */; }; 635FE7B12A20CDD40018A585 /* RocketList in Frameworks */ = {isa = PBXBuildFile; productRef = 635FE7B02A20CDD40018A585 /* RocketList */; }; - E50DF9542C9751A700BE1B5D /* RocketList in Frameworks */ = {isa = PBXBuildFile; productRef = E50DF9532C9751A700BE1B5D /* RocketList */; }; - E55944B42C97328A00594002 /* UIToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = E55944B32C97328A00594002 /* UIToolkit */; }; 6E3F7D202C972CFD00554BA3 /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3F7D1F2C972CFD00554BA3 /* App */; }; 6EA6ACB52C96F40A00EA0B8A /* Login in Frameworks */ = {isa = PBXBuildFile; productRef = 6EA6ACB42C96F40A00EA0B8A /* Login */; }; + E50818DC2CE2A31A00B9AE68 /* Login in Frameworks */ = {isa = PBXBuildFile; productRef = E50818DB2CE2A31A00B9AE68 /* Login */; }; + E50DF9542C9751A700BE1B5D /* RocketList in Frameworks */ = {isa = PBXBuildFile; productRef = E50DF9532C9751A700BE1B5D /* RocketList */; }; + E55944B42C97328A00594002 /* UIToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = E55944B32C97328A00594002 /* UIToolkit */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,6 +61,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E50818DC2CE2A31A00B9AE68 /* Login in Frameworks */, E50DF9542C9751A700BE1B5D /* RocketList in Frameworks */, E55944B42C97328A00594002 /* UIToolkit in Frameworks */, ); @@ -162,6 +164,7 @@ packageProductDependencies = ( E55944B32C97328A00594002 /* UIToolkit */, E50DF9532C9751A700BE1B5D /* RocketList */, + E50818DB2CE2A31A00B9AE68 /* Login */, ); productName = RocketAppUITests; productReference = E5A6B6F42C97216000376970 /* RocketAppUITests.xctest */; @@ -549,14 +552,6 @@ isa = XCSwiftPackageProductDependency; productName = RocketList; }; - E50DF9532C9751A700BE1B5D /* RocketList */ = { - isa = XCSwiftPackageProductDependency; - productName = RocketList; - }; - E55944B32C97328A00594002 /* UIToolkit */ = { - isa = XCSwiftPackageProductDependency; - productName = UIToolkit; - }; 6E3F7D1F2C972CFD00554BA3 /* App */ = { isa = XCSwiftPackageProductDependency; productName = App; @@ -565,6 +560,18 @@ isa = XCSwiftPackageProductDependency; productName = Login; }; + E50818DB2CE2A31A00B9AE68 /* Login */ = { + isa = XCSwiftPackageProductDependency; + productName = Login; + }; + E50DF9532C9751A700BE1B5D /* RocketList */ = { + isa = XCSwiftPackageProductDependency; + productName = RocketList; + }; + E55944B32C97328A00594002 /* UIToolkit */ = { + isa = XCSwiftPackageProductDependency; + productName = UIToolkit; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 160CA40528A506E4004A274A /* Project object */; diff --git a/Solution/iOS/RocketApp/Features/Sources/Login/AccessibilityKeys+Login.swift b/Solution/iOS/RocketApp/Features/Sources/Login/AccessibilityKeys+Login.swift new file mode 100644 index 00000000..bdfe278a --- /dev/null +++ b/Solution/iOS/RocketApp/Features/Sources/Login/AccessibilityKeys+Login.swift @@ -0,0 +1,7 @@ +import UIToolkit + +extension AccessibilityKeys { + public enum Login { + public static let title = "titleID" + } +} diff --git a/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift b/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift index c02e70a4..41a583be 100644 --- a/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift @@ -34,6 +34,7 @@ public struct LoginView: View { .font(.largeTitle) .fontWeight(.bold) .padding(.top, 20) + .accessibilityIdentifier(AccessibilityKeys.Login.title) Spacer() diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+RocketList.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+RocketList.swift deleted file mode 100644 index 38826640..00000000 --- a/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+RocketList.swift +++ /dev/null @@ -1,7 +0,0 @@ -import UIToolkit - -extension AccessibilityKeys { - public enum RocketList { - public static let list = "rocketListID" - } -} diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift index 336efba7..668e09ff 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift @@ -37,9 +37,9 @@ public struct RocketListView: View { loadingView } } - + Spacer() - + Button("Logout") { viewStore.send(.logoutTapped) } @@ -62,7 +62,6 @@ public struct RocketListView: View { RocketListCellView(store: $0) } } - .accessibilityIdentifier(AccessibilityKeys.RocketList.list) .listStyle(.sidebar) .navigationDestination( store: self.store.scope( diff --git a/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift b/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift new file mode 100644 index 00000000..221cb88b --- /dev/null +++ b/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift @@ -0,0 +1,21 @@ +import UIToolkit +import RocketList +import Login +import XCTest + +struct LoginScreen: Screen { + let app: XCUIApplication + + private let loginTitle: XCUIElement + + init(app: XCUIApplication) { + self.app = app + loginTitle = app.staticTexts[AccessibilityKeys.Login.title] + } + + @discardableResult + func checkLoginTitle() -> Self { + XCTAssert(loginTitle.waitForExistence(timeout: 5)) + return self + } +} diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift deleted file mode 100644 index da614dd0..00000000 --- a/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift +++ /dev/null @@ -1,20 +0,0 @@ -import UIToolkit -import RocketList -import XCTest - -struct RocketListScreen: Screen { - let app: XCUIApplication - - private let rocketList: XCUIElement - - init(app: XCUIApplication) { - self.app = app - rocketList = app.descendants(matching: .any)[AccessibilityKeys.RocketList.list] - } - - @discardableResult - func checkRocketList() -> Self { - XCTAssert(rocketList.waitForExistence(timeout: 5)) - return self - } -} diff --git a/Solution/iOS/RocketAppUITests/Tests/Test.swift b/Solution/iOS/RocketAppUITests/Tests/Test.swift index 5e3c3b60..ed4eafc4 100644 --- a/Solution/iOS/RocketAppUITests/Tests/Test.swift +++ b/Solution/iOS/RocketAppUITests/Tests/Test.swift @@ -2,7 +2,7 @@ import XCTest final class Test: BaseTestCase { func testTest() { - RocketListScreen(app: app) - .checkRocketList() + LoginScreen(app: app) + .checkLoginTitle() } } From d1fe0ae95e4fc4df7b7f6e173c079f43da42a6d8 Mon Sep 17 00:00:00 2001 From: nikolduchanova Date: Tue, 12 Nov 2024 12:11:41 +0100 Subject: [PATCH 10/21] Cleaning --- .../Features/Sources/RocketList/RocketListView.swift | 4 ++-- .../iOS/RocketAppUITests/Screens/LoginScreen.swift | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift index 668e09ff..881d4184 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift @@ -37,9 +37,9 @@ public struct RocketListView: View { loadingView } } - + Spacer() - + Button("Logout") { viewStore.send(.logoutTapped) } diff --git a/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift b/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift index 221cb88b..7f2bb780 100644 --- a/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift @@ -1,18 +1,18 @@ -import UIToolkit -import RocketList import Login +import RocketList +import UIToolkit import XCTest struct LoginScreen: Screen { let app: XCUIApplication - + private let loginTitle: XCUIElement - + init(app: XCUIApplication) { self.app = app loginTitle = app.staticTexts[AccessibilityKeys.Login.title] } - + @discardableResult func checkLoginTitle() -> Self { XCTAssert(loginTitle.waitForExistence(timeout: 5)) From ec0c0a08e332c8aa8c6c4ef78aa7612166d7b6b7 Mon Sep 17 00:00:00 2001 From: Cyril Colombier <> Date: Wed, 4 Dec 2024 15:39:00 +0100 Subject: [PATCH 11/21] Add Accessibility Keys for Login Screen Add Accessibility Keys for Rocket List Screen Add Accessibility Keys for Rocket Detail Screen Add Accessibility Keys for Rocket Launch Screen Add Rocket List Screen Add Rocket Rocket Detail Screen Add Rocket Launch screen Add Rocket Launch Test --- .../Login/AccessibilityKeys+Login.swift | 3 ++ .../Features/Sources/Login/LoginView.swift | 3 ++ .../AccessibilityKeys+Detail.swift | 8 +++++ .../RocketDetail/RocketDetailView.swift | 2 ++ .../AccessibilityKeys+Launch.swift | 7 ++++ .../RocketLaunch/RocketLaunchView.swift | 1 + .../RocketList/AccessibilityKeys+List.swift | 8 +++++ .../Sources/RocketList/RocketListView.swift | 3 +- .../Screens/LoginScreen.swift | 27 ++++++++++++++++ .../Screens/RocketDetailScreen.swift | 32 +++++++++++++++++++ .../Screens/RocketLaunchScreen.swift | 22 +++++++++++++ .../Screens/RocketListScreen.swift | 31 ++++++++++++++++++ .../Tests/RocketLaunchTest.swift | 29 +++++++++++++++++ 13 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 Solution/iOS/RocketApp/Features/Sources/RocketDetail/AccessibilityKeys+Detail.swift create mode 100644 Solution/iOS/RocketApp/Features/Sources/RocketLaunch/AccessibilityKeys+Launch.swift create mode 100644 Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+List.swift create mode 100644 Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift create mode 100644 Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift create mode 100644 Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift create mode 100644 Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift diff --git a/Solution/iOS/RocketApp/Features/Sources/Login/AccessibilityKeys+Login.swift b/Solution/iOS/RocketApp/Features/Sources/Login/AccessibilityKeys+Login.swift index bdfe278a..3778d4fd 100644 --- a/Solution/iOS/RocketApp/Features/Sources/Login/AccessibilityKeys+Login.swift +++ b/Solution/iOS/RocketApp/Features/Sources/Login/AccessibilityKeys+Login.swift @@ -3,5 +3,8 @@ import UIToolkit extension AccessibilityKeys { public enum Login { public static let title = "titleID" + public static let username = "usernameField" + public static let password = "passwordField" + public static let loginButton = "loginButton" } } diff --git a/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift b/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift index 41a583be..e1f00452 100644 --- a/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/Login/LoginView.swift @@ -41,9 +41,11 @@ public struct LoginView: View { TextField("Username", text: viewStore.binding(\.$username)) .textFieldStyle(.roundedBorder) .autocapitalization(.none) + .accessibilityIdentifier(AccessibilityKeys.Login.username) SecureField("Password", text: viewStore.binding(\.$password)) .textFieldStyle(.roundedBorder) + .accessibilityIdentifier(AccessibilityKeys.Login.password) Spacer() @@ -51,6 +53,7 @@ public struct LoginView: View { viewStore.send(.loginTapped) } .buttonStyle(QuantiButtonStyle()) + .accessibilityIdentifier(AccessibilityKeys.Login.loginButton) } .padding(.horizontal, 20) } diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketDetail/AccessibilityKeys+Detail.swift b/Solution/iOS/RocketApp/Features/Sources/RocketDetail/AccessibilityKeys+Detail.swift new file mode 100644 index 00000000..dd874760 --- /dev/null +++ b/Solution/iOS/RocketApp/Features/Sources/RocketDetail/AccessibilityKeys+Detail.swift @@ -0,0 +1,8 @@ +import UIToolkit + +extension AccessibilityKeys { + public enum RocketDetail { + public static let rocketTitle = "Falcon 1" + public static let launchButton = "Launch" + } +} diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketDetail/RocketDetailView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketDetail/RocketDetailView.swift index 603b2d4b..075bbde6 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketDetail/RocketDetailView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketDetail/RocketDetailView.swift @@ -97,8 +97,10 @@ public struct RocketDetailView: View { } .padding(.horizontal) .navigationTitle(viewStore.rocketData.name) + .accessibilityIdentifier(AccessibilityKeys.RocketDetail.rocketTitle) .onAppear { viewStore.send(.rocketLaunchDismiss) } .navigationBarItems(trailing: Button(.launch) { viewStore.send(.rocketLaunchTapped) }) + .accessibilityIdentifier(AccessibilityKeys.RocketDetail.launchButton) .navigationDestination( store: self.store.scope( state: \.$rocketLaunch, diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/AccessibilityKeys+Launch.swift b/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/AccessibilityKeys+Launch.swift new file mode 100644 index 00000000..97a1f676 --- /dev/null +++ b/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/AccessibilityKeys+Launch.swift @@ -0,0 +1,7 @@ +import UIToolkit + +extension AccessibilityKeys { + public enum RocketLaunch { + public static let rocketImage = "rocket_idle" + } +} diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/RocketLaunchView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/RocketLaunchView.swift index 797f7110..e3046415 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/RocketLaunchView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/RocketLaunchView.swift @@ -104,6 +104,7 @@ public struct RocketLaunchView: View { private var flyingRocket: some View { Image.rocketFlying + .accessibilityIdentifier(AccessibilityKeys.RocketLaunch.rocketImage) .scaleEffect(rocketScale) .overlay(alignment: .bottom) { Group { diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+List.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+List.swift new file mode 100644 index 00000000..a90be5b1 --- /dev/null +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+List.swift @@ -0,0 +1,8 @@ +import UIToolkit + +extension AccessibilityKeys { + public enum RocketList { + public static let logoutButton = "logoutButton" + public static let rocketCell = "rocketCell" + } +} diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift index 881d4184..c347b401 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift @@ -39,11 +39,11 @@ public struct RocketListView: View { } Spacer() - Button("Logout") { viewStore.send(.logoutTapped) } .buttonStyle(QuantiButtonStyle()) + .accessibilityIdentifier(AccessibilityKeys.RocketList.logoutButton) } .navigationTitle(.rockets) } @@ -60,6 +60,7 @@ public struct RocketListView: View { ) ) { RocketListCellView(store: $0) + .accessibilityIdentifier(AccessibilityKeys.RocketList.rocketCell) } } .listStyle(.sidebar) diff --git a/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift b/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift index 7f2bb780..41db4ec7 100644 --- a/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift @@ -7,10 +7,16 @@ struct LoginScreen: Screen { let app: XCUIApplication private let loginTitle: XCUIElement + private let usernameField: XCUIElement + private let passwordField: XCUIElement + private let loginButton: XCUIElement init(app: XCUIApplication) { self.app = app loginTitle = app.staticTexts[AccessibilityKeys.Login.title] + usernameField = app.textFields[AccessibilityKeys.Login.username] + passwordField = app.secureTextFields[AccessibilityKeys.Login.password] + loginButton = app.buttons[AccessibilityKeys.Login.loginButton] } @discardableResult @@ -18,4 +24,25 @@ struct LoginScreen: Screen { XCTAssert(loginTitle.waitForExistence(timeout: 5)) return self } + + @discardableResult + func enterUsername(_ username: String) -> Self { + usernameField.tap() + usernameField.typeText(username) + return self + } + + @discardableResult + func enterPassword(_ password: String) -> Self { + passwordField.tap() + passwordField.typeText(password) + return self + } + + @discardableResult + func tapLoginButton() -> Self { + loginButton.tap() + return self + } + } diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift new file mode 100644 index 00000000..8cf77447 --- /dev/null +++ b/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift @@ -0,0 +1,32 @@ +import Login +import RocketList +import UIToolkit +import XCTest + +struct RocketDetailScreen: Screen { + let app: XCUIApplication + + private let rocketTitle: XCUIElement + private let launchButton: XCUIElement + + init(app: XCUIApplication) { + self.app = app + rocketTitle = app.navigationBars.staticTexts[AccessibilityKeys.RocketDetail.rocketTitle] + launchButton = app.buttons[AccessibilityKeys.RocketDetail.launchButton] + } + + @discardableResult + func checkRocketTitle() -> Self { + XCTAssert(rocketTitle.waitForExistence(timeout: 5)) + return self + } + @discardableResult + func tapLaunchButton() -> Self { + launchButton.tap() + return self + } + + + + +} diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift new file mode 100644 index 00000000..d03d48ae --- /dev/null +++ b/Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift @@ -0,0 +1,22 @@ +import Login +import RocketList +import UIToolkit +import XCTest + +struct RocketLaunchScreen: Screen { + let app: XCUIApplication + + private let rocketImage: XCUIElement + + init(app: XCUIApplication) { + self.app = app + rocketImage = app.images[AccessibilityKeys.RocketLaunch.rocketImage] + + } + + @discardableResult + func checkRocketImage() -> Self { + XCTAssert(rocketImage.waitForExistence(timeout: 5)) + return self + } +} diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift new file mode 100644 index 00000000..83b0c97b --- /dev/null +++ b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift @@ -0,0 +1,31 @@ +import Login +import RocketList +import UIToolkit +import XCTest + +struct RocketListScreen: Screen { + let app: XCUIApplication + + private let logoutButton: XCUIElement + private let firstRocketCell: XCUIElement + + init(app: XCUIApplication) { + self.app = app + logoutButton = app.buttons[AccessibilityKeys.RocketList.logoutButton] + firstRocketCell = app.cells[AccessibilityKeys.RocketList.rocketCell] + } + + @discardableResult + func checkLogoutButton() -> Self { + XCTAssert(logoutButton.waitForExistence(timeout: 5)) + return self + } + + @discardableResult + func tapFirstRocketCell() -> Self { + let rocketCells = app.buttons.matching(identifier: "rocketCell") + let firstRocketCell = rocketCells.element(boundBy: 0) + firstRocketCell.tap() + return self + } +} diff --git a/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift b/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift new file mode 100644 index 00000000..7483800d --- /dev/null +++ b/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift @@ -0,0 +1,29 @@ +// +// Test 2.swift +// RocketApp +// +// Created by Cyril Colombier on 30.11.2024. +// + +import XCTest + +final class TestRocketLaunch: BaseTestCase { + func testRocketLaunchTest() { + let username = "astronaut1" + let password = "space" + + LoginScreen(app: app) + .checkLoginTitle() + .enterUsername(username) + .enterPassword(password) + .tapLoginButton() + RocketListScreen(app: app) + .checkLogoutButton() + .tapFirstRocketCell() + RocketDetailScreen(app: app) + .checkRocketTitle() + .tapLaunchButton() + RocketLaunchScreen(app: app) + .checkRocketImage() + } +} From a39ff8435f7099ee6282416048d54ad37a3f4982 Mon Sep 17 00:00:00 2001 From: Cyril Colombier <> Date: Sun, 8 Dec 2024 15:05:49 +0100 Subject: [PATCH 12/21] Add Accessibility Identifiers --- .../Features/Sources/Login/AccessibilityKeys+Login.swift | 6 +++--- .../Sources/RocketDetail/AccessibilityKeys+Detail.swift | 4 ++-- .../Sources/RocketLaunch/AccessibilityKeys+Launch.swift | 2 +- .../Features/Sources/RocketLaunch/RocketLaunchView.swift | 3 ++- .../Sources/RocketList/AccessibilityKeys+List.swift | 3 +-- .../Features/Sources/RocketList/RocketListView.swift | 3 +-- .../Sources/RocketListCell/AccessibilityKeys+List.swift | 7 +++++++ .../Sources/RocketListCell/RocketListCellView.swift | 1 + 8 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 Solution/iOS/RocketApp/Features/Sources/RocketListCell/AccessibilityKeys+List.swift diff --git a/Solution/iOS/RocketApp/Features/Sources/Login/AccessibilityKeys+Login.swift b/Solution/iOS/RocketApp/Features/Sources/Login/AccessibilityKeys+Login.swift index 3778d4fd..6c1422e4 100644 --- a/Solution/iOS/RocketApp/Features/Sources/Login/AccessibilityKeys+Login.swift +++ b/Solution/iOS/RocketApp/Features/Sources/Login/AccessibilityKeys+Login.swift @@ -3,8 +3,8 @@ import UIToolkit extension AccessibilityKeys { public enum Login { public static let title = "titleID" - public static let username = "usernameField" - public static let password = "passwordField" - public static let loginButton = "loginButton" + public static let username = "usernameFieldID" + public static let password = "passwordFieldID" + public static let loginButton = "loginButtonID" } } diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketDetail/AccessibilityKeys+Detail.swift b/Solution/iOS/RocketApp/Features/Sources/RocketDetail/AccessibilityKeys+Detail.swift index dd874760..31d49cbb 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketDetail/AccessibilityKeys+Detail.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketDetail/AccessibilityKeys+Detail.swift @@ -2,7 +2,7 @@ import UIToolkit extension AccessibilityKeys { public enum RocketDetail { - public static let rocketTitle = "Falcon 1" - public static let launchButton = "Launch" + public static let rocketTitle = "rocketTitleID" + public static let launchButton = "launchButtonID" } } diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/AccessibilityKeys+Launch.swift b/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/AccessibilityKeys+Launch.swift index 97a1f676..e0e5b3c2 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/AccessibilityKeys+Launch.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/AccessibilityKeys+Launch.swift @@ -2,6 +2,6 @@ import UIToolkit extension AccessibilityKeys { public enum RocketLaunch { - public static let rocketImage = "rocket_idle" + public static let rocketImage = "rocketImageID" } } diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/RocketLaunchView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/RocketLaunchView.swift index e3046415..44f78265 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/RocketLaunchView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketLaunch/RocketLaunchView.swift @@ -57,6 +57,8 @@ public struct RocketLaunchView: View { flyingRocket } else { Image.rocketIdle + .accessibilityIdentifier(AccessibilityKeys.RocketLaunch.rocketImage) + } Spacer() @@ -104,7 +106,6 @@ public struct RocketLaunchView: View { private var flyingRocket: some View { Image.rocketFlying - .accessibilityIdentifier(AccessibilityKeys.RocketLaunch.rocketImage) .scaleEffect(rocketScale) .overlay(alignment: .bottom) { Group { diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+List.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+List.swift index a90be5b1..2a78cb7a 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+List.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+List.swift @@ -2,7 +2,6 @@ import UIToolkit extension AccessibilityKeys { public enum RocketList { - public static let logoutButton = "logoutButton" - public static let rocketCell = "rocketCell" + public static let rocketCells = "rocketCellsID" } } diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift index c347b401..3d1bb9d9 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift @@ -43,7 +43,6 @@ public struct RocketListView: View { viewStore.send(.logoutTapped) } .buttonStyle(QuantiButtonStyle()) - .accessibilityIdentifier(AccessibilityKeys.RocketList.logoutButton) } .navigationTitle(.rockets) } @@ -60,7 +59,7 @@ public struct RocketListView: View { ) ) { RocketListCellView(store: $0) - .accessibilityIdentifier(AccessibilityKeys.RocketList.rocketCell) + .accessibilityIdentifier(AccessibilityKeys.RocketList.rocketCells) } } .listStyle(.sidebar) diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketListCell/AccessibilityKeys+List.swift b/Solution/iOS/RocketApp/Features/Sources/RocketListCell/AccessibilityKeys+List.swift new file mode 100644 index 00000000..41848a62 --- /dev/null +++ b/Solution/iOS/RocketApp/Features/Sources/RocketListCell/AccessibilityKeys+List.swift @@ -0,0 +1,7 @@ +import UIToolkit + +extension AccessibilityKeys { + public enum RocketListCell { + public static let rocketCellArrow = "rocketCellArrowID" + } +} diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketListCell/RocketListCellView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketListCell/RocketListCellView.swift index ed8b2361..a37703b7 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketListCell/RocketListCellView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketListCell/RocketListCellView.swift @@ -34,6 +34,7 @@ public struct RocketListCellView: View { Image.linkArrow .resizable() .frame(width: 32, height: 32) + .accessibilityIdentifier(AccessibilityKeys.RocketListCell.rocketCellArrow) } } } From 36997f23ea74cba30c99e17e75f32846c4eee47d Mon Sep 17 00:00:00 2001 From: Cyril Colombier <> Date: Sun, 8 Dec 2024 15:09:18 +0100 Subject: [PATCH 13/21] Add Accessibility Identifiers for RocketDetailView --- .../Sources/RocketDetail/RocketDetailView.swift | 10 +++++++--- .../RocketAppUITests/Screens/RocketDetailScreen.swift | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketDetail/RocketDetailView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketDetail/RocketDetailView.swift index 075bbde6..5b2d7948 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketDetail/RocketDetailView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketDetail/RocketDetailView.swift @@ -57,6 +57,7 @@ public struct RocketDetailView: View { section(.overview) { Text(viewStore.rocketData.overview) .font(.body) + .accessibilityIdentifier(AccessibilityKeys.RocketDetail.rocketTitle) } .padding(.bottom) @@ -97,10 +98,13 @@ public struct RocketDetailView: View { } .padding(.horizontal) .navigationTitle(viewStore.rocketData.name) - .accessibilityIdentifier(AccessibilityKeys.RocketDetail.rocketTitle) .onAppear { viewStore.send(.rocketLaunchDismiss) } - .navigationBarItems(trailing: Button(.launch) { viewStore.send(.rocketLaunchTapped) }) - .accessibilityIdentifier(AccessibilityKeys.RocketDetail.launchButton) + .navigationBarItems( + trailing: Button(.launch) { + viewStore.send(.rocketLaunchTapped) + } + .accessibilityIdentifier(AccessibilityKeys.RocketDetail.launchButton) + ) .navigationDestination( store: self.store.scope( state: \.$rocketLaunch, diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift index 8cf77447..c54be455 100644 --- a/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift @@ -11,8 +11,8 @@ struct RocketDetailScreen: Screen { init(app: XCUIApplication) { self.app = app - rocketTitle = app.navigationBars.staticTexts[AccessibilityKeys.RocketDetail.rocketTitle] - launchButton = app.buttons[AccessibilityKeys.RocketDetail.launchButton] + rocketTitle = app.staticTexts[AccessibilityKeys.RocketDetail.rocketTitle] + launchButton = app.navigationBars.buttons[AccessibilityKeys.RocketDetail.launchButton] } @discardableResult From 57d2737b1bfa5bdc5712382b73b27eba5c67488a Mon Sep 17 00:00:00 2001 From: Cyril Colombier <> Date: Sun, 8 Dec 2024 15:16:02 +0100 Subject: [PATCH 14/21] Add test to count rocket cells and improve test to tap arrow on first rocket cell. --- .../Screens/RocketListScreen.swift | 34 ++++++++++++------- .../Tests/RocketLaunchTest.swift | 9 +---- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift index 83b0c97b..e3a9adcb 100644 --- a/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift @@ -5,27 +5,37 @@ import XCTest struct RocketListScreen: Screen { let app: XCUIApplication - - private let logoutButton: XCUIElement - private let firstRocketCell: XCUIElement + + private let rocketCells: XCUIElementQuery + private let rocketCellArrow: XCUIElementQuery init(app: XCUIApplication) { self.app = app - logoutButton = app.buttons[AccessibilityKeys.RocketList.logoutButton] - firstRocketCell = app.cells[AccessibilityKeys.RocketList.rocketCell] + + rocketCells = app.buttons.matching(identifier: "rocketCellsID") + rocketCellArrow = app.images.matching(identifier: "rocketCellArrowID") + } - + @discardableResult - func checkLogoutButton() -> Self { - XCTAssert(logoutButton.waitForExistence(timeout: 5)) - return self - } + func countRocketCells() -> Self { + let exists = rocketCells.firstMatch.waitForExistence(timeout: 5) + XCTAssertTrue(exists, "Rocket cells did not appear within the timeout.") + + let cellCount = rocketCells.count + print("Number of rocket cells displayed: \(cellCount)") + + XCTAssertEqual(cellCount, 4, "Expected exactly 4 rocket cells to be displayed.") + + return self + } @discardableResult func tapFirstRocketCell() -> Self { - let rocketCells = app.buttons.matching(identifier: "rocketCell") - let firstRocketCell = rocketCells.element(boundBy: 0) + let firstRocketCell = rocketCellArrow.element(boundBy: 0) + firstRocketCell.tap() + return self } } diff --git a/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift b/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift index 7483800d..2e37ad59 100644 --- a/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift +++ b/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift @@ -1,10 +1,3 @@ -// -// Test 2.swift -// RocketApp -// -// Created by Cyril Colombier on 30.11.2024. -// - import XCTest final class TestRocketLaunch: BaseTestCase { @@ -18,7 +11,7 @@ final class TestRocketLaunch: BaseTestCase { .enterPassword(password) .tapLoginButton() RocketListScreen(app: app) - .checkLogoutButton() + .countRocketCells() .tapFirstRocketCell() RocketDetailScreen(app: app) .checkRocketTitle() From a0b74b4aac52a1b8a4ebf82279fc9b60ef7a70cf Mon Sep 17 00:00:00 2001 From: Cyril Colombier <> Date: Sun, 8 Dec 2024 15:17:03 +0100 Subject: [PATCH 15/21] Add failure messages --- Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift | 2 +- Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift | 2 +- Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift b/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift index 41db4ec7..4eeedc94 100644 --- a/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift @@ -21,7 +21,7 @@ struct LoginScreen: Screen { @discardableResult func checkLoginTitle() -> Self { - XCTAssert(loginTitle.waitForExistence(timeout: 5)) + XCTAssert(loginTitle.waitForExistence(timeout: 5), "Login title did not appear within the timeout.") return self } diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift index c54be455..d18b7d4e 100644 --- a/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift @@ -17,7 +17,7 @@ struct RocketDetailScreen: Screen { @discardableResult func checkRocketTitle() -> Self { - XCTAssert(rocketTitle.waitForExistence(timeout: 5)) + XCTAssert(rocketTitle.waitForExistence(timeout: 5), "Rocket title did not appear within the timeout.") return self } @discardableResult diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift index d03d48ae..ff81d5d5 100644 --- a/Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift @@ -16,7 +16,7 @@ struct RocketLaunchScreen: Screen { @discardableResult func checkRocketImage() -> Self { - XCTAssert(rocketImage.waitForExistence(timeout: 5)) + XCTAssert(rocketImage.waitForExistence(timeout: 5), "Rocket image did not appear within the timeout.") return self } } From e49a101f5fb9e42b95b170601b4b94ef1ae45559 Mon Sep 17 00:00:00 2001 From: Cyril Colombier <> Date: Tue, 10 Dec 2024 17:59:27 +0100 Subject: [PATCH 16/21] Fix countRocketCells func and naming --- .../RocketList/AccessibilityKeys+List.swift | 2 +- .../Screens/RocketListScreen.swift | 28 ++++++++----------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+List.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+List.swift index 2a78cb7a..5560eb44 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+List.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/AccessibilityKeys+List.swift @@ -2,6 +2,6 @@ import UIToolkit extension AccessibilityKeys { public enum RocketList { - public static let rocketCells = "rocketCellsID" + public static let rocketCell = "rocketCellID" } } diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift index e3a9adcb..cbbeb309 100644 --- a/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift @@ -7,33 +7,29 @@ struct RocketListScreen: Screen { let app: XCUIApplication private let rocketCells: XCUIElementQuery - private let rocketCellArrow: XCUIElementQuery + private let rocketCellArrows: XCUIElementQuery init(app: XCUIApplication) { self.app = app - rocketCells = app.buttons.matching(identifier: "rocketCellsID") - rocketCellArrow = app.images.matching(identifier: "rocketCellArrowID") - + rocketCells = app.buttons.matching(identifier: AccessibilityKeys.RocketList.rocketCell) + rocketCellArrows = app.images.matching(identifier: AccessibilityKeys.RocketListCell.rocketCellArrow) } @discardableResult - func countRocketCells() -> Self { - let exists = rocketCells.firstMatch.waitForExistence(timeout: 5) - XCTAssertTrue(exists, "Rocket cells did not appear within the timeout.") - - let cellCount = rocketCells.count - print("Number of rocket cells displayed: \(cellCount)") - - XCTAssertEqual(cellCount, 4, "Expected exactly 4 rocket cells to be displayed.") - - return self + func countRocketCells() -> Self { + let cellCount = rocketCells.count + + XCTAssert(rocketCells.firstMatch.waitForExistence(timeout: 5), "Rocket cells did not appear within the timeout.") + print("Number of rocket cells displayed: \(cellCount)") + XCTAssertEqual(cellCount, 4, "Expected exactly 4 rocket cells to be displayed.") + + return self } @discardableResult func tapFirstRocketCell() -> Self { - let firstRocketCell = rocketCellArrow.element(boundBy: 0) - + let firstRocketCell = rocketCellArrows.element(boundBy: 0) firstRocketCell.tap() return self From 688eec0d01861f6b5fe574ab51d286cae3ff319f Mon Sep 17 00:00:00 2001 From: Cyril Colombier <> Date: Tue, 10 Dec 2024 18:00:07 +0100 Subject: [PATCH 17/21] Fix whitespace trailing --- Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift | 1 - .../iOS/RocketAppUITests/Screens/RocketDetailScreen.swift | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift b/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift index 4eeedc94..d902744f 100644 --- a/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/LoginScreen.swift @@ -44,5 +44,4 @@ struct LoginScreen: Screen { loginButton.tap() return self } - } diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift index d18b7d4e..31ed1cf0 100644 --- a/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/RocketDetailScreen.swift @@ -20,13 +20,10 @@ struct RocketDetailScreen: Screen { XCTAssert(rocketTitle.waitForExistence(timeout: 5), "Rocket title did not appear within the timeout.") return self } + @discardableResult func tapLaunchButton() -> Self { launchButton.tap() return self } - - - - } From 010a8847f7968f96d983183d9beca1a79c305049 Mon Sep 17 00:00:00 2001 From: Cyril Colombier <> Date: Wed, 11 Dec 2024 16:12:16 +0100 Subject: [PATCH 18/21] Add checkRocketNames test --- .../AccessibilityKeys+List.swift | 1 + .../RocketListCell/RocketListCellView.swift | 1 + .../Screens/RocketListScreen.swift | 24 +++++++++++++------ .../Tests/RocketLaunchTest.swift | 1 + 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketListCell/AccessibilityKeys+List.swift b/Solution/iOS/RocketApp/Features/Sources/RocketListCell/AccessibilityKeys+List.swift index 41848a62..70cf1276 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketListCell/AccessibilityKeys+List.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketListCell/AccessibilityKeys+List.swift @@ -3,5 +3,6 @@ import UIToolkit extension AccessibilityKeys { public enum RocketListCell { public static let rocketCellArrow = "rocketCellArrowID" + public static let rocketNameLabel = "rocketNameLabelID" } } diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketListCell/RocketListCellView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketListCell/RocketListCellView.swift index a37703b7..b677f210 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketListCell/RocketListCellView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketListCell/RocketListCellView.swift @@ -23,6 +23,7 @@ public struct RocketListCellView: View { VStack(alignment: .leading, spacing: 4) { Text(viewStore.rocketData.name) .font(.title2.bold()) + .accessibilityIdentifier(AccessibilityKeys.RocketListCell.rocketNameLabel) Text("First flight: \(viewStore.rocketData.firstFlight)") .font(.callout) diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift index cbbeb309..8216a86c 100644 --- a/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift @@ -7,31 +7,41 @@ struct RocketListScreen: Screen { let app: XCUIApplication private let rocketCells: XCUIElementQuery + private let rocketNameLabels: XCUIElementQuery private let rocketCellArrows: XCUIElementQuery init(app: XCUIApplication) { self.app = app - + rocketCells = app.buttons.matching(identifier: AccessibilityKeys.RocketList.rocketCell) + rocketNameLabels = app.staticTexts.matching(identifier: AccessibilityKeys.RocketListCell.rocketNameLabel) rocketCellArrows = app.images.matching(identifier: AccessibilityKeys.RocketListCell.rocketCellArrow) } @discardableResult func countRocketCells() -> Self { - let cellCount = rocketCells.count - XCTAssert(rocketCells.firstMatch.waitForExistence(timeout: 5), "Rocket cells did not appear within the timeout.") - print("Number of rocket cells displayed: \(cellCount)") - XCTAssertEqual(cellCount, 4, "Expected exactly 4 rocket cells to be displayed.") - + print("Number of rocket cells displayed: \(rocketCells.count)") + XCTAssert(rocketCells.count == 4, "Expected exactly 4 rocket cells to be displayed.") return self } + @discardableResult + func checkRocketNames() -> Self { + let expectedRocketNames = ["Falcon 1", "Falcon 9", "Falcon Heavy", "Starship"] + for index in 0.. Self { let firstRocketCell = rocketCellArrows.element(boundBy: 0) firstRocketCell.tap() - return self } } diff --git a/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift b/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift index 2e37ad59..ce5df50e 100644 --- a/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift +++ b/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift @@ -12,6 +12,7 @@ final class TestRocketLaunch: BaseTestCase { .tapLoginButton() RocketListScreen(app: app) .countRocketCells() + .checkRocketNames() .tapFirstRocketCell() RocketDetailScreen(app: app) .checkRocketTitle() From ab20d51a11100050af9dd83f0fe5a6e4846ff9f6 Mon Sep 17 00:00:00 2001 From: Cyril Colombier <> Date: Wed, 11 Dec 2024 16:12:39 +0100 Subject: [PATCH 19/21] Fix naming and spacing --- .../RocketApp/Features/Sources/RocketList/RocketListView.swift | 2 +- Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift index 3d1bb9d9..f7d4bcf6 100644 --- a/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift +++ b/Solution/iOS/RocketApp/Features/Sources/RocketList/RocketListView.swift @@ -59,7 +59,7 @@ public struct RocketListView: View { ) ) { RocketListCellView(store: $0) - .accessibilityIdentifier(AccessibilityKeys.RocketList.rocketCells) + .accessibilityIdentifier(AccessibilityKeys.RocketList.rocketCell) } } .listStyle(.sidebar) diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift index ff81d5d5..166c2b37 100644 --- a/Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/RocketLaunchScreen.swift @@ -11,7 +11,6 @@ struct RocketLaunchScreen: Screen { init(app: XCUIApplication) { self.app = app rocketImage = app.images[AccessibilityKeys.RocketLaunch.rocketImage] - } @discardableResult From c50c57bc2845265bb699a1da1239e7633b10ba69 Mon Sep 17 00:00:00 2001 From: Cyril Colombier <> Date: Tue, 17 Dec 2024 15:04:35 +0100 Subject: [PATCH 20/21] Add separate test constants file --- .../iOS/RocketAppUITests/Tests/RocketLaunchTest.swift | 10 ++++------ .../iOS/RocketAppUITests/Utils/TestConstants.swift | 11 +++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 Solution/iOS/RocketAppUITests/Utils/TestConstants.swift diff --git a/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift b/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift index ce5df50e..30dee42d 100644 --- a/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift +++ b/Solution/iOS/RocketAppUITests/Tests/RocketLaunchTest.swift @@ -2,17 +2,15 @@ import XCTest final class TestRocketLaunch: BaseTestCase { func testRocketLaunchTest() { - let username = "astronaut1" - let password = "space" - + LoginScreen(app: app) .checkLoginTitle() - .enterUsername(username) - .enterPassword(password) + .enterUsername(TestConstants.LoginDetails.username) + .enterPassword(TestConstants.LoginDetails.password) .tapLoginButton() RocketListScreen(app: app) .countRocketCells() - .checkRocketNames() + .checkRocketNames(TestConstants.Rockets.rocketNames) .tapFirstRocketCell() RocketDetailScreen(app: app) .checkRocketTitle() diff --git a/Solution/iOS/RocketAppUITests/Utils/TestConstants.swift b/Solution/iOS/RocketAppUITests/Utils/TestConstants.swift new file mode 100644 index 00000000..da679f55 --- /dev/null +++ b/Solution/iOS/RocketAppUITests/Utils/TestConstants.swift @@ -0,0 +1,11 @@ +import Foundation + +enum TestConstants { + enum LoginDetails{ + public static let username = "astronaut1" + public static let password = "space" + } + enum Rockets { + public static let rocketNames = ["Falcon 1", "Falcon 9", "Falcon Heavy", "Starship"] + } +} From f1efe9b54fd569963dbceb813aba00fb66639b19 Mon Sep 17 00:00:00 2001 From: Cyril Colombier <> Date: Tue, 17 Dec 2024 15:05:48 +0100 Subject: [PATCH 21/21] Fix checkRocketNames test --- .../Screens/RocketListScreen.swift | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift index 8216a86c..d764164a 100644 --- a/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift +++ b/Solution/iOS/RocketAppUITests/Screens/RocketListScreen.swift @@ -7,14 +7,14 @@ struct RocketListScreen: Screen { let app: XCUIApplication private let rocketCells: XCUIElementQuery - private let rocketNameLabels: XCUIElementQuery + private let rocketNames: XCUIElementQuery private let rocketCellArrows: XCUIElementQuery init(app: XCUIApplication) { self.app = app rocketCells = app.buttons.matching(identifier: AccessibilityKeys.RocketList.rocketCell) - rocketNameLabels = app.staticTexts.matching(identifier: AccessibilityKeys.RocketListCell.rocketNameLabel) + rocketNames = app.staticTexts.matching(identifier: AccessibilityKeys.RocketListCell.rocketNameLabel) rocketCellArrows = app.images.matching(identifier: AccessibilityKeys.RocketListCell.rocketCellArrow) } @@ -24,16 +24,17 @@ struct RocketListScreen: Screen { print("Number of rocket cells displayed: \(rocketCells.count)") XCTAssert(rocketCells.count == 4, "Expected exactly 4 rocket cells to be displayed.") return self - } + } @discardableResult - func checkRocketNames() -> Self { - let expectedRocketNames = ["Falcon 1", "Falcon 9", "Falcon Heavy", "Starship"] - for index in 0.. Self { + for index in 0..