diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 15799f7..3c98e02 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -70,7 +70,7 @@ dependencies { androidTestImplementation(project(":cucumberShared")) androidTestImplementation("io.cucumber:cucumber-android:4.10.0") - // TODO figure out how it can be updated without breeaking the project + // TODO figure out how it can be updated without breaking the project androidTestImplementation("io.cucumber:cucumber-java8:4.8.1") implementation("io.insert-koin:koin-androidx-compose:3.4.1") diff --git a/android/src/androidTest/assets/features/Home.feature b/android/src/androidTest/assets/features/Home.feature index 177cb42..02a1f5b 100644 --- a/android/src/androidTest/assets/features/Home.feature +++ b/android/src/androidTest/assets/features/Home.feature @@ -1,8 +1,8 @@ Feature: Home - Scenario: Home screen - Given Email is "test@test.com" - Given I am in the "Home" screen - Then I see "test@test.com" text + + Scenario: Logout + Given I see the "Home screen" screen + Then I see the "test@test.com" text Then I see the "Logout" button - Then I press the logout button - Then I see the "Login" screen + Then I press the "Logout" button + Then I see the "Login screen" text \ No newline at end of file diff --git a/android/src/androidTest/assets/features/Login.feature b/android/src/androidTest/assets/features/Login.feature index ec36c05..d414320 100644 --- a/android/src/androidTest/assets/features/Login.feature +++ b/android/src/androidTest/assets/features/Login.feature @@ -1,11 +1,31 @@ Feature: Login - Scenario: Login screen - Given I am in the "Login" screen + + # will run before each scenario + Background: + Given I see the "Login screen" screen Then I see the "Email" textfield with text "Email" - Then I type "test@test.com" in the email field - Then I see the "Password" textfield with text "Password" - Then I type "1234" in the password field - Then I see the "Login" button - Then I press the login button - Then I see the "Home" screen - Then I see "test@test.com" text + And I see the "Password" textfield with text "Password" + And I see the "Login" button + + Scenario: Failed attempt with wrong credentials + Then I type "test@test.fail" in the "Email" field + And I type "1234" in the "Password" field + And I press the "Login" button + And I see the "Incorrect email or password" text + + Scenario: Failed attempt with empty email + Then I type "1234" in the "Password" field + And I press the "Login" button + And I see the "Missing email" text + + Scenario: Failed attempt with empty password + Then I type "alex@alex" in the "Email" field + Then I press the "Login" button + Then I see the "Missing password" text + + Scenario: Successful attempt + Then I type "test@test.com" in the "Email" field + Then I type "1234" in the "Password" field + Then I press the "Login" button + Then I see the "Home screen" text + Then I see the "test@test.com" text \ No newline at end of file diff --git a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/CommonStepDefinitions.kt b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/CommonStepDefinitions.kt new file mode 100644 index 0000000..440e675 --- /dev/null +++ b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/CommonStepDefinitions.kt @@ -0,0 +1,118 @@ +package com.corrado4eyes.cucumberplayground.test + +import android.content.Intent +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextContains +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.core.app.ActivityScenario +import androidx.test.platform.app.InstrumentationRegistry +import com.corrado4eyes.cucumber.GherkinLambda1 +import com.corrado4eyes.cucumber.GherkinLambda2 +import com.corrado4eyes.cucumberplayground.android.MainActivity +import com.corrado4eyes.cucumbershared.tests.TestCase +import io.cucumber.java.After +import io.cucumber.java.AfterStep +import io.cucumber.java.Before +import io.cucumber.java.BeforeStep +import io.cucumber.java8.En +import io.cucumber.junit.WithJunitRule +import org.junit.Rule + +@WithJunitRule +class CommonStepDefinitions : En { + + private val arguments = mutableMapOf() + private var scenario: ActivityScenario<*>? = null + + @get:Rule(order = 0) + val testRule = createComposeRule() + + @Before(order = 0) + fun beforeScenarioStart(scenario: io.cucumber.core.api.Scenario) { + // Will run before each scenario + println("-----------------Start of Scenario ${scenario.name}-----------------") + when (val scenarioName = scenario.name) { + "Failed attempt with wrong credentials", + "Failed attempt with empty email", + "Failed attempt with empty password", + "Successful attempt" -> { + arguments["isLoggedIn"] = "false" + arguments["testEmail"] = "" + } + "Logout" -> { + arguments["isLoggedIn"] = "true" + arguments["testEmail"] = "test@test.com" + } + + else -> throw IllegalArgumentException("Couldn't find scenario: $scenarioName ") + } + launchApp() + } + + @After(order = 0) + fun afterScenarioFinish(cucuScenario: io.cucumber.core.api.Scenario) { + // Will run after each scenario but first in order + println("-----------------End of Scenario ${cucuScenario.name}-----------------") + } + + // TODO figure out for specific step, crashes the app with exception if you try @BeforeStep("some step") + @BeforeStep(order = 0) + fun beforeStep(scenario: io.cucumber.core.api.Scenario) { + // Run stuff before each scenario step + } + + @AfterStep(order = 0) + fun afterStep(scenario: io.cucumber.core.api.Scenario) { + // Run stuff after each scenario step + } + + init { + TestCase.Common.TextIsVisible( + GherkinLambda1 { + testRule.onNodeWithText(it).assertIsDisplayed() + } + ) + TestCase.Common.ScreenIsVisible( + // TODO consider using UI tags for this as it is not necessary the view will contain text + GherkinLambda1 { + testRule.onNodeWithTag(it).assertIsDisplayed() + } + ) + TestCase.Common.ButtonIsVisible( + GherkinLambda1 { + testRule.onNodeWithText(it).assertIsDisplayed().assertHasClickAction() + } + ) + // TODO make into generic view is visible with tag is swift ui supports it + TestCase.Common.TextFieldIsVisible( + GherkinLambda2 { tag, text -> + testRule.onNodeWithTag(tag).assertIsDisplayed().assertTextContains(text) + } + ) + TestCase.Common.FillTextField( + GherkinLambda2 { input, tag -> + testRule.onNodeWithTag(tag).assertIsDisplayed().performTextInput(input) + } + ) + TestCase.Common.PressButton( + GherkinLambda1 { + testRule.onNodeWithTag(it).assertIsDisplayed().performClick() + } + ) + } + + private fun launchApp() { + // TODO figure out if destroying activity is needed after each test (try reordering Login.Feature scenarios to get into situations where this matters) + val instrumentation = InstrumentationRegistry.getInstrumentation() + scenario = ActivityScenario.launch( + Intent(instrumentation.targetContext, MainActivity::class.java) + .putExtra("isLoggedIn", arguments["isLoggedIn"]) + .putExtra("testEmail", arguments["testEmail"]) + ) + } +} diff --git a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt deleted file mode 100644 index 918eede..0000000 --- a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.corrado4eyes.cucumberplayground.test - -import android.app.Activity -import android.content.Intent -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertTextContains -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput -import androidx.test.core.app.ActivityScenario -import androidx.test.platform.app.InstrumentationRegistry -import com.corrado4eyes.cucumber.GherkinLambda0 -import com.corrado4eyes.cucumber.GherkinLambda1 -import com.corrado4eyes.cucumber.GherkinLambda2 -import com.corrado4eyes.cucumberplayground.android.MainActivity -import com.corrado4eyes.cucumbershared.tests.TestCase -import io.cucumber.java8.En -import io.cucumber.junit.WithJunitRule -import org.junit.Rule - -@WithJunitRule -class StepDefinitions : En { - - private val arguments = mutableMapOf() - private var scenario: ActivityScenario<*>? = null - - @get:Rule(order = 0) - val testRule = createComposeRule() - - init { - TestCase.Common.ScreenIsVisible( - GherkinLambda1 { screenName -> - val screenTitleTag = when (screenName) { - "Login" -> { - arguments["isLoggedIn"] = "false" - arguments["testEmail"] = "" - "Login screen" - } - "Home" -> { - arguments["isLoggedIn"] = "true" - "Home screen" - } - - else -> throw IllegalArgumentException("Couldn't find any $screenName screen") - } - setLaunchScreen() - testRule.onNodeWithTag(screenTitleTag).assertIsDisplayed() - } - ) - TestCase.Common.TitleIsVisible( - GherkinLambda1 { - val title = it - testRule.onNodeWithText(title).assertIsDisplayed() - } - ) - - TestCase.Common.ButtonIsVisible( - GherkinLambda1 { - when (it) { - "Login" -> testRule.onNodeWithText("Login").assertIsDisplayed().assertHasClickAction() - "Logout" -> testRule.onNodeWithTag("Logout").assertIsDisplayed().assertHasClickAction() - else -> throw IllegalArgumentException("Couldn't find $it button") - } - } - ) - TestCase.Common.NavigateToScreen( - GherkinLambda1 { - Thread.sleep(1000) - when (it) { - "Login" -> testRule.onNodeWithTag("Login screen").assertIsDisplayed() - "Home" -> testRule.onNodeWithTag("Home screen").assertIsDisplayed() - } - } - ) - TestCase.Login.Common.TextFieldIsVisible( - GherkinLambda2 { tag, text -> - testRule.onNodeWithTag(tag).assertIsDisplayed().assertTextContains(text) - } - ) - TestCase.Login.FillEmailTextField( - GherkinLambda1 { - testRule.onNodeWithText("Email").performTextInput(it) - } - ) - TestCase.Login.FillPasswordTextField( - GherkinLambda1 { - testRule.onNodeWithText("Password").performTextInput(it) - } - ) - TestCase.Login.PressLoginButton( - GherkinLambda0 { - testRule.onNodeWithText("Login").assertIsDisplayed().performClick() - } - ) - TestCase.Home.PressLogoutButton( - GherkinLambda0 { - testRule.onNodeWithTag("Logout").assertIsDisplayed().performClick() - } - ) - TestCase.Home.LoggedInEmail( - GherkinLambda1 { - arguments["testEmail"] = "test@test.com" - } - ) - } - - private fun setLaunchScreen() { - val instrumentation = InstrumentationRegistry.getInstrumentation() - launch( - Intent(instrumentation.targetContext, MainActivity::class.java) - .putExtra("isLoggedIn", arguments["isLoggedIn"]) - .putExtra("testEmail", arguments["testEmail"]) - ) - } - - private fun launch(intent: Intent) { - scenario = ActivityScenario.launch(intent) - } -} \ No newline at end of file diff --git a/android/src/main/java/com/corrado4eyes/cucumberplayground/android/login/LoginLayout.kt b/android/src/main/java/com/corrado4eyes/cucumberplayground/android/login/LoginLayout.kt index e241528..b4a6f91 100644 --- a/android/src/main/java/com/corrado4eyes/cucumberplayground/android/login/LoginLayout.kt +++ b/android/src/main/java/com/corrado4eyes/cucumberplayground/android/login/LoginLayout.kt @@ -26,18 +26,21 @@ fun LoginLayout() { ViewModelComposable(viewModel) { val isLoading by this.isLoading.state() Column { - Text(text = this@ViewModelComposable.screenTitle, modifier = Modifier.testTag("Login screen")) + Text( + text = this@ViewModelComposable.screenTitle, + modifier = Modifier.testTag("Login screen") + ) CustomTextField( value = this@ViewModelComposable.emailText, - label = "Email", - modifier = Modifier.testTag("Email") + label = viewModel.emailPlaceholder, + modifier = Modifier.testTag(viewModel.emailPlaceholder) ) val emailErrorText by this@ViewModelComposable.emailErrorText.state() Text(text = emailErrorText, color = Color.Red) CustomTextField( value = this@ViewModelComposable.passwordText, - label = "Password", - modifier = Modifier.testTag("Password") + label = viewModel.passwordPlaceholder, + modifier = Modifier.testTag(viewModel.passwordPlaceholder) ) val passwordErrorText by this@ViewModelComposable.passwordErrorText.state() Text(text = passwordErrorText, color = Color.Red) @@ -48,7 +51,10 @@ fun LoginLayout() { CircularProgressIndicator() } - Button(this@ViewModelComposable::login) { + Button( + onClick = this@ViewModelComposable::login, + modifier = Modifier.testTag("Login") + ) { Text("Login") } } diff --git a/cucumber/src/commonMain/kotlin/com/corrado4eyes/cucumber/CucumberDefinition.kt b/cucumber/src/commonMain/kotlin/com/corrado4eyes/cucumber/CucumberDefinition.kt index c37df4e..db1ca93 100644 --- a/cucumber/src/commonMain/kotlin/com/corrado4eyes/cucumber/CucumberDefinition.kt +++ b/cucumber/src/commonMain/kotlin/com/corrado4eyes/cucumber/CucumberDefinition.kt @@ -157,8 +157,8 @@ sealed class CucumberDefinition(val regex: String, execute: () -> Unit = {}): Ba } } -interface GherkinTestCase { - val step: D +interface GherkinTestCase { + val step: CucumberDefinition val lambda: L } diff --git a/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt b/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt index ad20827..b9b1cdc 100644 --- a/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt +++ b/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt @@ -1,57 +1,44 @@ package com.corrado4eyes.cucumbershared.tests import com.corrado4eyes.cucumber.CucumberDefinition -import com.corrado4eyes.cucumber.Definition import com.corrado4eyes.cucumber.EXPECT_VALUE_STRING import com.corrado4eyes.cucumber.GherkinLambda -import com.corrado4eyes.cucumber.GherkinLambda0 import com.corrado4eyes.cucumber.GherkinLambda1 import com.corrado4eyes.cucumber.GherkinLambda2 import com.corrado4eyes.cucumber.GherkinTestCase -sealed class TestCase : GherkinTestCase { - sealed class Common : TestCase() { - class ScreenIsVisible(override val lambda: GherkinLambda1) : Common() { - override val step: CucumberDefinition.Step.GivenSingle = CucumberDefinition.Step.GivenSingle("I am in the $EXPECT_VALUE_STRING screen", lambda) - } +sealed class TestCase : GherkinTestCase { + sealed class Common : TestCase() { - class TitleIsVisible(override val lambda: GherkinLambda1) : Common() { - override val step: CucumberDefinition.Step.ThenSingle = CucumberDefinition.Step.ThenSingle("I see $EXPECT_VALUE_STRING text", lambda) + class TextIsVisible(override val lambda: GherkinLambda1) : Common() { + override val step: CucumberDefinition = CucumberDefinition.Step.ThenSingle("I see the $EXPECT_VALUE_STRING text", lambda) } - class ButtonIsVisible(override val lambda: GherkinLambda1) : Common() { - override val step: CucumberDefinition.Step.ThenSingle = CucumberDefinition.Step.ThenSingle("I see the $EXPECT_VALUE_STRING button", lambda) + class ScreenIsVisible(override val lambda: GherkinLambda1) : Common() { + override val step: CucumberDefinition = CucumberDefinition.Step.GivenSingle("I see the $EXPECT_VALUE_STRING screen", lambda) } - class NavigateToScreen(override val lambda: GherkinLambda1) : Common() { - override val step: CucumberDefinition.Step.ThenSingle = CucumberDefinition.Step.ThenSingle("I see the $EXPECT_VALUE_STRING screen", lambda) + class ButtonIsVisible(override val lambda: GherkinLambda1) : Common() { + override val step: CucumberDefinition = CucumberDefinition.Step.ThenSingle("I see the $EXPECT_VALUE_STRING button", lambda) } - } - sealed class Login : TestCase() { - sealed class Common : Login() { - class TextFieldIsVisible(override val lambda: GherkinLambda2) : Common() { - override val step: CucumberDefinition.Step.ThenMultiple = CucumberDefinition.Step.ThenMultiple("I see the $EXPECT_VALUE_STRING textfield with text $EXPECT_VALUE_STRING", lambda) - } + class PressButton(override val lambda: GherkinLambda1): Common() { + override val step: CucumberDefinition = CucumberDefinition.Step.ThenSingle("I press the $EXPECT_VALUE_STRING button", lambda) } - class FillEmailTextField(override val lambda: GherkinLambda1) : Login() { - override val step: CucumberDefinition.Step.ThenSingle = CucumberDefinition.Step.ThenSingle("I type $EXPECT_VALUE_STRING in the email field", lambda) + + class TextFieldIsVisible(override val lambda: GherkinLambda2) : Common() { + override val step: CucumberDefinition = CucumberDefinition.Step.ThenMultiple("I see the $EXPECT_VALUE_STRING textfield with text $EXPECT_VALUE_STRING", lambda) } - class FillPasswordTextField(override val lambda: GherkinLambda1) : Login() { - override val step: CucumberDefinition.Step.ThenSingle = CucumberDefinition.Step.ThenSingle("I type $EXPECT_VALUE_STRING in the password field", lambda) + class FillTextField(override val lambda: GherkinLambda2) : Common() { + override val step: CucumberDefinition = CucumberDefinition.Step.ThenMultiple("I type $EXPECT_VALUE_STRING in the $EXPECT_VALUE_STRING field", lambda) } - class PressLoginButton(override val lambda: GherkinLambda0) : Login() { - override val step: CucumberDefinition.Step.Then = CucumberDefinition.Step.Then("I press the login button", lambda) + class FillPasswordTextField(override val lambda: GherkinLambda2) : Common() { + override val step: CucumberDefinition = CucumberDefinition.Step.ThenMultiple("I type $EXPECT_VALUE_STRING in the password field called $EXPECT_VALUE_STRING", lambda) } - } - sealed class Home : TestCase() { - class LoggedInEmail(override val lambda: GherkinLambda1) : Home() { + class LoggedInEmail(override val lambda: GherkinLambda1) : Common() { override val step: CucumberDefinition.Step.GivenSingle = CucumberDefinition.Step.GivenSingle("Email is $EXPECT_VALUE_STRING", lambda) } - class PressLogoutButton(override val lambda: GherkinLambda0) : Login() { - override val step: CucumberDefinition.Step.Then = CucumberDefinition.Step.Then("I press the logout button", lambda) - } } } \ No newline at end of file diff --git a/ios/CucumberTests/CucumberTests.swift b/ios/CucumberTests/CucumberTests.swift index 0110e27..2aa9c6b 100644 --- a/ios/CucumberTests/CucumberTests.swift +++ b/ios/CucumberTests/CucumberTests.swift @@ -21,15 +21,16 @@ import Cucumberish app.launchArguments.append("test") } + TestCaseCommonScreenIsVisible { args, userInfo in guard let screenName = args?[0] as? String else { return KotlinUnit() } let text: XCUIElement switch(screenName) { - case "Home": + case "Home screen": app.launchEnvironment["isLoggedIn"] = "true" text = app.staticTexts["Home screen"] - case "Login": + case "Login screen": app.launchEnvironment["isLoggedIn"] = "false" text = app.staticTexts["Login screen"] @@ -40,18 +41,18 @@ import Cucumberish app.launch() - XCTAssert(text.exists(timeout: .short), "Couldn't validate to be in \(screenName)") + XCTAssert(text.exists(timeout: .long), "Couldn't validate to be in \(screenName)") return KotlinUnit() } - TestCaseCommonTitleIsVisible { args, userInfo in + TestCaseCommonTextIsVisible { args, userInfo in guard let textString = args?[0] as? String else { return KotlinUnit() } let text = app.staticTexts[textString] XCTAssert(text.exists(timeout: .short), "Couldn't find \(text) text") return KotlinUnit() } - TestCaseLoginCommonTextFieldIsVisible { args, userInfo in + TestCaseCommonTextFieldIsVisible { args, userInfo in guard let textfieldName = args?[0] as? String else { return KotlinUnit() } guard let textfieldText = args?[1] as? String else { return KotlinUnit() } let textfield: XCUIElement? = { @@ -73,15 +74,16 @@ import Cucumberish return KotlinUnit() } - TestCaseLoginFillEmailTextField { args, userInfo in - guard let email = args?[0] as? String else { return KotlinUnit() } - let textfield = app.textFields["Email"] + TestCaseCommonFillTextField { args, userInfo in + guard let textFieldText = args?[1] as? String else { return KotlinUnit() } + guard let text = args?[0] as? String else { return KotlinUnit() } + let textfield = app.textFields[textFieldText] textfield.tap() - textfield.typeText(email) + textfield.typeText(text) return KotlinUnit() } - TestCaseLoginFillPasswordTextField { args, userInfo in + TestCaseCommonFillPasswordTextField { args, userInfo in guard let password = args?[0] as? String else { return KotlinUnit() } let textfield = app.secureTextFields["Password"] textfield.tap() @@ -96,20 +98,21 @@ import Cucumberish return KotlinUnit() } - TestCaseLoginPressLoginButton { args, userInfo in - let button = app.buttons["Login"] - let link = app.links["Login"] + TestCaseCommonPressButton { args, userInfo in + guard let buttonString = args?[0] as? String else { return KotlinUnit() } + let button = app.buttons[buttonString] + let link = app.links[buttonString] if button.exists(timeout: .medium) && button.isEnabled(timeout: .short) { button.tap() } else if link.exists(timeout: .medium) && link.isEnabled(timeout: .short) { link.tap() } else { - XCTFail("I press Login button failed") + XCTFail("I press \(buttonString) button failed") } return KotlinUnit() } - TestCaseHomeLoggedInEmail { args, userInfo in + TestCaseCommonLoggedInEmail { args, userInfo in guard let email = args?[0] as? String else { return KotlinUnit() } app.launchEnvironment["testEmail"] = email return KotlinUnit() @@ -129,19 +132,6 @@ import Cucumberish return KotlinUnit() } - TestCaseHomePressLogoutButton { args, userInfo in - let button = app.buttons["Logout"] - let link = app.links["Logout"] - if button.exists(timeout: .medium) && button.isEnabled(timeout: .short) { - button.tap() - } else if link.exists(timeout: .medium) && link.isEnabled(timeout: .short) { - link.tap() - } else { - XCTFail("I press Logout button failed") - } - return KotlinUnit() - } - let bundle = Bundle(for: CucumberishInitializer.self) Cucumberish.executeFeatures(inDirectory: "Features", from: bundle, includeTags: nil, excludeTags: ["ignore"]) } diff --git a/ios/CucumberTests/Features/Home.feature b/ios/CucumberTests/Features/Home.feature index 177cb42..fe405e7 100644 --- a/ios/CucumberTests/Features/Home.feature +++ b/ios/CucumberTests/Features/Home.feature @@ -1,8 +1,8 @@ Feature: Home Scenario: Home screen Given Email is "test@test.com" - Given I am in the "Home" screen - Then I see "test@test.com" text + Given I see the "Home screen" screen + Then I see the "test@test.com" text Then I see the "Logout" button - Then I press the logout button + Then I press the "Logout" button Then I see the "Login" screen diff --git a/ios/CucumberTests/Features/Login.feature b/ios/CucumberTests/Features/Login.feature index ec36c05..1259d1a 100644 --- a/ios/CucumberTests/Features/Login.feature +++ b/ios/CucumberTests/Features/Login.feature @@ -1,11 +1,31 @@ Feature: Login - Scenario: Login screen - Given I am in the "Login" screen + + # will run before each scenario + Background: + Given I see the "Login screen" screen Then I see the "Email" textfield with text "Email" - Then I type "test@test.com" in the email field Then I see the "Password" textfield with text "Password" - Then I type "1234" in the password field Then I see the "Login" button - Then I press the login button - Then I see the "Home" screen - Then I see "test@test.com" text + + Scenario: Failed attempt with wrong credentials + Then I type "test@test.fail" in the "Email" field + Then I type "1234" in the "Password" field + Then I press the "Login" button + Then I see the "Incorrect email or password" text + + Scenario: Failed attempt with empty email + Then I type "1234" in the "Password" field + Then I press the "Login" button + Then I see the "Missing email" text + + Scenario: Failed attempt with empty password + Then I type "alex@alex" in the "Email" field + Then I press the "Login" button + Then I see the "Missing password" text + + Scenario: Successful attempt + Then I type "test@test.com" in the "Email" field + Then I type "1234" in the "Password" field + Then I press the "Login" button + Then I see the "Home screen" text + Then I see the "test@test.com" text \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index 07525ae..428af09 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -3,3 +3,11 @@ target 'CucumberTests' do platform :ios, '14.1' pod 'Cucumberish' end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = Gem::Version.new('14.1') + end + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5d198fb..a37a092 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -11,6 +11,6 @@ SPEC REPOS: SPEC CHECKSUMS: Cucumberish: 6cbd0c1f50306b369acebfe7d9f514c9c287d26c -PODFILE CHECKSUM: 628c667357c176acb30372b1f5ac2574ef0cf182 +PODFILE CHECKSUM: a416e91f0850ff41496968699d0a5073dfa8a80f -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 diff --git a/ios/ios.xcodeproj/project.pbxproj b/ios/ios.xcodeproj/project.pbxproj index 0323218..584cf1f 100644 --- a/ios/ios.xcodeproj/project.pbxproj +++ b/ios/ios.xcodeproj/project.pbxproj @@ -398,10 +398,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-CucumberTests/Pods-CucumberTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-CucumberTests/Pods-CucumberTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-CucumberTests/Pods-CucumberTests-frameworks.sh\"\n"; @@ -555,7 +559,7 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = NHQ559J67Q; + DEVELOPMENT_TEAM = AQA8K92RVA; FRAMEWORK_SEARCH_PATHS = ( "\"$(PLATFORM_DIR)/Developer/Library/Frameworks\"", "$(inherited)", @@ -596,7 +600,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = NHQ559J67Q; + DEVELOPMENT_TEAM = AQA8K92RVA; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(FRAMEWORK_SEARCH_PATHS)", @@ -748,6 +752,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"ios/Preview Content\""; + DEVELOPMENT_TEAM = AQA8K92RVA; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -764,7 +769,7 @@ "-framework", shared, ); - PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.ios; + PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.ios.alex; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -777,6 +782,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"ios/Preview Content\""; + DEVELOPMENT_TEAM = AQA8K92RVA; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; INFOPLIST_FILE = ios/Info.plist; @@ -789,7 +795,7 @@ "-framework", shared, ); - PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.ios; + PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.ios.alex; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/ios/ios/ContentView.swift b/ios/ios/ContentView.swift index 4a5d865..fe4efff 100644 --- a/ios/ios/ContentView.swift +++ b/ios/ios/ContentView.swift @@ -27,12 +27,13 @@ struct ContentView: View { viewModel = LifecycleViewModel(mainViewModel) navState = ObjectObservable(mainViewModel.navState) } + var body: some View { NavigationView { if navState.value is AppNavigator.Home { - HomeView(user: (navState.value as! AppNavigator.Home).user, authService: authService) + HomeView() } else if navState.value is AppNavigator.Login{ - LoginView(authService: authService) + LoginView() } else { ProgressView() } diff --git a/ios/ios/Info.plist b/ios/ios/Info.plist index 8044709..9a269f5 100644 --- a/ios/ios/Info.plist +++ b/ios/ios/Info.plist @@ -25,6 +25,8 @@ UIApplicationSupportsMultipleScenes + UILaunchScreen + UIRequiredDeviceCapabilities armv7 @@ -42,7 +44,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UILaunchScreen - - \ No newline at end of file + diff --git a/ios/ios/Screens/Home/HomeView.swift b/ios/ios/Screens/Home/HomeView.swift index fc557f0..d20c7c7 100644 --- a/ios/ios/Screens/Home/HomeView.swift +++ b/ios/ios/Screens/Home/HomeView.swift @@ -11,9 +11,16 @@ import shared struct HomeView: SwiftUI.View { private let viewModel: LifecycleViewModel + private let containerView = ContainerView(.alert, .hud) - init(user: User, authService: AuthService) { - viewModel = LifecycleViewModel(HomeViewModel()) + init() { + viewModel = LifecycleViewModel( + HomeViewModel( + alertPresenterBuilder: containerView.alertBuilder, + hudBuilder: containerView.hudBuilder + ), + containerView: containerView + ) } var body: some View { @@ -29,6 +36,7 @@ struct HomeView: SwiftUI.View { }.toolbar { ToolbarItem(placement: .principal) { Text(viewModel.screenTitle) + .accessibilityLabel(viewModel.screenTitle) } }.navigationBarTitleDisplayMode(.inline) } diff --git a/ios/ios/Screens/Login/LoginView.swift b/ios/ios/Screens/Login/LoginView.swift index 69ab862..63c58f2 100644 --- a/ios/ios/Screens/Login/LoginView.swift +++ b/ios/ios/Screens/Login/LoginView.swift @@ -22,7 +22,7 @@ struct LoginView: SwiftUI.View { private let viewModel: LifecycleViewModel - init(authService: AuthService) { + init() { let loginViewModel = LoginViewModel() viewModel = LifecycleViewModel(loginViewModel) emailText = StringSubject(loginViewModel.emailText) @@ -67,6 +67,7 @@ struct LoginView: SwiftUI.View { .toolbar { ToolbarItem(placement: .principal) { Text(viewModel.screenTitle) + .accessibilityLabel(viewModel.screenTitle) } }.navigationBarTitleDisplayMode(.inline) } diff --git a/ios/kaluga.sourcery.yml b/ios/kaluga.sourcery.yml index 4ea1618..3ce9529 100644 --- a/ios/kaluga.sourcery.yml +++ b/ios/kaluga.sourcery.yml @@ -15,8 +15,7 @@ args: sharedFrameworkName: shared includeResources: true includeAlerts: true - includeHud: false + includeHud: true includeDatePicker: false - includeKeyboard: true includePartialSheet: false includeKeyboard: false diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 461ee2d..b9cadf6 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -17,6 +17,7 @@ kotlin { val target: org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget.() -> Unit = { binaries { framework { + transitiveExport = true baseName = "shared" export("com.splendo.kaluga:alerts:$kalugaVersion") @@ -62,7 +63,6 @@ kotlin { val commonTest by getting { dependencies { api(kotlin("test")) - api(kotlin("test-junit")) api("com.splendo.kaluga:test-utils:$kalugaVersion") api("io.insert-koin:koin-test:$koinVersion") } diff --git a/shared/src/androidMain/kotlin/com/corrado4eyes/cucumberplayground/di/CucumberDependencyInjection.kt b/shared/src/androidMain/kotlin/com/corrado4eyes/cucumberplayground/di/CucumberDependencyInjection.kt index 28bc1b4..4fbcda6 100644 --- a/shared/src/androidMain/kotlin/com/corrado4eyes/cucumberplayground/di/CucumberDependencyInjection.kt +++ b/shared/src/androidMain/kotlin/com/corrado4eyes/cucumberplayground/di/CucumberDependencyInjection.kt @@ -6,7 +6,12 @@ import com.corrado4eyes.cucumberplayground.models.TestConfiguration import com.corrado4eyes.cucumberplayground.viewModels.home.HomeViewModel import com.corrado4eyes.cucumberplayground.viewModels.login.LoginViewModel import com.corrado4eyes.cucumberplayground.viewModels.main.MainViewModel +import com.splendo.kaluga.alerts.AlertPresenter +import com.splendo.kaluga.alerts.BaseAlertPresenter +import com.splendo.kaluga.architecture.navigation.Navigator import com.splendo.kaluga.base.ApplicationHolder +import com.splendo.kaluga.hud.BaseHUD +import com.splendo.kaluga.hud.HUD import kotlinx.coroutines.Dispatchers import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel @@ -18,11 +23,13 @@ import kotlin.coroutines.CoroutineContext internal actual object PlatformModuleFactory : BasePlatformModuleFactory() { override val declaration: ModuleDeclaration = { + factory { AlertPresenter.Builder() } + factory { HUD.Builder() } viewModel { LoginViewModel() } viewModel { - HomeViewModel() + HomeViewModel(get(), get()) } viewModel { (testConfig: TestConfiguration?) -> MainViewModel(testConfig) diff --git a/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/viewModels/home/HomeViewModel.kt b/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/viewModels/home/HomeViewModel.kt index dbc9391..365e3e3 100644 --- a/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/viewModels/home/HomeViewModel.kt +++ b/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/viewModels/home/HomeViewModel.kt @@ -1,12 +1,18 @@ package com.corrado4eyes.cucumberplayground.viewModels.home import com.corrado4eyes.cucumberplayground.services.AuthService +import com.splendo.kaluga.alerts.BaseAlertPresenter +import com.splendo.kaluga.alerts.buildAlert import com.splendo.kaluga.architecture.viewmodel.BaseLifecycleViewModel +import com.splendo.kaluga.hud.BaseHUD import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject -class HomeViewModel : BaseLifecycleViewModel(), KoinComponent { +class HomeViewModel( + private val alertPresenterBuilder: BaseAlertPresenter.Builder, + private val hudBuilder: BaseHUD.Builder, +) : BaseLifecycleViewModel(alertPresenterBuilder), KoinComponent { private val authService: AuthService by inject() @@ -16,9 +22,24 @@ class HomeViewModel : BaseLifecycleViewModel(), KoinComponent { fun getCurrentUser() = authService.getCurrentUserIfAny()!! val user = authService.getCurrentUserIfAny()!! + private fun displayConfirmationDialog() { + alertPresenterBuilder.buildAlert(coroutineScope) { + setTitle("Are you sure you want to logout?") + setPositiveButton("Yes") { + coroutineScope.launch { + authService.logout() + } + } + setNegativeButton("No") { + // Do nothing + } + }.showAsync() + } + fun logout() { coroutineScope.launch { authService.logout() } + //displayConfirmationDialog() } } diff --git a/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/viewModels/login/LoginViewModel.kt b/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/viewModels/login/LoginViewModel.kt index 38641f2..f9e7cfe 100644 --- a/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/viewModels/login/LoginViewModel.kt +++ b/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/viewModels/login/LoginViewModel.kt @@ -82,33 +82,25 @@ class LoginViewModel : BaseLifecycleViewModel(), KoinComponent { fun login() { viewState.value = LoginViewState.Loading - coroutineScope.launch { - val password by passwordText - val email by emailText - - when { - email.value.isEmpty() -> { - viewState.value = LoginViewState.Error.EmptyField( - LoginViewState.Error.MissingField.EMAIL - ) - return@launch - } - - password.value.isEmpty() -> { - viewState.value = LoginViewState.Error.EmptyField( - LoginViewState.Error.MissingField.PASSWORD - ) - return@launch - } + val password by passwordText + val email by emailText + when { + email.value.isEmpty() -> { + viewState.value = LoginViewState.Error.EmptyField( + LoginViewState.Error.MissingField.EMAIL + ) + return } - delay(1000) - when (authService.login(email.value, password.value)) { - is AuthResponse.Success -> viewState.value = - LoginViewState.Idle // navigate to Home screen - is AuthResponse.Error -> { - viewState.value = LoginViewState.Error.IncorrectEmailOrPassword - return@launch - } + password.value.isEmpty() -> { + viewState.value = LoginViewState.Error.EmptyField( + LoginViewState.Error.MissingField.PASSWORD + ) + return + } + } + coroutineScope.launch { + if (authService.login(email.value, password.value) is AuthResponse.Error) { + viewState.value = LoginViewState.Error.IncorrectEmailOrPassword } } } diff --git a/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/viewModels/main/MainViewModel.kt b/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/viewModels/main/MainViewModel.kt index c206a92..1761294 100644 --- a/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/viewModels/main/MainViewModel.kt +++ b/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/viewModels/main/MainViewModel.kt @@ -14,9 +14,7 @@ sealed class AppNavigator { object Loading : AppNavigator() object Login : AppNavigator() - data class Home( - val user: User, - ) : AppNavigator() + object Home : AppNavigator() } class MainViewModel( @@ -37,7 +35,7 @@ class MainViewModel( } authService.observeUser.collect { user -> user?.let { - _navState.value = AppNavigator.Home(it) + _navState.value = AppNavigator.Home } ?: run { _navState.value = AppNavigator.Login } diff --git a/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/viewModels/LoginViewModelTest.kt b/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/viewModels/LoginViewModelTest.kt index a311605..3832ad5 100644 --- a/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/viewModels/LoginViewModelTest.kt +++ b/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/viewModels/LoginViewModelTest.kt @@ -19,14 +19,13 @@ class LoginViewModelTest : KoinUIThreadViewModelTest() as AuthServiceMock - override val viewModel: LoginViewModel = LoginViewModel(authService) + override val viewModel: LoginViewModel = LoginViewModel() } @Test fun test_on_login_button_pressed_fail_with_empty_email() = testOnUIThread { viewModel.login() assertTrue(viewModel.emailText.stateFlow.value.isEmpty()) -// assertEquals(Colors.red, viewModel.emailTextFieldBorderColor.stateFlow.value) assertEquals("Missing email", viewModel.emailErrorText.stateFlow.value) } @@ -54,7 +53,7 @@ class LoginViewModelTest : KoinUIThreadViewModelTest