From 4b640768cc87040de845afee421e6c569af4b3b2 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 20 Sep 2023 14:02:48 +0200 Subject: [PATCH 01/32] Create ui testing assertion module --- pistakio/build.gradle.kts | 88 +++++++++++++++++++ .../pistakio/DefaultCMApplication.kt | 54 ++++++++++++ .../com/corrado4eyes/pistakio/DefaultNode.kt | 42 +++++++++ .../pistakio/ApplicationAdapter.kt | 62 +++++++++++++ .../kotlin/com/corrado4eyes/pistakio/Node.kt | 61 +++++++++++++ .../pistakio}/errors/UIElementError.kt | 2 +- .../pistakio/CMApplicationTest.kt | 57 ++++++++++++ .../pistakio/mocks/StubCMApplication.kt | 25 ++++++ .../pistakio/CMApplicationAdapter.kt | 34 +++++++ .../com/corrado4eyes/pistakio/DefaultNode.kt | 67 ++++++++++++++ pistakio/src/iosMain/xctest.def | 8 ++ settings.gradle.kts | 4 +- 12 files changed, 502 insertions(+), 2 deletions(-) create mode 100644 pistakio/build.gradle.kts create mode 100644 pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt create mode 100644 pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt create mode 100644 pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt create mode 100644 pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt rename {cucumber/src/commonMain/kotlin/com/corrado4eyes/cucumber => pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio}/errors/UIElementError.kt (96%) create mode 100644 pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/CMApplicationTest.kt create mode 100644 pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubCMApplication.kt create mode 100644 pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt create mode 100644 pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt create mode 100644 pistakio/src/iosMain/xctest.def diff --git a/pistakio/build.gradle.kts b/pistakio/build.gradle.kts new file mode 100644 index 0000000..cd1f374 --- /dev/null +++ b/pistakio/build.gradle.kts @@ -0,0 +1,88 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") +} + +kotlin { + android { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + val target: org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget.() -> Unit = { + compilations.getByName("main") { + val xctest by cinterops.creating { + // Def-file describing the native API. + defFile(project.file("src/iosMain/xctest.def")) + } + } + } + + iosX64(configure = target) + iosArm64(configure = target) + iosSimulatorArm64(configure = target) + + sourceSets { + val commonMain by getting { + dependencies { + implementation(kotlin("test")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") + + } + } + val commonTest by getting { + dependencies { + val kalugaVersion: String by project + implementation(kotlin("test")) + implementation("com.splendo.kaluga:test-utils-base:$kalugaVersion") + } + } + val androidMain by getting { + dependencies { + implementation("androidx.compose.ui:ui-test-junit4:1.4.3") + implementation("androidx.compose.ui:ui-test-manifest:$1.4.3") + } + } + val androidUnitTest by getting + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + val iosX64Test by getting + val iosArm64Test by getting + val iosSimulatorArm64Test by getting + val iosTest by creating { + dependsOn(commonTest) + iosX64Test.dependsOn(this) + iosArm64Test.dependsOn(this) + iosSimulatorArm64Test.dependsOn(this) + } + } +} + +android { + namespace = "com.corrado4eyes.pistakio" + compileSdk = 33 + defaultConfig { + minSdk = 29 + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +android { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} diff --git a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt new file mode 100644 index 0000000..31be165 --- /dev/null +++ b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt @@ -0,0 +1,54 @@ +package com.corrado4eyes.pistakio + +import android.content.Intent +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.test.platform.app.InstrumentationRegistry +import kotlin.time.DurationUnit + +actual class DefaultApplicationAdapter( + testRule: ComposeContentTestRule? = null +) : BaseApplicationAdapter() { + private val testRule = testRule ?: createComposeRule() + + override fun launch(identifier: String?, arguments: Map) { + super.launch(identifier, arguments) + + val instrumentation = InstrumentationRegistry.getInstrumentation() + val appPackage = instrumentation.targetContext.packageName + val activityName = "$appPackage.$identifier" + val intent = Intent(Intent.ACTION_MAIN) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.setClassName(instrumentation.targetContext, activityName) + + arguments.forEach { + intent.putExtra(it.key, it.value) + } + + instrumentation.startActivitySync(intent) + } + + override fun findView(tag: String): Node { + assertAppIsRunning() + return DefaultNode(testRule.onNodeWithTag(tag)) + } + + override fun assert(assertionResult: AssertionResult) { + when (assertionResult) { + is AssertionResult.Failure -> { + throw assertionResult.exception + } + is AssertionResult.Success -> Unit // Do nothing to succeed + } + } + + override fun assertUntil( + timeout: TimeoutDuration, + blockAssertionResult: () -> AssertionResult + ) { + testRule.waitUntil(timeout.duration.toLong(DurationUnit.MILLISECONDS)) { + blockAssertionResult() is AssertionResult.Success + } + } +} \ No newline at end of file diff --git a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt new file mode 100644 index 0000000..54d93ba --- /dev/null +++ b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt @@ -0,0 +1,42 @@ +package com.corrado4eyes.pistakio + +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput + +actual class DefaultNode(internal val node: SemanticsNodeInteraction) : Node { + override fun exists(): AssertionResult = assertionResultFor(node::assertExists) + + override fun waitExists(timeout: TimeoutDuration): AssertionResult = + assertionResultFor(node:: assertExists) + + override fun isVisible(): AssertionResult = assertionResultFor(node::assertIsDisplayed) + + override fun isButton(): AssertionResult = assertionResultFor(node::assertHasClickAction) + + override fun isTextEqualTo(value: String, contains: Boolean): AssertionResult = + assertionResultFor { node.assertTextEquals(value) } + + override fun isHintEqualTo(value: String, contains: Boolean): AssertionResult = + assertionResultFor { node.assertTextEquals(value, includeEditableText = false) } + + override fun typeText(text: String) { + node.performTextInput(text) + } + + override fun tap() { + node.performClick() + } +} + +actual typealias AssertionBlockReturnType = SemanticsNodeInteraction +actual fun DefaultNode.assertionResultFor(assertionBlock: () -> AssertionBlockReturnType): AssertionResult = try { + assertionBlock() + AssertionResult.Success() +} catch (e: AssertionError) { +// Could take screeshot with node.captureToImage() + AssertionResult.Failure(e) +} diff --git a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt new file mode 100644 index 0000000..bd41021 --- /dev/null +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt @@ -0,0 +1,62 @@ +package com.corrado4eyes.pistakio + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.flow.MutableStateFlow + +class CMAppLaunchedAlreadyException : Throwable() { + override val message: String = "The app is already launched" +} + +class CMAppNotLaunchedYetException : Throwable() { + override val message: String = "The app was not launched" +} + +enum class TimeoutDuration(val duration: Duration) { + SHORT(10.seconds), + MEDIUM(30.seconds), + LONG(60.seconds) +} + +typealias ApplicationArguments = List + +interface ApplicationAdapter { + + companion object { + fun getArgument(arguments: List?, index: Int): String? = arguments?.get(index) + } + + fun launch(identifier: String? = null, arguments: Map) + fun findView(tag: String): Node + fun tearDown() + + fun assert(assertionResult: AssertionResult) + fun assertUntil(timeout: TimeoutDuration, blockAssertionResult: () -> AssertionResult) + + interface TearDownHandler { + fun tearDown() + } +} + +abstract class BaseApplicationAdapter : ApplicationAdapter { + private val isAppLaunched = MutableStateFlow(false) + + override fun launch(identifier: String?, arguments: Map) { + if (isAppLaunched.value) { + throw CMAppLaunchedAlreadyException() + } + isAppLaunched.value = true + } + + override fun tearDown() { + isAppLaunched.value = false + } + + protected fun assertAppIsRunning() { + if (!isAppLaunched.value) { + throw CMAppNotLaunchedYetException() + } + } +} + +expect class DefaultApplicationAdapter: BaseApplicationAdapter diff --git a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt new file mode 100644 index 0000000..f0b0cf8 --- /dev/null +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt @@ -0,0 +1,61 @@ +package com.corrado4eyes.pistakio + +sealed class AssertionResult { + class Success : AssertionResult() + class Failure(val exception: AssertionError) : AssertionResult() +} + + +interface Node { + fun typeText(text: String) + fun tap() + + /** + * If the [Node] exists at the moment of the call, it returns [AssertionResult.Success]. + * Otherwise it returns [AssertionResult.Failure]. + */ + fun exists(): AssertionResult + + /** + * Wait for an [Node] to appear in the view for the given [timeout] time. + * Returns an [AssertionResult.Failure] if the [Node] is not found or [AssertionResult.Success] instead. + * @param timeout Duration in seconds for the [Node] to appear in the view + */ + fun waitExists(timeout: TimeoutDuration): AssertionResult + + /** + * Returns [AssertionResult.Success] when the [Node] exists and is visible on the view. + * If the [Node] exists, but is not visible because is hidden by other views, + * it returns [AssertionResult.Failure]. Also if an [Node] exists in a ScrollView and when the + * method is called that [Node] is not visible, it will return [AssertionResult.Failure]. + */ + fun isVisible(): AssertionResult + + /** + * Returns [AssertionResult.Success] if the node is a button and possesses a click property. + * Otherwise it returns [AssertionResult.Failure]. + */ + fun isButton(): AssertionResult + + /** + * Validates whether a [Node]'s value is equal to a given text. + * @param value Expected content of the [Node]. + * @param contains If true it will try to find a match in the substrings of the text. + */ + fun isTextEqualTo(value: String, contains: Boolean): AssertionResult + + /** + * Validates whether a [Node]'s value is equal to a given hint. + * @param value Expected content of the [Node]. + * @param contains If true it will try to find a match in the substrings of the hint. + */ + fun isHintEqualTo(value: String, contains: Boolean): AssertionResult + +// fun doubleTap() +// fun press() +} + +expect class DefaultNode : Node + +expect class AssertionBlockReturnType +expect fun DefaultNode.assertionResultFor(assertionBlock: () -> AssertionBlockReturnType): AssertionResult diff --git a/cucumber/src/commonMain/kotlin/com/corrado4eyes/cucumber/errors/UIElementError.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/errors/UIElementError.kt similarity index 96% rename from cucumber/src/commonMain/kotlin/com/corrado4eyes/cucumber/errors/UIElementError.kt rename to pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/errors/UIElementError.kt index 9405ce7..ce3c746 100644 --- a/cucumber/src/commonMain/kotlin/com/corrado4eyes/cucumber/errors/UIElementError.kt +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/errors/UIElementError.kt @@ -1,4 +1,4 @@ -package com.corrado4eyes.cucumber.errors +package com.corrado4eyes.pistakio.errors enum class UIElementType { SCREEN, diff --git a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/CMApplicationTest.kt b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/CMApplicationTest.kt new file mode 100644 index 0000000..a51df09 --- /dev/null +++ b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/CMApplicationTest.kt @@ -0,0 +1,57 @@ +package com.corrado4eyes.pistakio + +import com.corrado4eyes.pistakio.mocks.StubCMApplication +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class CMApplicationTest { + + private lateinit var app: StubCMApplication + + @BeforeTest + fun setUp() { + app = StubCMApplication() + } + + @Test + fun test_launch_app() { + app.launch() + assertEquals(1, app.launchCalled) + } + + @Test + fun test_launch_app_find_view() { + app.launch() + assertEquals(1, app.launchCalled) + + app.findView(tag = "test") + assertEquals(1, app.findView) + assertEquals("test", app.findViewWithTag) + } + + @Test + fun test_app_launched_twice() { + assertFailsWith { + app.launch() + app.launch() + } + } + + @Test + fun test_app_launched_tear_down_app_launched_again() { + app.launch() + assertEquals(1, app.launchCalled) + app.tearDown() + app.launch() + assertEquals(2, app.launchCalled) + } + + @Test + fun test_find_view_fails_must_launch_app_first() { + assertFailsWith { + app.findView(tag = "test") + } + } +} \ No newline at end of file diff --git a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubCMApplication.kt b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubCMApplication.kt new file mode 100644 index 0000000..36dbdac --- /dev/null +++ b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubCMApplication.kt @@ -0,0 +1,25 @@ +package com.corrado4eyes.pistakio.mocks + +import com.corrado4eyes.pistakio.BaseApplicationAdapter + +class StubCMApplication : BaseApplicationAdapter() { + var launchCalled = 0 + override fun launch() { + super.launch() + launchCalled++ + } + + var findView = 0 + var findViewWithTag: String? = null + override fun findView(tag: String) { + super.findView(tag) + findViewWithTag = tag + findView++ + } + + var tearDown = 0 + override fun tearDown() { + super.tearDown() + tearDown++ + } +} diff --git a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt new file mode 100644 index 0000000..9aa0032 --- /dev/null +++ b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt @@ -0,0 +1,34 @@ +package com.corrado4eyes.pistakio + +import platform.XCTest.XCUIApplication +import platform.XCTest.XCUIElementTypeAny + +actual class DefaultApplicationAdapter(app: XCUIApplication?) : BaseApplicationAdapter() { + private val app: XCUIApplication = app ?: XCUIApplication() + + override fun launch(identifier: String?, arguments: Map) { + super.launch(identifier, arguments) + app.launch() + } + + override fun findView(tag: String): Node { + assertAppIsRunning() + val element = app + .descendantsMatchingType(XCUIElementTypeAny) + .matchingIdentifier(tag) + .element + return DefaultNode(element) + } + + override fun assert(assertionResult: AssertionResult) { + when (assertionResult) { + is AssertionResult.Failure -> throw assertionResult.exception + is AssertionResult.Success -> Unit // Do nothing to succeed + } + } + + override fun assertUntil( + timeout: TimeoutDuration, + blockAssertionResult: () -> AssertionResult + ) = assert(blockAssertionResult()) +} \ No newline at end of file diff --git a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt new file mode 100644 index 0000000..1c22cf8 --- /dev/null +++ b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt @@ -0,0 +1,67 @@ +package com.corrado4eyes.pistakio + +import kotlin.time.DurationUnit +import platform.Foundation.NSPredicate +import platform.XCTest.XCTWaiter +import platform.XCTest.XCTWaiterResultCompleted +import platform.XCTest.XCTestCase +import platform.XCTest.XCUIElement +import platform.XCTest.expectationForPredicate +import platform.XCTest.tap +import platform.XCTest.typeText + +actual class DefaultNode(private val element: XCUIElement) : Node { + + override fun exists(): AssertionResult { + val predicate = NSPredicate.predicateWithFormat("exists == true") + val expectation = XCTestCase().expectationForPredicate( + predicate = predicate, + evaluatedWithObject = element, + handler = null + ) + + val result = XCTWaiter().waitForExpectations(expectations = listOf(expectation)) + + return assertionResultFor { result == XCTWaiterResultCompleted } + } + + override fun waitExists(timeout: TimeoutDuration): AssertionResult = assertionResultFor { + element.waitForExistenceWithTimeout( + timeout.duration.toDouble(DurationUnit.SECONDS) + ) + } + + override fun isVisible(): AssertionResult = exists() + + override fun isButton(): AssertionResult = assertionResultFor(element::isHittable) + + override fun isTextEqualTo(value: String, contains: Boolean): AssertionResult = + assertionResultFor { isTextEqualOrContains(value, contains) } + + override fun isHintEqualTo(value: String, contains: Boolean): AssertionResult = + assertionResultFor { isTextEqualOrContains(value, contains) } + + override fun typeText(text: String) { + element.tap() + element.typeText(text) + } + + override fun tap() { + element.tap() + } + + private fun isTextEqualOrContains(value: String, contains: Boolean): Boolean = if (contains) { + (element.value as? String)?.contains(value) == true + } else { + element.value == value + } +} + +actual typealias AssertionBlockReturnType = Boolean +actual fun DefaultNode.assertionResultFor( + assertionBlock: () -> AssertionBlockReturnType +): AssertionResult = if (assertionBlock()) { + AssertionResult.Success() +} else { + AssertionResult.Failure(AssertionError()) +} diff --git a/pistakio/src/iosMain/xctest.def b/pistakio/src/iosMain/xctest.def new file mode 100644 index 0000000..a7defaa --- /dev/null +++ b/pistakio/src/iosMain/xctest.def @@ -0,0 +1,8 @@ +language = Objective-C +package = platform.XCTest +depends = UIKit +modules = XCTest +linkerOpts.ios_arm64 = -weak_framework XCTest -L/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/UIKit.framework -F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Frameworks/ +linkerOpts.ios_x64 = -weak_framework XCTest -L/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/UIKit.framework -F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/ +linkerOpts.ios_simulator_arm64 = -weak_framework XCTest -L/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/UIKit.framework -F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/ +compilerOpts = -weak_framework XCTest -F/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks/ \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 0e3e57c..d38d778 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,5 +16,7 @@ dependencyResolutionManagement { rootProject.name = "CucumberPlayground" include(":android") include(":shared") -include(":cucumber") include(":cucumberShared") + +include(":pistakio") +include(":cucumber") From 8e01f4cd82994c8908739954ec8ce1fca8192642 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 20 Sep 2023 14:05:06 +0200 Subject: [PATCH 02/32] Update strings structure and references --- .../cucumberplayground/models/Strings.kt | 14 +++++++++----- .../viewModels/home/HomeViewModel.kt | 2 +- .../viewModels/login/LoginViewModel.kt | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/models/Strings.kt b/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/models/Strings.kt index 2493ef9..63307c4 100644 --- a/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/models/Strings.kt +++ b/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/models/Strings.kt @@ -3,13 +3,13 @@ package com.corrado4eyes.cucumberplayground.models object Strings { object Screen { object Title { - val login = "Login screen" - val home = "Home screen" - } - object Tag { val login = "Login" val home = "Home" } + object Tag { + val login = "Login Screen" + val home = "Home Screen" + } } object TextField { @@ -25,9 +25,13 @@ object Strings { } object Button { - object Text { + object Title { val login = "Login" val logout = "Logout" } + object Tag { + val login = "Login Button" + val logout = "Logout Button" + } } } \ No newline at end of file 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 e859478..5c95491 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 @@ -12,7 +12,7 @@ class HomeViewModel : BaseLifecycleViewModel(), KoinComponent { private val authService: AuthService by inject() val screenTitle = Strings.Screen.Title.home - val buttonTitle = Strings.Button.Text.logout + val buttonTitle = Strings.Button.Title.logout fun getCurrentUser() = authService.getCurrentUserIfAny()!! val user = authService.getCurrentUserIfAny()!! 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 7afa255..c3fb419 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 @@ -75,7 +75,7 @@ class LoginViewModel : BaseLifecycleViewModel(), KoinComponent { } else "" }.toInitializedObservable("", coroutineScope) - val buttonTitle = Strings.Button.Text.login + val buttonTitle = Strings.Button.Title.login val isButtonEnabled = viewState.map { it !is LoginViewState.Loading } .toInitializedObservable(false, coroutineScope) val isLoading = viewState.map { it is LoginViewState.Loading } From 261df5ada428d2ae03863968b7aa6edfd4285970 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 20 Sep 2023 14:05:26 +0200 Subject: [PATCH 03/32] Update unit tests --- .../cucumberplayground/cucumber/login/AuthServiceMock.kt | 8 +++++--- .../cucumber/viewModels/LoginViewModelTest.kt | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/login/AuthServiceMock.kt b/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/login/AuthServiceMock.kt index 24f9086..c3936c2 100644 --- a/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/login/AuthServiceMock.kt +++ b/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/login/AuthServiceMock.kt @@ -1,8 +1,8 @@ package com.corrado4eyes.cucumberplayground.cucumber.login +import com.corrado4eyes.cucumberplayground.login.model.AuthResponse import com.corrado4eyes.cucumberplayground.models.User import com.corrado4eyes.cucumberplayground.services.AuthService -import com.corrado4eyes.cucumberplayground.login.model.AuthResponse import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -50,7 +50,9 @@ class AuthServiceMock : AuthService { } } - override fun observeUser(): Flow { - return currentUser.asStateFlow() + override val observeUser: Flow = currentUser.asStateFlow() + + override fun getCurrentUserIfAny(): User? { + return currentUser.asStateFlow().value } } 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..3c73ba3 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,7 +19,7 @@ class LoginViewModelTest : KoinUIThreadViewModelTest() as AuthServiceMock - override val viewModel: LoginViewModel = LoginViewModel(authService) + override val viewModel: LoginViewModel = LoginViewModel() } @Test From 0c126d56a8f3763faac07af6af76fbd293702a1b Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 20 Sep 2023 14:07:20 +0200 Subject: [PATCH 04/32] Update gradle files so that platforms can use the new module --- android/build.gradle.kts | 1 + cucumberShared/build.gradle.kts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 15799f7..e49c949 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation(project(":shared")) implementation(project(":cucumber")) + implementation(project(":pistakio")) implementation("androidx.compose.ui:ui:1.4.3") implementation("androidx.compose.ui:ui-tooling:1.4.3") implementation("androidx.compose.ui:ui-tooling-preview:1.4.3") diff --git a/cucumberShared/build.gradle.kts b/cucumberShared/build.gradle.kts index 9f6e57c..1c3c7c1 100644 --- a/cucumberShared/build.gradle.kts +++ b/cucumberShared/build.gradle.kts @@ -25,12 +25,14 @@ kotlin { transitiveExport = true export(project(":shared")) export(project(":cucumber")) + export(project(":pistakio")) baseName = "shared" linkFrameworkSearchPaths("$projectDir/../cucumber") + linkFrameworkSearchPaths("$projectDir/../pistakio") getTest("DEBUG").apply { - linkFrameworkSearchPaths("$projectDir/../cucumber") + linkFrameworkSearchPaths("$projectDir/../pistakio") } } } @@ -45,6 +47,7 @@ kotlin { dependencies { api(project(":shared")) api(project(":cucumber")) + api(project(":pistakio")) } } val commonTest by getting { From d0b13674046d95f864aae2761fa3c99d4151c3c2 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 20 Sep 2023 14:08:15 +0200 Subject: [PATCH 05/32] Update platforms tests and layout --- .../test/StepDefinitions.kt | 119 ++++++------- .../android/home/HomeLayout.kt | 10 +- .../android/login/LoginLayout.kt | 14 +- ios/CucumberTests/CucumberTests.swift | 165 ++++++++++-------- ios/CucumberTests/Util/Node+Extensions.swift | 16 ++ ios/ios.xcodeproj/project.pbxproj | 4 + ios/ios/Screens/Home/HomeView.swift | 5 +- ios/ios/Screens/Login/LoginView.swift | 7 +- 8 files changed, 184 insertions(+), 156 deletions(-) create mode 100644 ios/CucumberTests/Util/Node+Extensions.swift diff --git a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt index 4e4f2c1..5d43098 100644 --- a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt +++ b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt @@ -1,19 +1,10 @@ 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.errors.UIElementException -import com.corrado4eyes.cucumberplayground.android.MainActivity +import com.corrado4eyes.pistakio.DefaultApplicationAdapter +import com.corrado4eyes.pistakio.TimeoutDuration +import com.corrado4eyes.pistakio.errors.UIElementException import com.corrado4eyes.cucumberplayground.models.Strings import com.corrado4eyes.cucumbershared.tests.Definitions import io.cucumber.java8.En @@ -28,69 +19,76 @@ class StepDefinitions : En { @get:Rule(order = 0) val testRule = createComposeRule() + val application = DefaultApplicationAdapter(testRule) init { Definitions.values().forEach { val definitionString = it.definition.definitionString when (it) { Definitions.I_AM_IN_THE_EXPECT_VALUE_STRING_SCREEN -> Given(definitionString) { screenName: String -> - val screenTitleTag = when (screenName) { - Strings.Screen.Tag.login -> { - arguments["isLoggedIn"] = "false" - arguments["testEmail"] = "" - Strings.Screen.Title.login - } - - Strings.Screen.Tag.home -> { - arguments["isLoggedIn"] = "true" - Strings.Screen.Title.home - } + val screenTitleTag = when (screenName) { + Strings.Screen.Title.login -> { + arguments["isLoggedIn"] = "false" + arguments["testEmail"] = "" + Strings.Screen.Tag.login + } - else -> throw UIElementException.Screen.NotFound(screenName) + Strings.Screen.Title.home -> { + arguments["isLoggedIn"] = "true" + Strings.Screen.Tag.home } - setLaunchScreen() - testRule.onNodeWithTag(screenTitleTag).assertIsDisplayed() + + else -> throw UIElementException.Screen.NotFound(screenName) + } + + application.launch("MainActivity", arguments) + val element = application.findView(screenTitleTag) + + application.assert(element.waitExists(TimeoutDuration.SHORT)) } - Definitions.I_SEE_EXPECT_VALUE_STRING_TEXT -> Then(definitionString) { title: String -> - testRule.onNodeWithText(title).assertIsDisplayed() + Definitions.I_SEE_EXPECT_VALUE_STRING_TEXT -> Then(definitionString) { textViewTitle: String -> + val element = application.findView(textViewTitle) + application.assert(element.waitExists(TimeoutDuration.SHORT)) + application.assert(element.isVisible()) } - Definitions.I_SEE_THE_EXPECT_VALUE_STRING_BUTTON -> Then(definitionString) { buttonName: String -> - when (buttonName) { - Strings.Button.Text.login -> testRule.onNodeWithText(Strings.Button.Text.login).assertIsDisplayed().assertHasClickAction() - Strings.Button.Text.logout -> testRule.onNodeWithTag(Strings.Button.Text.logout).assertIsDisplayed().assertHasClickAction() - else -> throw UIElementException.Button.NotFound(buttonName) + Definitions.I_SEE_THE_EXPECT_VALUE_STRING_BUTTON -> Then(definitionString) { buttonTitle: String -> + val element = when (buttonTitle) { + Strings.Button.Title.login -> application.findView(Strings.Button.Tag.login)/*testRule.onNodeWithText(Strings.Button.Text.login)*/ + Strings.Button.Title.logout -> application.findView(Strings.Button.Tag.logout)/*testRule.onNodeWithTag(Strings.Button.Text.logout)*/ + else -> throw UIElementException.Button.NotFound(buttonTitle) } + application.assert(element.waitExists(TimeoutDuration.SHORT)) + application.assert(element.isVisible()) + application.assert(element.isButton()) } - Definitions.I_SEE_THE_EXPECT_VALUE_STRING_SCREEN -> Then(definitionString) { screenName: String -> - Thread.sleep(1000) - when (screenName) { - Strings.Screen.Tag.login -> testRule.onNodeWithTag(Strings.Screen.Title.login).assertIsDisplayed() - Strings.Screen.Tag.home -> testRule.onNodeWithTag(Strings.Screen.Title.home).assertIsDisplayed() - else -> throw UIElementException.Screen.NotFound(screenName) + Definitions.I_SEE_THE_EXPECT_VALUE_STRING_SCREEN -> Then(definitionString) { screenTitle: String -> + val element = when (screenTitle) { + Strings.Screen.Title.login -> application.findView(Strings.Screen.Tag.login)/*testRule.onNodeWithTag(Strings.Screen.Title.login).assertIsDisplayed()*/ + Strings.Screen.Title.home -> application.findView(Strings.Screen.Tag.home)/*testRule.onNodeWithTag(Strings.Screen.Title.home).assertIsDisplayed()*/ + else -> throw UIElementException.Screen.NotFound(screenTitle) } + application.assertUntil(TimeoutDuration.SHORT, element::exists) } - Definitions.I_SEE_THE_EXPECT_VALUE_STRING_TEXT_FIELD_WITH_TEXT_EXPECT_VALUE_STRING -> Then(definitionString) { tag: String, text: String -> - val elementNode = try { - testRule.onNodeWithTag(tag).assertIsDisplayed() - } catch (e: AssertionError) { - throw UIElementException.TextField.NotFound(tag) - } - elementNode.assertTextContains(text) + Definitions.I_SEE_THE_EXPECT_VALUE_STRING_TEXT_FIELD_WITH_TEXT_EXPECT_VALUE_STRING -> Then(definitionString) { textFieldTag: String, textFieldText: String -> + val element = application.findView(textFieldTag) + application.assert(element.waitExists(TimeoutDuration.SHORT)) + application.assert(element.isHintEqualTo(textFieldText, true)) } Definitions.I_TYPE_EXPECT_VALUE_STRING_IN_THE_EXPECT_VALUE_STRING_TEXT_FIELD -> When(definitionString) { textInput: String, tag: String, -> - testRule.onNodeWithText(tag).performTextInput(textInput) + application.findView(tag).typeText(textInput) } Definitions.I_TYPE_EXPECT_VALUE_STRING_IN_THE_EXPECT_VALUE_STRING_SECURE_TEXT_FIELD -> When(definitionString) { textInput: String, tag: String -> - testRule.onNodeWithText(tag).performTextInput(textInput) + application.findView(tag).typeText(textInput) } - Definitions.I_PRESS_THE_EXPECT_VALUE_STRING_BUTTON -> When(definitionString) { buttonName: String -> - val elementNode = try { - testRule.onNodeWithTag(buttonName).assertIsDisplayed() - } catch (e: AssertionError) { - throw UIElementException.TextField.NotFound(buttonName) + Definitions.I_PRESS_THE_EXPECT_VALUE_STRING_BUTTON -> When(definitionString) { buttonTag: String -> + val element = when(buttonTag) { + Strings.Button.Title.login -> application.findView(Strings.Button.Tag.login) + Strings.Button.Title.logout -> application.findView(Strings.Button.Tag.logout) + else -> throw UIElementException.Button.NotFound(buttonTag) } - elementNode.performClick() + application.assert(element.waitExists(TimeoutDuration.SHORT)) + element.tap() } Definitions.EMAIL_IS_EXPECT_VALUE_STRING -> Given(definitionString) { loggedInEmail: String -> arguments["testEmail"] = loggedInEmail @@ -98,17 +96,4 @@ class StepDefinitions : En { } } } - - 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/home/HomeLayout.kt b/android/src/main/java/com/corrado4eyes/cucumberplayground/android/home/HomeLayout.kt index ae217f4..3d1c390 100644 --- a/android/src/main/java/com/corrado4eyes/cucumberplayground/android/home/HomeLayout.kt +++ b/android/src/main/java/com/corrado4eyes/cucumberplayground/android/home/HomeLayout.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview +import com.corrado4eyes.cucumberplayground.models.Strings import com.corrado4eyes.cucumberplayground.viewModels.home.HomeViewModel import com.splendo.kaluga.architecture.compose.viewModel.ViewModelComposable import org.koin.androidx.compose.koinViewModel @@ -19,12 +20,15 @@ fun HomeLayout() { Column(modifier = Modifier.fillMaxSize()) { Text( this@ViewModelComposable.screenTitle, - modifier = Modifier.testTag(this@ViewModelComposable.screenTitle) + modifier = Modifier.testTag(Strings.Screen.Tag.home) + ) + Text( + this@ViewModelComposable.getCurrentUser().email, + modifier = Modifier.testTag(this@ViewModelComposable.getCurrentUser().email) ) - Text(this@ViewModelComposable.getCurrentUser().email) Button( this@ViewModelComposable::logout, - modifier = Modifier.testTag(this@ViewModelComposable.buttonTitle) + modifier = Modifier.testTag(Strings.Button.Tag.logout) ) { Text(this@ViewModelComposable.buttonTitle) } 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 db3721e..2b25f76 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 @@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview +import com.corrado4eyes.cucumberplayground.models.Strings import com.corrado4eyes.cucumberplayground.viewModels.login.LoginViewModel import com.splendo.kaluga.architecture.compose.state import com.splendo.kaluga.architecture.compose.viewModel.ViewModelComposable @@ -26,18 +27,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(Strings.Screen.Tag.login) + ) CustomTextField( value = this@ViewModelComposable.emailText, label = "Email", - modifier = Modifier.testTag("Email") + modifier = Modifier.testTag(Strings.TextField.Tag.email) ) val emailErrorText by this@ViewModelComposable.emailErrorText.state() Text(text = emailErrorText, color = Color.Red) CustomTextField( value = this@ViewModelComposable.passwordText, label = "Password", - modifier = Modifier.testTag("Password") + modifier = Modifier.testTag(Strings.TextField.Tag.password) ) val passwordErrorText by this@ViewModelComposable.passwordErrorText.state() Text(text = passwordErrorText, color = Color.Red) @@ -50,9 +54,9 @@ fun LoginLayout() { Button( this@ViewModelComposable::login, - modifier = Modifier.testTag("Login") + modifier = Modifier.testTag(Strings.Button.Tag.login) ) { - Text("Login") + Text(viewModel.buttonTitle) } } } diff --git a/ios/CucumberTests/CucumberTests.swift b/ios/CucumberTests/CucumberTests.swift index 1d0e8f8..aab3e7c 100644 --- a/ios/CucumberTests/CucumberTests.swift +++ b/ios/CucumberTests/CucumberTests.swift @@ -13,118 +13,131 @@ import Cucumberish @objc public class CucumberishInitializer: NSObject { static var app: XCUIApplication! + static var applicationAdapter: ApplicationAdapter! @objc public class func CucumberishSwiftInit() { - - before { _ in + beforeStart { app = XCUIApplication() + applicationAdapter = DefaultApplicationAdapter(app: app) app.launchArguments.append("test") } + afterFinish { + applicationAdapter.tearDown() + } + for test in Definitions.companion.allCases { let definitionString = test.definition.definitionString switch test { case .iAmInTheExpectValueStringScreen: Given(definitionString) { args, userInfo in - guard let screenName = args?[0] as? String else { return } - - let text: XCUIElement - switch(screenName) { - case Strings.ScreenTag.shared.home: + guard let screenTitle = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } + + let screenTag: String + switch screenTitle { + case Strings.ScreenTitle.shared.home: app.launchEnvironment["isLoggedIn"] = "true" - text = app.staticTexts[Strings.ScreenTitle.shared.home] - case Strings.ScreenTag.shared.login: + screenTag = Strings.ScreenTag.shared.home + case Strings.ScreenTitle.shared.login: app.launchEnvironment["isLoggedIn"] = "false" - text = app.staticTexts[Strings.ScreenTitle.shared.login] + screenTag = Strings.ScreenTag.shared.login default: - text = app.staticTexts["Fail"] - XCTFail("Couldn't find \(screenName) screen") + screenTag = "Fail" + XCTFail("Couldn't find \(screenTag) screen") } - app.launch() + applicationAdapter.launch(identifier: nil, arguments: app.launchEnvironment) - XCTAssert(text.exists(timeout: .short), "Couldn't validate to be in \(screenName)") - return + let element = applicationAdapter.findView(tag: screenTag) + + element.assert(assertionResult: element.waitExists(timeout: .short_), message: "Couldn't validate to be in \(screenTitle)") } - case .iSeeExpectValueStringText: Then(definitionString){ args, userInfo in - guard let textString = args?[0] as? String else { return } - let text = app.staticTexts[textString] - XCTAssert(text.exists(timeout: .short), "Couldn't find \(text) text") - return + case .iSeeExpectValueStringText: Then(definitionString) { args, userInfo in + guard let textViewTitle = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } + let element = applicationAdapter.findView(tag: textViewTitle) + + element.assert(assertionResult: element.waitExists(timeout: .short_), message: "Couldn't find \(element) text") } case .iSeeTheExpectValueStringButton: Then(definitionString) { args, userInfo in - guard let text = args?[0] as? String else { return } - let button = app.buttons[text] - XCTAssert(button.exists(timeout: .short), "\"\(text)\" button should be visible") - return + guard let buttonTitle = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } + + let buttonTag: String + switch buttonTitle { + case Strings.ButtonTitle.shared.login: + buttonTag = Strings.ButtonTag.shared.login + case Strings.ButtonTitle.shared.logout: + buttonTag = Strings.ButtonTag.shared.logout + default: + buttonTag = "Fail" + XCTFail("Couldn't find \(buttonTitle) button") + } + + let element = applicationAdapter.findView(tag: buttonTag) + element.assert(assertionResult: element.waitExists(timeout: .short_), message: "Couldn't find \(buttonTitle) button") + + element.assert(assertionResult: element.isButton(), message: "Couldn't validate that \(buttonTitle) is a button") } case .iSeeTheExpectValueStringScreen: Then(definitionString) { args, userInfo in - guard let screenName = args?[0] as? String else { return } - switch(screenName) { - case Strings.ScreenTag.shared.home: - let text = app.staticTexts[Strings.ScreenTitle.shared.home] - XCTAssert(text.exists(timeout: .short), "Couldn't validate to be in \(screenName)") - case Strings.ScreenTag.shared.login: - let text = app.staticTexts[Strings.ScreenTitle.shared.login] - XCTAssert(text.exists(timeout: .short), "Couldn't validate to be in \(screenName)") - default: XCTFail("Couldn't find \(screenName) screen") + guard let screenTitle = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } + + let screenTag: String + switch screenTitle { + case Strings.ScreenTitle.shared.home: + screenTag = Strings.ScreenTag.shared.home + case Strings.ScreenTitle.shared.login: + screenTag = Strings.ScreenTag.shared.login + default: + screenTag = "Fail" + XCTFail("Couldn't find \(screenTitle) screen") } - return + + let element = applicationAdapter.findView(tag: screenTag) + let elementExists = element.waitExists(timeout: .long_) + element.assert(assertionResult: elementExists, message: "Couldn't find \(screenTag) screen") } case .iSeeTheExpectValueStringTextFieldWithTextExpectValueString: Then(definitionString) { args, userInfo in - guard let textfieldName = args?[0] as? String else { return } - guard let textfieldText = args?[1] as? String else { return } - let textfield: XCUIElement? = { - switch (textfieldName){ - case Strings.TextFieldTag.shared.email: - return app.textFields[textfieldName] - case Strings.TextFieldTag.shared.password: - return app.secureTextFields[textfieldName] - default: - return nil - } - }() - if textfield == nil { - XCTFail("Couldn't find \"\(textfieldName)\" field") - return - } + guard let textFieldTag = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } + guard let textFieldText = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 1) else { return } - XCTAssert(textfield?.value as? String == textfieldText, "\"\(textfieldName)\" field text should be \"\(textfield?.value)\"") - return + let element = applicationAdapter.findView(tag: textFieldTag) + + element.assert(assertionResult: element.waitExists(timeout: .short_), message: "Couldn't find \(textFieldTag) field") + + element.assert(assertionResult: element.isHintEqualTo(value: textFieldText, contains: true), message: "TextField value doesn't match \(textFieldText)") } case .iTypeExpectValueStringInTheExpectValueStringTextField: When(definitionString) { args, userInfo in - guard let textfieldName = args?[1] as? String else { return } - guard let textfieldText = args?[0] as? String else { return } - let textfield = app.textFields[textfieldName] - textfield.tap() - textfield.typeText(textfieldText) - return + guard let textfieldTag = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 1) else { return } + guard let textfieldText = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } + + let element = applicationAdapter.findView(tag: textfieldTag) + element.typeText(text: textfieldText) } case .iTypeExpectValueStringInTheExpectValueStringSecureTextField: When(definitionString) { args, userInfo in - guard let textfieldName = args?[1] as? String else { return } - guard let textfieldText = args?[0] as? String else { return } - let textfield = app.secureTextFields[textfieldName] - textfield.tap() - textfield.typeText(textfieldText) - return + guard let textfieldTag = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 1) else { return } + guard let textfieldText = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } + + let element = applicationAdapter.findView(tag: textfieldTag) + element.typeText(text: textfieldText) } case .iPressTheExpectValueStringButton: When(definitionString) { args, userInfo in - guard let buttonName = args?[0] as? String else { return } - let button = app.buttons[buttonName] - let link = app.links[buttonName] - - 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") + guard let buttonTitle = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } + let buttonTag: String + switch buttonTitle { + case Strings.ButtonTitle.shared.login: + buttonTag = Strings.ButtonTag.shared.login + case Strings.ButtonTitle.shared.logout: + buttonTag = Strings.ButtonTag.shared.logout + default: + buttonTag = "Fail" + XCTFail("I press the \(buttonTitle) button failed") } - return + + let element = applicationAdapter.findView(tag: buttonTag) + element.tap() } case .emailIsExpectValueString: Given(definitionString) { args, userInfo in - guard let email = args?[0] as? String else { return } + guard let email = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } app.launchEnvironment["testEmail"] = email return } diff --git a/ios/CucumberTests/Util/Node+Extensions.swift b/ios/CucumberTests/Util/Node+Extensions.swift new file mode 100644 index 0000000..37fd3b6 --- /dev/null +++ b/ios/CucumberTests/Util/Node+Extensions.swift @@ -0,0 +1,16 @@ +// +// Node+Extensions.swift +// CucumberTests +// +// Created by Corrado Quattrocchi on 15/09/2023. +// Copyright © 2023 orgName. All rights reserved. +// + +import shared +import XCTest + +extension Node { + func assert(assertionResult: AssertionResult, message: String) { + XCTAssert(assertionResult is AssertionResult.Success, message) + } +} diff --git a/ios/ios.xcodeproj/project.pbxproj b/ios/ios.xcodeproj/project.pbxproj index 0323218..8ab44b2 100644 --- a/ios/ios.xcodeproj/project.pbxproj +++ b/ios/ios.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 48C5442ECE76BED5A8FFFAE9 /* ListObservable.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 140577234C8D9C9BD309475E /* ListObservable.generated.swift */; }; 4D7D41A3A4FA6F91C431D068 /* KalugaDate+Extensions.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D787A9CEB2779127CD15B6F /* KalugaDate+Extensions.generated.swift */; }; 5A0347FCDC0723695A7D6A7C /* PlatformValueMapper.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E798EA8B136B833082411B4 /* PlatformValueMapper.generated.swift */; }; + 640CC3C82AB49ECA003E3D8C /* Node+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640CC3C72AB49ECA003E3D8C /* Node+Extensions.swift */; }; 640DFEEC2A370DA300ABF2DD /* CucumberTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640DFEEB2A370DA300ABF2DD /* CucumberTests.swift */; }; 640DFEF52A370DD200ABF2DD /* Features in Resources */ = {isa = PBXBuildFile; fileRef = 640DFEF42A370DD200ABF2DD /* Features */; }; 640DFEF82A370F5C00ABF2DD /* CucumberTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 640DFEF72A370F5C00ABF2DD /* CucumberTests.m */; }; @@ -89,6 +90,7 @@ 48B350D3EB640C8B7B253753 /* UninitializedObservable.generated.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = UninitializedObservable.generated.swift; sourceTree = ""; }; 4DEA2654EFE69B013946D9B0 /* Pods-CucumberTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CucumberTests.release.xcconfig"; path = "Target Support Files/Pods-CucumberTests/Pods-CucumberTests.release.xcconfig"; sourceTree = ""; }; 5416DF2E8BB6BE190C0431AF /* NavigationBarColor.generated.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = NavigationBarColor.generated.swift; sourceTree = ""; }; + 640CC3C72AB49ECA003E3D8C /* Node+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Node+Extensions.swift"; sourceTree = ""; }; 640DFEE92A370DA300ABF2DD /* CucumberTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CucumberTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 640DFEEB2A370DA300ABF2DD /* CucumberTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CucumberTests.swift; sourceTree = ""; }; 640DFEF42A370DD200ABF2DD /* Features */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Features; sourceTree = ""; }; @@ -202,6 +204,7 @@ children = ( 649B6F992A389F5F00C40DCC /* XCUIElement.swift */, 649B6F9B2A389F9300C40DCC /* Timeout.swift */, + 640CC3C72AB49ECA003E3D8C /* Node+Extensions.swift */, ); path = Util; sourceTree = ""; @@ -489,6 +492,7 @@ 648329182A4AE6BB00E5A4A8 /* RoutingState.generated.swift in Sources */, 648329262A4AE6BB00E5A4A8 /* DefaultValues.generated.swift in Sources */, 6483291D2A4AE6BB00E5A4A8 /* Shape+Helpers.generated.swift in Sources */, + 640CC3C82AB49ECA003E3D8C /* Node+Extensions.swift in Sources */, 648329032A43391500E5A4A8 /* iOSApp.swift in Sources */, 640DFEF82A370F5C00ABF2DD /* CucumberTests.m in Sources */, 648329152A4AE6BB00E5A4A8 /* Subject.generated.swift in Sources */, diff --git a/ios/ios/Screens/Home/HomeView.swift b/ios/ios/Screens/Home/HomeView.swift index fc557f0..6f49ec9 100644 --- a/ios/ios/Screens/Home/HomeView.swift +++ b/ios/ios/Screens/Home/HomeView.swift @@ -21,14 +21,15 @@ struct HomeView: SwiftUI.View { NavigationView { VStack { Text(viewModel.user.email) - + .accessibilityLabel(viewModel.user.email) Button(action: viewModel.logout) { Text(viewModel.buttonTitle) } - .accessibilityLabel(viewModel.buttonTitle) + .accessibilityLabel(Strings.ButtonTag.shared.logout) }.toolbar { ToolbarItem(placement: .principal) { Text(viewModel.screenTitle) + .accessibilityLabel(Strings.ScreenTag.shared.home) } }.navigationBarTitleDisplayMode(.inline) } diff --git a/ios/ios/Screens/Login/LoginView.swift b/ios/ios/Screens/Login/LoginView.swift index 69ab862..7b0f6f4 100644 --- a/ios/ios/Screens/Login/LoginView.swift +++ b/ios/ios/Screens/Login/LoginView.swift @@ -41,12 +41,12 @@ struct LoginView: SwiftUI.View { VStack { TextField(viewModel.emailPlaceholder, text: $emailText.value) .autocapitalization(.none) - .accessibilityLabel(viewModel.emailPlaceholder) + .accessibilityLabel(Strings.TextFieldTag.shared.email) Text(emailErrorText.value) .foregroundColor(Color.red) SecureField(viewModel.passwordPlaceholder, text: $passwordText.value) .autocapitalization(.none) - .accessibilityLabel(viewModel.passwordPlaceholder) + .accessibilityLabel(Strings.TextFieldTag.shared.password) Text(passwordErrorText.value) .foregroundColor(Color.red) @@ -61,12 +61,13 @@ struct LoginView: SwiftUI.View { Text(viewModel.buttonTitle) } .disabled(!isButtonEnabled.value) - .accessibilityLabel(viewModel.buttonTitle) + .accessibilityLabel(Strings.ButtonTag.shared.login) Spacer() } .toolbar { ToolbarItem(placement: .principal) { Text(viewModel.screenTitle) + .accessibilityLabel(Strings.ScreenTag.shared.login) } }.navigationBarTitleDisplayMode(.inline) } From 5052d8f876372dbe3ffcf4126ed351fae3ebe015 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 20 Sep 2023 16:36:59 +0200 Subject: [PATCH 06/32] Add simple stub implementation for Node --- ...ationTest.kt => ApplicationAdapterTest.kt} | 11 +++--- ...tubCMApplication.kt => StubApplication.kt} | 13 +++++++ .../corrado4eyes/pistakio/mocks/StubNode.kt | 39 +++++++++++++++++++ 3 files changed, 58 insertions(+), 5 deletions(-) rename pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/{CMApplicationTest.kt => ApplicationAdapterTest.kt} (80%) rename pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/{StubCMApplication.kt => StubApplication.kt} (59%) create mode 100644 pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubNode.kt diff --git a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/CMApplicationTest.kt b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/ApplicationAdapterTest.kt similarity index 80% rename from pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/CMApplicationTest.kt rename to pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/ApplicationAdapterTest.kt index a51df09..1309529 100644 --- a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/CMApplicationTest.kt +++ b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/ApplicationAdapterTest.kt @@ -1,6 +1,6 @@ package com.corrado4eyes.pistakio -import com.corrado4eyes.pistakio.mocks.StubCMApplication +import com.corrado4eyes.pistakio.mocks.StubApplication import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -8,17 +8,18 @@ import kotlin.test.assertFailsWith class CMApplicationTest { - private lateinit var app: StubCMApplication + private lateinit var app: StubApplication @BeforeTest fun setUp() { - app = StubCMApplication() + app = StubApplication() } @Test fun test_launch_app() { - app.launch() + app.launch(identifier = null, arguments = mapOf("foo" to "bar", "baz" to qux)) assertEquals(1, app.launchCalled) + } @Test @@ -27,7 +28,7 @@ class CMApplicationTest { assertEquals(1, app.launchCalled) app.findView(tag = "test") - assertEquals(1, app.findView) + assertEquals(1, app.findViewCalled) assertEquals("test", app.findViewWithTag) } diff --git a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubCMApplication.kt b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubApplication.kt similarity index 59% rename from pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubCMApplication.kt rename to pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubApplication.kt index 36dbdac..5feaa5d 100644 --- a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubCMApplication.kt +++ b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubApplication.kt @@ -1,6 +1,8 @@ package com.corrado4eyes.pistakio.mocks +import com.corrado4eyes.pistakio.AssertionResult import com.corrado4eyes.pistakio.BaseApplicationAdapter +import com.corrado4eyes.pistakio.TimeoutDuration class StubCMApplication : BaseApplicationAdapter() { var launchCalled = 0 @@ -17,6 +19,17 @@ class StubCMApplication : BaseApplicationAdapter() { findView++ } + override fun assert(assertionResult: AssertionResult) { + TODO("Not yet implemented") + } + + override fun assertUntil( + timeout: TimeoutDuration, + blockAssertionResult: () -> AssertionResult + ) { + TODO("Not yet implemented") + } + var tearDown = 0 override fun tearDown() { super.tearDown() diff --git a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubNode.kt b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubNode.kt new file mode 100644 index 0000000..980653e --- /dev/null +++ b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubNode.kt @@ -0,0 +1,39 @@ +package com.corrado4eyes.pistakio.mocks + +import com.corrado4eyes.pistakio.AssertionResult +import com.corrado4eyes.pistakio.Node +import com.corrado4eyes.pistakio.TimeoutDuration + +class StubNode : Node { + override fun typeText(text: String) { + TODO("Not yet implemented") + } + + override fun tap() { + TODO("Not yet implemented") + } + + override fun exists(): AssertionResult { + TODO("Not yet implemented") + } + + override fun waitExists(timeout: TimeoutDuration): AssertionResult { + TODO("Not yet implemented") + } + + override fun isVisible(): AssertionResult { + TODO("Not yet implemented") + } + + override fun isButton(): AssertionResult { + TODO("Not yet implemented") + } + + override fun isTextEqualTo(value: String, contains: Boolean): AssertionResult { + TODO("Not yet implemented") + } + + override fun isHintEqualTo(value: String, contains: Boolean): AssertionResult { + TODO("Not yet implemented") + } +} \ No newline at end of file From ea46cd7948c7feb63861bb74636f59ae2cd33528 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 20 Sep 2023 16:37:27 +0200 Subject: [PATCH 07/32] Update tests --- .../pistakio/ApplicationAdapterTest.kt | 27 +++++++---- .../pistakio/mocks/StubApplication.kt | 46 ++++++++++++++----- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/ApplicationAdapterTest.kt b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/ApplicationAdapterTest.kt index 1309529..e80a8b5 100644 --- a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/ApplicationAdapterTest.kt +++ b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/ApplicationAdapterTest.kt @@ -1,12 +1,13 @@ package com.corrado4eyes.pistakio import com.corrado4eyes.pistakio.mocks.StubApplication +import com.corrado4eyes.pistakio.mocks.StubNode import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith -class CMApplicationTest { +class ApplicationAdapterTest { private lateinit var app: StubApplication @@ -17,35 +18,41 @@ class CMApplicationTest { @Test fun test_launch_app() { - app.launch(identifier = null, arguments = mapOf("foo" to "bar", "baz" to qux)) + app.launch( + identifier = null, + arguments = mapOf( + "foo" to "bar", + "baz" to "qux" + ) + ) assertEquals(1, app.launchCalled) - + assertEquals(mapOf("foo" to "bar", "baz" to "qux"), app.launchArguments) } @Test fun test_launch_app_find_view() { - app.launch() + app.launch(identifier = null, arguments = mapOf()) assertEquals(1, app.launchCalled) - + app.nodeToReturn = StubNode() app.findView(tag = "test") assertEquals(1, app.findViewCalled) - assertEquals("test", app.findViewWithTag) + assertEquals("test", app.findViewCalledWith) } @Test fun test_app_launched_twice() { assertFailsWith { - app.launch() - app.launch() + app.launch(identifier = null, arguments = mapOf()) + app.launch(identifier = null, arguments = mapOf()) } } @Test fun test_app_launched_tear_down_app_launched_again() { - app.launch() + app.launch(identifier = null, arguments = mapOf()) assertEquals(1, app.launchCalled) app.tearDown() - app.launch() + app.launch(identifier = null, arguments = mapOf()) assertEquals(2, app.launchCalled) } diff --git a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubApplication.kt b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubApplication.kt index 5feaa5d..81acb5c 100644 --- a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubApplication.kt +++ b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubApplication.kt @@ -1,33 +1,57 @@ package com.corrado4eyes.pistakio.mocks +import com.corrado4eyes.pistakio.ApplicationArguments import com.corrado4eyes.pistakio.AssertionResult import com.corrado4eyes.pistakio.BaseApplicationAdapter +import com.corrado4eyes.pistakio.Node import com.corrado4eyes.pistakio.TimeoutDuration -class StubCMApplication : BaseApplicationAdapter() { +class StubApplication : BaseApplicationAdapter() { var launchCalled = 0 - override fun launch() { - super.launch() + var launchArguments: ApplicationArguments = emptyMap() + override fun launch(identifier: String?, arguments: Map) { + super.launch(identifier, arguments) launchCalled++ + launchArguments = arguments } - var findView = 0 - var findViewWithTag: String? = null - override fun findView(tag: String) { - super.findView(tag) - findViewWithTag = tag - findView++ + var findViewCalled = 0 + var findViewCalledWith: String? = null + var nodeToReturn: Node? = null + override fun findView(tag: String): Node { + assertAppIsRunning() + findViewCalled++ + findViewCalledWith = tag + return nodeToReturn ?: throw IllegalStateException( + "You must set first the node to return" + ) } + var assertCalled = 0 + var assertCalledWith: AssertionResult? = null override fun assert(assertionResult: AssertionResult) { - TODO("Not yet implemented") + assertCalled++ + assertCalledWith = assertionResult } + var assertUntilCalled = 0 override fun assertUntil( timeout: TimeoutDuration, blockAssertionResult: () -> AssertionResult ) { - TODO("Not yet implemented") + assertUntilCalled++ + } + + var assertAllCalled = 0 + var assertAllRunTimes = 0 + private val _assertAllResult = mutableListOf() + val assertAllResult = _assertAllResult.toList() + override fun assertAll(assertions: List) { + assertAllCalled++ + assertions.forEach { + assertAllRunTimes++ + _assertAllResult.add(it) + } } var tearDown = 0 From 9a8435da7cb68437c3406cc0e8e1d4f884dc4f69 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 20 Sep 2023 16:38:29 +0200 Subject: [PATCH 08/32] Update TestCase class with sealed class and cases --- .../test/StepDefinitions.kt | 70 ++----- .../cucumbershared/tests/TestCase.kt | 174 +++++++++++++++++- ios/CucumberTests/CucumberTests.swift | 129 ++++--------- 3 files changed, 225 insertions(+), 148 deletions(-) diff --git a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt index 5d43098..8cfc9c3 100644 --- a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt +++ b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt @@ -2,11 +2,9 @@ package com.corrado4eyes.cucumberplayground.test import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.core.app.ActivityScenario -import com.corrado4eyes.pistakio.DefaultApplicationAdapter -import com.corrado4eyes.pistakio.TimeoutDuration -import com.corrado4eyes.pistakio.errors.UIElementException -import com.corrado4eyes.cucumberplayground.models.Strings import com.corrado4eyes.cucumbershared.tests.Definitions +import com.corrado4eyes.cucumbershared.tests.SealedDefinitions +import com.corrado4eyes.pistakio.DefaultApplicationAdapter import io.cucumber.java8.En import io.cucumber.junit.WithJunitRule import org.junit.Rule @@ -26,69 +24,33 @@ class StepDefinitions : En { val definitionString = it.definition.definitionString when (it) { Definitions.I_AM_IN_THE_EXPECT_VALUE_STRING_SCREEN -> Given(definitionString) { screenName: String -> - val screenTitleTag = when (screenName) { - Strings.Screen.Title.login -> { - arguments["isLoggedIn"] = "false" - arguments["testEmail"] = "" - Strings.Screen.Tag.login - } - - Strings.Screen.Title.home -> { - arguments["isLoggedIn"] = "true" - Strings.Screen.Tag.home - } - - else -> throw UIElementException.Screen.NotFound(screenName) - } - - application.launch("MainActivity", arguments) - val element = application.findView(screenTitleTag) - - application.assert(element.waitExists(TimeoutDuration.SHORT)) + val assertions = SealedDefinitions.IAmInScreen(application, listOf(screenName)).runAndGetAssertions() + application.assertAll(assertions) } Definitions.I_SEE_EXPECT_VALUE_STRING_TEXT -> Then(definitionString) { textViewTitle: String -> - val element = application.findView(textViewTitle) - application.assert(element.waitExists(TimeoutDuration.SHORT)) - application.assert(element.isVisible()) + val assertions = SealedDefinitions.ISeeText(application, listOf(textViewTitle)).runAndGetAssertions() + application.assertAll(assertions) } Definitions.I_SEE_THE_EXPECT_VALUE_STRING_BUTTON -> Then(definitionString) { buttonTitle: String -> - val element = when (buttonTitle) { - Strings.Button.Title.login -> application.findView(Strings.Button.Tag.login)/*testRule.onNodeWithText(Strings.Button.Text.login)*/ - Strings.Button.Title.logout -> application.findView(Strings.Button.Tag.logout)/*testRule.onNodeWithTag(Strings.Button.Text.logout)*/ - else -> throw UIElementException.Button.NotFound(buttonTitle) - } - application.assert(element.waitExists(TimeoutDuration.SHORT)) - application.assert(element.isVisible()) - application.assert(element.isButton()) + val assertions = SealedDefinitions.ISeeButton(application, listOf(buttonTitle)).runAndGetAssertions() + application.assertAll(assertions) } Definitions.I_SEE_THE_EXPECT_VALUE_STRING_SCREEN -> Then(definitionString) { screenTitle: String -> - val element = when (screenTitle) { - Strings.Screen.Title.login -> application.findView(Strings.Screen.Tag.login)/*testRule.onNodeWithTag(Strings.Screen.Title.login).assertIsDisplayed()*/ - Strings.Screen.Title.home -> application.findView(Strings.Screen.Tag.home)/*testRule.onNodeWithTag(Strings.Screen.Title.home).assertIsDisplayed()*/ - else -> throw UIElementException.Screen.NotFound(screenTitle) - } - application.assertUntil(TimeoutDuration.SHORT, element::exists) + val assertions = SealedDefinitions.ISeeScreen(application, listOf(screenTitle)).runAndGetAssertions() + application.assertAll(assertions) } Definitions.I_SEE_THE_EXPECT_VALUE_STRING_TEXT_FIELD_WITH_TEXT_EXPECT_VALUE_STRING -> Then(definitionString) { textFieldTag: String, textFieldText: String -> - val element = application.findView(textFieldTag) - application.assert(element.waitExists(TimeoutDuration.SHORT)) - application.assert(element.isHintEqualTo(textFieldText, true)) + val assertions = SealedDefinitions.ISeeTextFieldWithText(application, listOf(textFieldTag, textFieldText)).runAndGetAssertions() + application.assertAll(assertions) } Definitions.I_TYPE_EXPECT_VALUE_STRING_IN_THE_EXPECT_VALUE_STRING_TEXT_FIELD -> When(definitionString) { textInput: String, tag: String, -> - application.findView(tag).typeText(textInput) - } - Definitions.I_TYPE_EXPECT_VALUE_STRING_IN_THE_EXPECT_VALUE_STRING_SECURE_TEXT_FIELD -> When(definitionString) { textInput: String, tag: String -> - application.findView(tag).typeText(textInput) + val assertions = SealedDefinitions.ITypeTextIntoTextField(application, listOf(textInput, tag)).runAndGetAssertions() + application.assertAll(assertions) } Definitions.I_PRESS_THE_EXPECT_VALUE_STRING_BUTTON -> When(definitionString) { buttonTag: String -> - val element = when(buttonTag) { - Strings.Button.Title.login -> application.findView(Strings.Button.Tag.login) - Strings.Button.Title.logout -> application.findView(Strings.Button.Tag.logout) - else -> throw UIElementException.Button.NotFound(buttonTag) - } - application.assert(element.waitExists(TimeoutDuration.SHORT)) - element.tap() + val assertions = SealedDefinitions.IPressTheButton(application, listOf(buttonTag)).runAndGetAssertions() + application.assertAll(assertions) } Definitions.EMAIL_IS_EXPECT_VALUE_STRING -> Given(definitionString) { loggedInEmail: String -> arguments["testEmail"] = loggedInEmail 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 d811af6..0d87c20 100644 --- a/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt +++ b/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt @@ -4,6 +4,11 @@ import com.corrado4eyes.cucumber.CucumberDefinition import com.corrado4eyes.cucumber.Definition import com.corrado4eyes.cucumber.EXPECT_VALUE_STRING import com.corrado4eyes.cucumber.GherkinTestCase +import com.corrado4eyes.cucumberplayground.models.Strings +import com.corrado4eyes.pistakio.ApplicationAdapter +import com.corrado4eyes.pistakio.AssertionResult +import com.corrado4eyes.pistakio.TimeoutDuration +import com.corrado4eyes.pistakio.errors.UIElementException enum class Definitions(override val definition: Definition): GherkinTestCase { I_AM_IN_THE_EXPECT_VALUE_STRING_SCREEN( @@ -24,9 +29,6 @@ enum class Definitions(override val definition: Definition): GherkinTestCase = Definitions.values().toList() } } + +sealed class SealedDefinitions( + protected val application: ApplicationAdapter, + args: List? +) { + abstract val definition: Definition + abstract fun runAndGetAssertions(): List + + protected val args = args ?: emptyList() + + class IAmInScreen( + application: ApplicationAdapter, + args: List? + ) : SealedDefinitions(application, args) { + override val definition: Definition = CucumberDefinition.Step.Given("I am in the $EXPECT_VALUE_STRING screen") + + override fun runAndGetAssertions(): List { + val arguments = mutableMapOf() + + val screenTitleTag = when (val screenName = args[0]) { + Strings.Screen.Title.login -> { + arguments["isLoggedIn"] = "false" // platform specific + arguments["testEmail"] = "" + Strings.Screen.Tag.login + } + + Strings.Screen.Title.home -> { + arguments["isLoggedIn"] = "true" + arguments["testEmail"] = "test@test.com" + Strings.Screen.Tag.home + } + else -> throw UIElementException.Screen.NotFound(screenName) + } + + application.launch(null, arguments) + val element = application.findView(screenTitleTag) + return listOf( + element.waitExists(TimeoutDuration.SHORT) + ) + } + } + + class ISeeText( + application: ApplicationAdapter, + args: List? + ) : SealedDefinitions(application, args) { + override val definition: Definition = CucumberDefinition.Step.Then( + "I see $EXPECT_VALUE_STRING text" + ) + + override fun runAndGetAssertions(): List { + val textViewTitle = args[0] + val element = application.findView(textViewTitle) + return listOf(element.waitExists(TimeoutDuration.SHORT), element.isVisible()) + } + } + + class ISeeButton( + application: ApplicationAdapter, + args: List? + ) : SealedDefinitions(application, args) { + override val definition: Definition = CucumberDefinition.Step.Then( + "I see the $EXPECT_VALUE_STRING button" + ) + + override fun runAndGetAssertions(): List { + val element = when (val buttonTitle = args[0]) { + Strings.Button.Title.login -> application.findView(Strings.Button.Tag.login) + Strings.Button.Title.logout -> application.findView(Strings.Button.Tag.logout) + else -> throw UIElementException.Button.NotFound(buttonTitle) + } + return listOf( + element.waitExists(TimeoutDuration.SHORT), + element.isVisible(), + element.isButton() + ) + } + + } + class ISeeScreen( + application: ApplicationAdapter, + args: List? + ) : SealedDefinitions(application, args) { + override val definition: Definition = CucumberDefinition.Step.Then("I see the $EXPECT_VALUE_STRING screen") + + override fun runAndGetAssertions(): List { + val element = when (val screenTitle = args[0]) { + Strings.Screen.Title.login -> application.findView(Strings.Screen.Tag.login) + Strings.Screen.Title.home -> application.findView(Strings.Screen.Tag.home) + else -> throw UIElementException.Screen.NotFound(screenTitle) + } + return listOf(element.exists()) + } + } + + class ISeeTextFieldWithText( + application: ApplicationAdapter, + args: List? + ) : SealedDefinitions(application, args) { + override val definition: Definition = CucumberDefinition.Step.Then("I see the $EXPECT_VALUE_STRING textfield with text $EXPECT_VALUE_STRING") + + override fun runAndGetAssertions(): List { + val textFieldTag = args[1] + val textFieldText = args[1] + val element = application.findView(textFieldTag) + return listOf( + element.waitExists(TimeoutDuration.SHORT), + element.isHintEqualTo(textFieldText, true) + ) + } + } + + class ITypeTextIntoTextField( + application: ApplicationAdapter, + args: List? + ) : SealedDefinitions(application, args) { + override val definition: Definition = CucumberDefinition.Step.When( + "I type $EXPECT_VALUE_STRING in the $EXPECT_VALUE_STRING textfield" + ) + + override fun runAndGetAssertions(): List { + val textInput = args[0] + val tag = args[1] + application + .findView(tag) + .typeText(textInput) + return emptyList() + } + } + + class IPressTheButton( + application: ApplicationAdapter, + args: List? + ) : SealedDefinitions(application, args) { + override val definition: Definition = CucumberDefinition.Step.When( + "I press the $EXPECT_VALUE_STRING button" + ) + + override fun runAndGetAssertions(): List { + val element = when(val buttonTag = args[0]) { + Strings.Button.Title.login -> application.findView(Strings.Button.Tag.login) + Strings.Button.Title.logout -> application.findView(Strings.Button.Tag.logout) + else -> throw UIElementException.Button.NotFound(buttonTag) + } + application.assert(element.waitExists(TimeoutDuration.SHORT)) + element.tap() + return emptyList() + } + } + + class SetLoggedInUserEmail( + application: ApplicationAdapter, + args: List? + ) : SealedDefinitions(application, args) { + override val definition: Definition = CucumberDefinition.Step.Given( + "Email is $EXPECT_VALUE_STRING" + ) + + override fun runAndGetAssertions(): List { + val loggedInEmail = args[0] +// appArguments?.toMutableMap()?.set("testEmail", loggedInEmail) +// application. + return emptyList() // TODO: Fix issue with arguments + } + } +} diff --git a/ios/CucumberTests/CucumberTests.swift b/ios/CucumberTests/CucumberTests.swift index aab3e7c..540c20e 100644 --- a/ios/CucumberTests/CucumberTests.swift +++ b/ios/CucumberTests/CucumberTests.swift @@ -15,11 +15,17 @@ import Cucumberish static var app: XCUIApplication! static var applicationAdapter: ApplicationAdapter! + @objc public class func assertAll(assertions: [AssertionResult]) { + for assertion in assertions { + XCTAssert(assertion is AssertionResult.Success) + } + } + @objc public class func CucumberishSwiftInit() { beforeStart { app = XCUIApplication() - applicationAdapter = DefaultApplicationAdapter(app: app) app.launchArguments.append("test") + applicationAdapter = DefaultApplicationAdapter(app: app) } afterFinish { @@ -30,116 +36,57 @@ import Cucumberish let definitionString = test.definition.definitionString switch test { case .iAmInTheExpectValueStringScreen: Given(definitionString) { args, userInfo in - guard let screenTitle = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } - - let screenTag: String - switch screenTitle { - case Strings.ScreenTitle.shared.home: - app.launchEnvironment["isLoggedIn"] = "true" - screenTag = Strings.ScreenTag.shared.home - case Strings.ScreenTitle.shared.login: - app.launchEnvironment["isLoggedIn"] = "false" - screenTag = Strings.ScreenTag.shared.login - - default: - screenTag = "Fail" - XCTFail("Couldn't find \(screenTag) screen") - } - - applicationAdapter.launch(identifier: nil, arguments: app.launchEnvironment) + let assertions = SealedDefinitions.IAmInScreen( + application: applicationAdapter, + args: args + ).runAndGetAssertions() - let element = applicationAdapter.findView(tag: screenTag) - - element.assert(assertionResult: element.waitExists(timeout: .short_), message: "Couldn't validate to be in \(screenTitle)") + assertAll(assertions: assertions) } case .iSeeExpectValueStringText: Then(definitionString) { args, userInfo in - guard let textViewTitle = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } - let element = applicationAdapter.findView(tag: textViewTitle) + let assertions = SealedDefinitions.ISeeText( + application: applicationAdapter, + args: args + ).runAndGetAssertions() - element.assert(assertionResult: element.waitExists(timeout: .short_), message: "Couldn't find \(element) text") + assertAll(assertions: assertions) } case .iSeeTheExpectValueStringButton: Then(definitionString) { args, userInfo in - guard let buttonTitle = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } - - let buttonTag: String - switch buttonTitle { - case Strings.ButtonTitle.shared.login: - buttonTag = Strings.ButtonTag.shared.login - case Strings.ButtonTitle.shared.logout: - buttonTag = Strings.ButtonTag.shared.logout - default: - buttonTag = "Fail" - XCTFail("Couldn't find \(buttonTitle) button") - } + let assertions = SealedDefinitions.ISeeButton( + application: applicationAdapter, + args: args + ).runAndGetAssertions() - let element = applicationAdapter.findView(tag: buttonTag) - element.assert(assertionResult: element.waitExists(timeout: .short_), message: "Couldn't find \(buttonTitle) button") - - element.assert(assertionResult: element.isButton(), message: "Couldn't validate that \(buttonTitle) is a button") + assertAll(assertions: assertions) } case .iSeeTheExpectValueStringScreen: Then(definitionString) { args, userInfo in - guard let screenTitle = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } - - let screenTag: String - switch screenTitle { - case Strings.ScreenTitle.shared.home: - screenTag = Strings.ScreenTag.shared.home - case Strings.ScreenTitle.shared.login: - screenTag = Strings.ScreenTag.shared.login - default: - screenTag = "Fail" - XCTFail("Couldn't find \(screenTitle) screen") - } + let assertions = SealedDefinitions.ISeeScreen( + application: applicationAdapter, + args: args + ).runAndGetAssertions() - let element = applicationAdapter.findView(tag: screenTag) - let elementExists = element.waitExists(timeout: .long_) - element.assert(assertionResult: elementExists, message: "Couldn't find \(screenTag) screen") + assertAll(assertions: assertions) } case .iSeeTheExpectValueStringTextFieldWithTextExpectValueString: Then(definitionString) { args, userInfo in - guard let textFieldTag = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } - guard let textFieldText = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 1) else { return } - - let element = applicationAdapter.findView(tag: textFieldTag) - - element.assert(assertionResult: element.waitExists(timeout: .short_), message: "Couldn't find \(textFieldTag) field") + let assertions = SealedDefinitions.ISeeTextFieldWithText( + application: applicationAdapter, + args: args + ).runAndGetAssertions() - element.assert(assertionResult: element.isHintEqualTo(value: textFieldText, contains: true), message: "TextField value doesn't match \(textFieldText)") + assertAll(assertions: assertions) } case .iTypeExpectValueStringInTheExpectValueStringTextField: When(definitionString) { args, userInfo in - guard let textfieldTag = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 1) else { return } - guard let textfieldText = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } - - let element = applicationAdapter.findView(tag: textfieldTag) - element.typeText(text: textfieldText) - } - case .iTypeExpectValueStringInTheExpectValueStringSecureTextField: When(definitionString) { args, userInfo in - guard let textfieldTag = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 1) else { return } - guard let textfieldText = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } - - let element = applicationAdapter.findView(tag: textfieldTag) - element.typeText(text: textfieldText) + let assertions = SealedDefinitions.ITypeTextIntoTextField(application: applicationAdapter, args: args).runAndGetAssertions() + assertAll(assertions: assertions) } case .iPressTheExpectValueStringButton: When(definitionString) { args, userInfo in - guard let buttonTitle = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } - let buttonTag: String - switch buttonTitle { - case Strings.ButtonTitle.shared.login: - buttonTag = Strings.ButtonTag.shared.login - case Strings.ButtonTitle.shared.logout: - buttonTag = Strings.ButtonTag.shared.logout - default: - buttonTag = "Fail" - XCTFail("I press the \(buttonTitle) button failed") - } - - let element = applicationAdapter.findView(tag: buttonTag) - element.tap() + let assertions = SealedDefinitions.IPressTheButton(application: applicationAdapter, args: args).runAndGetAssertions() + assertAll(assertions: assertions) } case .emailIsExpectValueString: Given(definitionString) { args, userInfo in - guard let email = ApplicationAdapterCompanion.shared.getArgument(arguments: args, index: 0) else { return } - app.launchEnvironment["testEmail"] = email - return + let assertions = SealedDefinitions.SetLoggedInUserEmail(application: applicationAdapter, args: args).runAndGetAssertions() + assertAll(assertions: assertions) } default: XCTFail("unrecognised test case.") From 4fb3b115cade1af30ebf2e9071c8b0bf4b6c72fe Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 20 Sep 2023 16:39:13 +0200 Subject: [PATCH 09/32] Update assertion library --- .../corrado4eyes/pistakio/DefaultCMApplication.kt | 10 +++++++++- .../com/corrado4eyes/pistakio/ApplicationAdapter.kt | 7 ++----- .../corrado4eyes/pistakio/CMApplicationAdapter.kt | 12 ++++++++++++ .../kotlin/com/corrado4eyes/pistakio/DefaultNode.kt | 7 +++++-- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt index 31be165..b45df15 100644 --- a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt +++ b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt @@ -17,7 +17,7 @@ actual class DefaultApplicationAdapter( val instrumentation = InstrumentationRegistry.getInstrumentation() val appPackage = instrumentation.targetContext.packageName - val activityName = "$appPackage.$identifier" + val activityName = "$appPackage.MainActivity" val intent = Intent(Intent.ACTION_MAIN) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.setClassName(instrumentation.targetContext, activityName) @@ -51,4 +51,12 @@ actual class DefaultApplicationAdapter( blockAssertionResult() is AssertionResult.Success } } + + override fun assertAll(assertions: List) { + assertions.forEach { + testRule.waitUntil(TimeoutDuration.LONG.duration.toLong(DurationUnit.MILLISECONDS)) { + it is AssertionResult.Success + } + } + } } \ No newline at end of file diff --git a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt index bd41021..348c4f2 100644 --- a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt @@ -18,20 +18,17 @@ enum class TimeoutDuration(val duration: Duration) { LONG(60.seconds) } -typealias ApplicationArguments = List +typealias ApplicationArguments = Map? interface ApplicationAdapter { - companion object { - fun getArgument(arguments: List?, index: Int): String? = arguments?.get(index) - } - fun launch(identifier: String? = null, arguments: Map) fun findView(tag: String): Node fun tearDown() fun assert(assertionResult: AssertionResult) fun assertUntil(timeout: TimeoutDuration, blockAssertionResult: () -> AssertionResult) + fun assertAll(assertions: List) interface TearDownHandler { fun tearDown() diff --git a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt index 9aa0032..2924fca 100644 --- a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt +++ b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt @@ -8,6 +8,7 @@ actual class DefaultApplicationAdapter(app: XCUIApplication?) : BaseApplicationA override fun launch(identifier: String?, arguments: Map) { super.launch(identifier, arguments) + app.setLaunchEnvironment((arguments as Map)) app.launch() } @@ -20,6 +21,9 @@ actual class DefaultApplicationAdapter(app: XCUIApplication?) : BaseApplicationA return DefaultNode(element) } + /** + * Not called from iOS + */ override fun assert(assertionResult: AssertionResult) { when (assertionResult) { is AssertionResult.Failure -> throw assertionResult.exception @@ -27,8 +31,16 @@ actual class DefaultApplicationAdapter(app: XCUIApplication?) : BaseApplicationA } } + /** + * Not called from iOS + */ override fun assertUntil( timeout: TimeoutDuration, blockAssertionResult: () -> AssertionResult ) = assert(blockAssertionResult()) + + /** + * Not called from iOS + */ + override fun assertAll(assertions: List) = assert(assertions.first()) } \ No newline at end of file diff --git a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt index 1c22cf8..4fccc49 100644 --- a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt +++ b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt @@ -20,7 +20,10 @@ actual class DefaultNode(private val element: XCUIElement) : Node { handler = null ) - val result = XCTWaiter().waitForExpectations(expectations = listOf(expectation)) + val result = XCTWaiter().waitForExpectations( + expectations = listOf(expectation), + timeout = TimeoutDuration.SHORT.duration.toDouble(DurationUnit.SECONDS) + ) return assertionResultFor { result == XCTWaiterResultCompleted } } @@ -31,7 +34,7 @@ actual class DefaultNode(private val element: XCUIElement) : Node { ) } - override fun isVisible(): AssertionResult = exists() + override fun isVisible(): AssertionResult = waitExists(TimeoutDuration.SHORT) override fun isButton(): AssertionResult = assertionResultFor(element::isHittable) From bef8a79bcba690d2d1f7c0723c9ff7f3a7f88045 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 20 Sep 2023 16:39:31 +0200 Subject: [PATCH 10/32] Remove prints and update feature file --- ios/CucumberTests/Features/Login.feature | 2 +- ios/ios/ContentView.swift | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/ios/CucumberTests/Features/Login.feature b/ios/CucumberTests/Features/Login.feature index 4290305..f4d1263 100644 --- a/ios/CucumberTests/Features/Login.feature +++ b/ios/CucumberTests/Features/Login.feature @@ -5,7 +5,7 @@ Feature: Login And I see the "Password" textfield with text "Password" And I see the "Login" button When I type "test@test.com" in the "Email" textfield - And I type "1234" in the "Password" secure textfield + And I type "1234" in the "Password" textfield And I press the "Login" button Then I see the "Home" screen And I see "test@test.com" text diff --git a/ios/ios/ContentView.swift b/ios/ios/ContentView.swift index 4a5d865..9507fda 100644 --- a/ios/ios/ContentView.swift +++ b/ios/ios/ContentView.swift @@ -16,11 +16,7 @@ struct ContentView: View { init() { let testConfiguration: TestConfiguration? = { let arguments = CommandLine.arguments - guard arguments.contains("test") else { - return nil - } let tc = DefaultTestConfiguration(configuration: ProcessInfo.processInfo.environment) - print(tc) return tc }() let mainViewModel = MainViewModel(testConfiguration: testConfiguration) From 8946885fcbd659c0daad114f074fdaedb1713c3d Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Mon, 25 Sep 2023 08:52:29 +0200 Subject: [PATCH 11/32] Use identifier in android source set instead of Hardcoded string. Add map to set application arguments. --- .../pistakio/DefaultCMApplication.kt | 2 +- .../pistakio/ApplicationAdapter.kt | 29 ++++++++++++++----- .../pistakio/mocks/StubApplication.kt | 1 + .../pistakio/CMApplicationAdapter.kt | 1 + 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt index b45df15..e0086a6 100644 --- a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt +++ b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt @@ -17,7 +17,7 @@ actual class DefaultApplicationAdapter( val instrumentation = InstrumentationRegistry.getInstrumentation() val appPackage = instrumentation.targetContext.packageName - val activityName = "$appPackage.MainActivity" + val activityName = "$appPackage.$identifier" val intent = Intent(Intent.ACTION_MAIN) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.setClassName(instrumentation.targetContext, activityName) diff --git a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt index 348c4f2..b677d9c 100644 --- a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt @@ -2,7 +2,6 @@ package com.corrado4eyes.pistakio import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.flow.MutableStateFlow class CMAppLaunchedAlreadyException : Throwable() { override val message: String = "The app is already launched" @@ -18,10 +17,12 @@ enum class TimeoutDuration(val duration: Duration) { LONG(60.seconds) } -typealias ApplicationArguments = Map? +typealias ApplicationArguments = Map interface ApplicationAdapter { + val applicationArguments: ApplicationArguments + fun launch(identifier: String? = null, arguments: Map) fun findView(tag: String): Node fun tearDown() @@ -33,27 +34,41 @@ interface ApplicationAdapter { interface TearDownHandler { fun tearDown() } + + operator fun get(key: String): String? + operator fun set(key: String, value: String) } abstract class BaseApplicationAdapter : ApplicationAdapter { - private val isAppLaunched = MutableStateFlow(false) + + private val _applicationArguments = mutableMapOf() + override val applicationArguments: ApplicationArguments + get() = _applicationArguments + + private var isAppLaunched = false override fun launch(identifier: String?, arguments: Map) { - if (isAppLaunched.value) { + if (isAppLaunched) { throw CMAppLaunchedAlreadyException() } - isAppLaunched.value = true + + isAppLaunched = true } override fun tearDown() { - isAppLaunched.value = false + isAppLaunched = false } protected fun assertAppIsRunning() { - if (!isAppLaunched.value) { + if (!isAppLaunched) { throw CMAppNotLaunchedYetException() } } + + override operator fun get(key: String): String? = applicationArguments[key] + override operator fun set(key: String, value: String) { + _applicationArguments[key] = value + } } expect class DefaultApplicationAdapter: BaseApplicationAdapter diff --git a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubApplication.kt b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubApplication.kt index 81acb5c..9309b90 100644 --- a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubApplication.kt +++ b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubApplication.kt @@ -7,6 +7,7 @@ import com.corrado4eyes.pistakio.Node import com.corrado4eyes.pistakio.TimeoutDuration class StubApplication : BaseApplicationAdapter() { + var launchCalled = 0 var launchArguments: ApplicationArguments = emptyMap() override fun launch(identifier: String?, arguments: Map) { diff --git a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt index 2924fca..9a86a3e 100644 --- a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt +++ b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt @@ -6,6 +6,7 @@ import platform.XCTest.XCUIElementTypeAny actual class DefaultApplicationAdapter(app: XCUIApplication?) : BaseApplicationAdapter() { private val app: XCUIApplication = app ?: XCUIApplication() + @Suppress("UNCHECKED_CAST") override fun launch(identifier: String?, arguments: Map) { super.launch(identifier, arguments) app.setLaunchEnvironment((arguments as Map)) From 9f08e913e07995b4aee36bc513a8a627fa18a220 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Mon, 25 Sep 2023 08:55:17 +0200 Subject: [PATCH 12/32] Add usage of application map for arguments --- .../cucumberplayground/test/StepDefinitions.kt | 2 +- .../cucumbershared/tests/TestCase.kt | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt index 8cfc9c3..373a150 100644 --- a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt +++ b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt @@ -53,7 +53,7 @@ class StepDefinitions : En { application.assertAll(assertions) } Definitions.EMAIL_IS_EXPECT_VALUE_STRING -> Given(definitionString) { loggedInEmail: String -> - arguments["testEmail"] = loggedInEmail + application.assertAll(SealedDefinitions.SetLoggedInUserEmail(application, listOf(loggedInEmail)).runAndGetAssertions()) } } } 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 0d87c20..2950f09 100644 --- a/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt +++ b/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt @@ -57,24 +57,21 @@ sealed class SealedDefinitions( override val definition: Definition = CucumberDefinition.Step.Given("I am in the $EXPECT_VALUE_STRING screen") override fun runAndGetAssertions(): List { - val arguments = mutableMapOf() val screenTitleTag = when (val screenName = args[0]) { Strings.Screen.Title.login -> { - arguments["isLoggedIn"] = "false" // platform specific - arguments["testEmail"] = "" + application["isLoggedIn"] = "false" + application["testEmail"] = "" Strings.Screen.Tag.login } Strings.Screen.Title.home -> { - arguments["isLoggedIn"] = "true" - arguments["testEmail"] = "test@test.com" + application["isLoggedIn"] = "true" Strings.Screen.Tag.home } else -> throw UIElementException.Screen.NotFound(screenName) } - - application.launch(null, arguments) + application.launch("MainActivity", application.applicationArguments) val element = application.findView(screenTitleTag) return listOf( element.waitExists(TimeoutDuration.SHORT) @@ -200,9 +197,8 @@ sealed class SealedDefinitions( override fun runAndGetAssertions(): List { val loggedInEmail = args[0] -// appArguments?.toMutableMap()?.set("testEmail", loggedInEmail) -// application. - return emptyList() // TODO: Fix issue with arguments + application["testEmail"] = loggedInEmail + return emptyList() } } } From 157d5b56e2808bacb5d92608155d23c1eb1f794d Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Mon, 25 Sep 2023 08:58:16 +0200 Subject: [PATCH 13/32] Add doc for applicationArguments --- .../kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt index b677d9c..e27620f 100644 --- a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt @@ -21,6 +21,11 @@ typealias ApplicationArguments = Map interface ApplicationAdapter { + /** + * Map of arguments passed through the application arguments into the app MainActivity or MainView. + * Such map contains arguments to set up the app in a state required by certain tests. Both the + * content of the map and the implementation is up to the developer. + */ val applicationArguments: ApplicationArguments fun launch(identifier: String? = null, arguments: Map) From d928217ffd36a99c1700fd8da0ad9f8ac2becee2 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Mon, 25 Sep 2023 08:59:30 +0200 Subject: [PATCH 14/32] Rename exceptions removing prefix --- .../com/corrado4eyes/pistakio/ApplicationAdapter.kt | 8 ++++---- .../com/corrado4eyes/pistakio/ApplicationAdapterTest.kt | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt index e27620f..e14b2f2 100644 --- a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt @@ -3,11 +3,11 @@ package com.corrado4eyes.pistakio import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -class CMAppLaunchedAlreadyException : Throwable() { +class AppLaunchedAlreadyException : Throwable() { override val message: String = "The app is already launched" } -class CMAppNotLaunchedYetException : Throwable() { +class AppNotLaunchedYetException : Throwable() { override val message: String = "The app was not launched" } @@ -54,7 +54,7 @@ abstract class BaseApplicationAdapter : ApplicationAdapter { override fun launch(identifier: String?, arguments: Map) { if (isAppLaunched) { - throw CMAppLaunchedAlreadyException() + throw AppLaunchedAlreadyException() } isAppLaunched = true @@ -66,7 +66,7 @@ abstract class BaseApplicationAdapter : ApplicationAdapter { protected fun assertAppIsRunning() { if (!isAppLaunched) { - throw CMAppNotLaunchedYetException() + throw AppNotLaunchedYetException() } } diff --git a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/ApplicationAdapterTest.kt b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/ApplicationAdapterTest.kt index e80a8b5..77c51a1 100644 --- a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/ApplicationAdapterTest.kt +++ b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/ApplicationAdapterTest.kt @@ -41,7 +41,7 @@ class ApplicationAdapterTest { @Test fun test_app_launched_twice() { - assertFailsWith { + assertFailsWith { app.launch(identifier = null, arguments = mapOf()) app.launch(identifier = null, arguments = mapOf()) } @@ -58,7 +58,7 @@ class ApplicationAdapterTest { @Test fun test_find_view_fails_must_launch_app_first() { - assertFailsWith { + assertFailsWith { app.findView(tag = "test") } } From b3aced6c2ae7416239c6e286213019d74b962086 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Tue, 26 Sep 2023 10:45:00 +0200 Subject: [PATCH 15/32] Update feature file --- android/src/androidTest/assets/features/Login.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/androidTest/assets/features/Login.feature b/android/src/androidTest/assets/features/Login.feature index 4290305..f4d1263 100644 --- a/android/src/androidTest/assets/features/Login.feature +++ b/android/src/androidTest/assets/features/Login.feature @@ -5,7 +5,7 @@ Feature: Login And I see the "Password" textfield with text "Password" And I see the "Login" button When I type "test@test.com" in the "Email" textfield - And I type "1234" in the "Password" secure textfield + And I type "1234" in the "Password" textfield And I press the "Login" button Then I see the "Home" screen And I see "test@test.com" text From ae07b354ad17270b69f3789dc5a5d6f64fc64be7 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Tue, 26 Sep 2023 10:45:58 +0200 Subject: [PATCH 16/32] Remove waitUntil from assertion method --- .../kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt index e0086a6..9b55ac9 100644 --- a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt +++ b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt @@ -54,9 +54,7 @@ actual class DefaultApplicationAdapter( override fun assertAll(assertions: List) { assertions.forEach { - testRule.waitUntil(TimeoutDuration.LONG.duration.toLong(DurationUnit.MILLISECONDS)) { - it is AssertionResult.Success - } + it is AssertionResult.Success } } } \ No newline at end of file From 3eccea140c11590e173f3ce1267f951c8e52b67e Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Tue, 26 Sep 2023 10:56:45 +0200 Subject: [PATCH 17/32] Rename files --- .../pistakio/{DefaultCMApplication.kt => ApplicationAdapter.kt} | 0 .../kotlin/com/corrado4eyes/pistakio/{DefaultNode.kt => Node.kt} | 0 .../pistakio/{CMApplicationAdapter.kt => ApplicationAdapter.kt} | 0 .../kotlin/com/corrado4eyes/pistakio/{DefaultNode.kt => Node.kt} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/{DefaultCMApplication.kt => ApplicationAdapter.kt} (100%) rename pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/{DefaultNode.kt => Node.kt} (100%) rename pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/{CMApplicationAdapter.kt => ApplicationAdapter.kt} (100%) rename pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/{DefaultNode.kt => Node.kt} (100%) diff --git a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt similarity index 100% rename from pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultCMApplication.kt rename to pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt diff --git a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/Node.kt similarity index 100% rename from pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt rename to pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/Node.kt diff --git a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt similarity index 100% rename from pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/CMApplicationAdapter.kt rename to pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt diff --git a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/Node.kt similarity index 100% rename from pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/DefaultNode.kt rename to pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/Node.kt From 379c97ff25784114fc45e33cf1c59b9948f7b012 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 27 Sep 2023 09:19:34 +0200 Subject: [PATCH 18/32] Extract interface --- .../kotlin/com/corrado4eyes/pistakio/TestCase.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/TestCase.kt diff --git a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/TestCase.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/TestCase.kt new file mode 100644 index 0000000..bb635d1 --- /dev/null +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/TestCase.kt @@ -0,0 +1,13 @@ +package com.corrado4eyes.pistakio + +/** + * To be used on the shared module to represent test cases. + * [TestCase.CrossPlatform] represents tests that can be done completely on the shared module, except for the assertion. + * [TestCase.Platform] instead represents tests that should be implemented completely on the platform. + */ +interface TestCase { + interface CrossPlatform : TestCase { + fun runAndGetAssertions(): List + } + interface Platform : TestCase +} \ No newline at end of file From 9f1a71a4521a7c128251c57e12231c48e1fbee4e Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 27 Sep 2023 09:20:03 +0200 Subject: [PATCH 19/32] Refactor so that TestCase interface is used. Also add platform specific test --- .../cucumbershared/tests/TestCase.kt | 278 +++++++++--------- 1 file changed, 146 insertions(+), 132 deletions(-) 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 2950f09..5fa37ab 100644 --- a/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt +++ b/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt @@ -7,6 +7,7 @@ import com.corrado4eyes.cucumber.GherkinTestCase import com.corrado4eyes.cucumberplayground.models.Strings import com.corrado4eyes.pistakio.ApplicationAdapter import com.corrado4eyes.pistakio.AssertionResult +import com.corrado4eyes.pistakio.TestCase import com.corrado4eyes.pistakio.TimeoutDuration import com.corrado4eyes.pistakio.errors.UIElementException @@ -34,6 +35,9 @@ enum class Definitions(override val definition: Definition): GherkinTestCase? -) { +sealed class AppDefinitions : TestCase { abstract val definition: Definition - abstract fun runAndGetAssertions(): List - - protected val args = args ?: emptyList() - class IAmInScreen( - application: ApplicationAdapter, + sealed class CrossPlatform( + protected val application: ApplicationAdapter, args: List? - ) : SealedDefinitions(application, args) { - override val definition: Definition = CucumberDefinition.Step.Given("I am in the $EXPECT_VALUE_STRING screen") + ) : TestCase.CrossPlatform, AppDefinitions() { + + protected val args = args ?: emptyList() + + class IAmInScreen( + application: ApplicationAdapter, + args: List? + ) : CrossPlatform(application, args) { + override val definition: Definition = CucumberDefinition.Step.Given("I am in the $EXPECT_VALUE_STRING screen") + + override fun runAndGetAssertions(): List { + + val screenTitleTag = when (val screenName = args[0]) { + Strings.Screen.Title.login -> { + application["isLoggedIn"] = "false" + application["testEmail"] = "" + Strings.Screen.Tag.login + } + + Strings.Screen.Title.home -> { + application["isLoggedIn"] = "true" + Strings.Screen.Tag.home + } + else -> throw UIElementException.Screen.NotFound(screenName) + } + application.launch("MainActivity", application.applicationArguments) + val element = application.findView(screenTitleTag) + return listOf( + element.waitExists(TimeoutDuration.SHORT) + ) + } + } + + class ISeeText( + application: ApplicationAdapter, + args: List? + ) : CrossPlatform(application, args) { + override val definition: Definition = CucumberDefinition.Step.Then( + "I see $EXPECT_VALUE_STRING text" + ) + + override fun runAndGetAssertions(): List { + val textViewTitle = args[0] + val element = application.findView(textViewTitle) + return listOf(element.waitExists(TimeoutDuration.SHORT), element.isVisible()) + } + } - override fun runAndGetAssertions(): List { + class ISeeButton( + application: ApplicationAdapter, + args: List? + ) : CrossPlatform(application, args) { + override val definition: Definition = CucumberDefinition.Step.Then( + "I see the $EXPECT_VALUE_STRING button" + ) - val screenTitleTag = when (val screenName = args[0]) { - Strings.Screen.Title.login -> { - application["isLoggedIn"] = "false" - application["testEmail"] = "" - Strings.Screen.Tag.login + override fun runAndGetAssertions(): List { + val element = when (val buttonTitle = args[0]) { + Strings.Button.Title.login -> application.findView(Strings.Button.Tag.login) + Strings.Button.Title.logout -> application.findView(Strings.Button.Tag.logout) + else -> throw UIElementException.Button.NotFound(buttonTitle) } + return listOf( + element.waitExists(TimeoutDuration.SHORT), + element.isVisible(), + element.isButton() + ) + } - Strings.Screen.Title.home -> { - application["isLoggedIn"] = "true" - Strings.Screen.Tag.home + } + class ISeeScreen( + application: ApplicationAdapter, + args: List? + ) : CrossPlatform(application, args) { + override val definition: Definition = CucumberDefinition.Step.Then("I see the $EXPECT_VALUE_STRING screen") + + override fun runAndGetAssertions(): List { + val element = when (val screenTitle = args[0]) { + Strings.Screen.Title.login -> application.findView(Strings.Screen.Tag.login) + Strings.Screen.Title.home -> application.findView(Strings.Screen.Tag.home) + else -> throw UIElementException.Screen.NotFound(screenTitle) } - else -> throw UIElementException.Screen.NotFound(screenName) + return listOf(element.exists()) } - application.launch("MainActivity", application.applicationArguments) - val element = application.findView(screenTitleTag) - return listOf( - element.waitExists(TimeoutDuration.SHORT) - ) } - } - class ISeeText( - application: ApplicationAdapter, - args: List? - ) : SealedDefinitions(application, args) { - override val definition: Definition = CucumberDefinition.Step.Then( - "I see $EXPECT_VALUE_STRING text" - ) - - override fun runAndGetAssertions(): List { - val textViewTitle = args[0] - val element = application.findView(textViewTitle) - return listOf(element.waitExists(TimeoutDuration.SHORT), element.isVisible()) + class ISeeTextFieldWithText( + application: ApplicationAdapter, + args: List? + ) : CrossPlatform(application, args) { + override val definition: Definition = CucumberDefinition.Step.Then("I see the $EXPECT_VALUE_STRING textfield with text $EXPECT_VALUE_STRING") + + override fun runAndGetAssertions(): List { + val textFieldTag = args[1] + val textFieldText = args[1] + val element = application.findView(textFieldTag) + return listOf( + element.waitExists(TimeoutDuration.SHORT), + element.isHintEqualTo(textFieldText, true) + ) + } } - } - class ISeeButton( - application: ApplicationAdapter, - args: List? - ) : SealedDefinitions(application, args) { - override val definition: Definition = CucumberDefinition.Step.Then( - "I see the $EXPECT_VALUE_STRING button" - ) - - override fun runAndGetAssertions(): List { - val element = when (val buttonTitle = args[0]) { - Strings.Button.Title.login -> application.findView(Strings.Button.Tag.login) - Strings.Button.Title.logout -> application.findView(Strings.Button.Tag.logout) - else -> throw UIElementException.Button.NotFound(buttonTitle) - } - return listOf( - element.waitExists(TimeoutDuration.SHORT), - element.isVisible(), - element.isButton() + class ITypeTextIntoTextField( + application: ApplicationAdapter, + args: List? + ) : CrossPlatform(application, args) { + override val definition: Definition = CucumberDefinition.Step.When( + "I type $EXPECT_VALUE_STRING in the $EXPECT_VALUE_STRING textfield" ) - } - } - class ISeeScreen( - application: ApplicationAdapter, - args: List? - ) : SealedDefinitions(application, args) { - override val definition: Definition = CucumberDefinition.Step.Then("I see the $EXPECT_VALUE_STRING screen") - - override fun runAndGetAssertions(): List { - val element = when (val screenTitle = args[0]) { - Strings.Screen.Title.login -> application.findView(Strings.Screen.Tag.login) - Strings.Screen.Title.home -> application.findView(Strings.Screen.Tag.home) - else -> throw UIElementException.Screen.NotFound(screenTitle) + override fun runAndGetAssertions(): List { + val textInput = args[0] + val tag = args[1] + application + .findView(tag) + .typeText(textInput) + return emptyList() } - return listOf(element.exists()) } - } - class ISeeTextFieldWithText( - application: ApplicationAdapter, - args: List? - ) : SealedDefinitions(application, args) { - override val definition: Definition = CucumberDefinition.Step.Then("I see the $EXPECT_VALUE_STRING textfield with text $EXPECT_VALUE_STRING") - - override fun runAndGetAssertions(): List { - val textFieldTag = args[1] - val textFieldText = args[1] - val element = application.findView(textFieldTag) - return listOf( - element.waitExists(TimeoutDuration.SHORT), - element.isHintEqualTo(textFieldText, true) + class IPressTheButton( + application: ApplicationAdapter, + args: List? + ) : CrossPlatform(application, args) { + override val definition: Definition = CucumberDefinition.Step.When( + "I press the $EXPECT_VALUE_STRING button" ) - } - } - class ITypeTextIntoTextField( - application: ApplicationAdapter, - args: List? - ) : SealedDefinitions(application, args) { - override val definition: Definition = CucumberDefinition.Step.When( - "I type $EXPECT_VALUE_STRING in the $EXPECT_VALUE_STRING textfield" - ) - - override fun runAndGetAssertions(): List { - val textInput = args[0] - val tag = args[1] - application - .findView(tag) - .typeText(textInput) - return emptyList() + override fun runAndGetAssertions(): List { + val element = when(val buttonTag = args[0]) { + Strings.Button.Title.login -> application.findView(Strings.Button.Tag.login) + Strings.Button.Title.logout -> application.findView(Strings.Button.Tag.logout) + else -> throw UIElementException.Button.NotFound(buttonTag) + } + application.assert(element.waitExists(TimeoutDuration.SHORT)) + element.tap() + return emptyList() + } } - } - class IPressTheButton( - application: ApplicationAdapter, - args: List? - ) : SealedDefinitions(application, args) { - override val definition: Definition = CucumberDefinition.Step.When( - "I press the $EXPECT_VALUE_STRING button" - ) - - override fun runAndGetAssertions(): List { - val element = when(val buttonTag = args[0]) { - Strings.Button.Title.login -> application.findView(Strings.Button.Tag.login) - Strings.Button.Title.logout -> application.findView(Strings.Button.Tag.logout) - else -> throw UIElementException.Button.NotFound(buttonTag) + class SetLoggedInUserEmail( + application: ApplicationAdapter, + args: List? + ) : CrossPlatform(application, args) { + override val definition: Definition = CucumberDefinition.Step.Given( + "Email is $EXPECT_VALUE_STRING" + ) + + override fun runAndGetAssertions(): List { + val loggedInEmail = args[0] + application["testEmail"] = loggedInEmail + return emptyList() } - application.assert(element.waitExists(TimeoutDuration.SHORT)) - element.tap() - return emptyList() } } - class SetLoggedInUserEmail( - application: ApplicationAdapter, - args: List? - ) : SealedDefinitions(application, args) { - override val definition: Definition = CucumberDefinition.Step.Given( - "Email is $EXPECT_VALUE_STRING" - ) - - override fun runAndGetAssertions(): List { - val loggedInEmail = args[0] - application["testEmail"] = loggedInEmail - return emptyList() + sealed class Platform : TestCase.Platform, AppDefinitions() { + class ISeeValueInScrollView : Platform() { + override val definition: Definition = CucumberDefinition.Step.Then( + "I see $EXPECT_VALUE_STRING in the scrollview" + ) } } } From 0f686521d47d67df2fc17bd04250bc381ec63589 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 27 Sep 2023 09:21:19 +0200 Subject: [PATCH 20/32] Update view model and views created unit test --- .../android/home/HomeLayout.kt | 21 +++++++ ios/ios/Screens/Home/HomeView.swift | 6 ++ .../viewModels/home/HomeViewModel.kt | 5 +- .../cucumber/viewModels/HomeViewModelTest.kt | 57 +++++++++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/viewModels/HomeViewModelTest.kt diff --git a/android/src/main/java/com/corrado4eyes/cucumberplayground/android/home/HomeLayout.kt b/android/src/main/java/com/corrado4eyes/cucumberplayground/android/home/HomeLayout.kt index 3d1c390..50949c3 100644 --- a/android/src/main/java/com/corrado4eyes/cucumberplayground/android/home/HomeLayout.kt +++ b/android/src/main/java/com/corrado4eyes/cucumberplayground/android/home/HomeLayout.kt @@ -2,12 +2,18 @@ package com.corrado4eyes.cucumberplayground.android.home import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.corrado4eyes.cucumberplayground.models.Strings import com.corrado4eyes.cucumberplayground.viewModels.home.HomeViewModel import com.splendo.kaluga.architecture.compose.viewModel.ViewModelComposable @@ -32,6 +38,21 @@ fun HomeLayout() { ) { Text(this@ViewModelComposable.buttonTitle) } + + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .testTag(Strings.ScrollView.Tag.homeScrollView) + ) { + items( + viewModel.scrollableItems, + key = { it } + ) { + Text(it.toString()) + } + } } } } diff --git a/ios/ios/Screens/Home/HomeView.swift b/ios/ios/Screens/Home/HomeView.swift index 6f49ec9..56ea156 100644 --- a/ios/ios/Screens/Home/HomeView.swift +++ b/ios/ios/Screens/Home/HomeView.swift @@ -26,6 +26,12 @@ struct HomeView: SwiftUI.View { Text(viewModel.buttonTitle) } .accessibilityLabel(Strings.ButtonTag.shared.logout) + ScrollView(.vertical) { + ForEach(viewModel.scrollableItems, id: \.intValue) { index in + Text("\(index)") + } + }.frame(height: 200) + .accessibilityLabel(Strings.ScrollViewTag.shared.homeScrollView) }.toolbar { ToolbarItem(placement: .principal) { Text(viewModel.screenTitle) 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 5c95491..f022aec 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 @@ -10,12 +10,13 @@ import org.koin.core.component.inject class HomeViewModel : BaseLifecycleViewModel(), KoinComponent { private val authService: AuthService by inject() - val screenTitle = Strings.Screen.Title.home - val buttonTitle = Strings.Button.Title.logout + val buttonTitle = Strings.Button.Title.logout fun getCurrentUser() = authService.getCurrentUserIfAny()!! + val user = authService.getCurrentUserIfAny()!! + val scrollableItems: List = (1..20).toList() fun logout() { coroutineScope.launch { diff --git a/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/viewModels/HomeViewModelTest.kt b/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/viewModels/HomeViewModelTest.kt new file mode 100644 index 0000000..8bd2344 --- /dev/null +++ b/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/viewModels/HomeViewModelTest.kt @@ -0,0 +1,57 @@ +package com.corrado4eyes.cucumberplayground.cucumber.viewModels + +import com.corrado4eyes.cucumberplayground.cucumber.login.AuthServiceMock +import com.corrado4eyes.cucumberplayground.cucumber.login.DummyUsers.defaultTestUser +import com.corrado4eyes.cucumberplayground.models.Strings +import com.corrado4eyes.cucumberplayground.services.AuthService +import com.corrado4eyes.cucumberplayground.viewModels.home.HomeViewModel +import com.splendo.kaluga.test.base.yieldMultiple +import com.splendo.kaluga.test.koin.KoinUIThreadViewModelTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlinx.coroutines.CoroutineScope +import org.koin.core.component.get +import org.koin.dsl.module + +class HomeViewModelTest : KoinUIThreadViewModelTest() { + class KoinContext : KoinViewModelTestContext( + module { + single { AuthServiceMock(getOrNull()) } + single { defaultTestUser } + } + ) { + val authService = get() as AuthServiceMock + override val viewModel: HomeViewModel = HomeViewModel() + } + + @Test + fun test_title_matches_value() = testOnUIThread { + assertEquals(Strings.Screen.Title.home, viewModel.screenTitle) + } + + @Test + fun test_button_title_matches_value() = testOnUIThread { + assertEquals(Strings.Button.Title.logout, viewModel.buttonTitle) + } + + @Test + fun test_button_action_triggers_logout() = testOnUIThread { + assertEquals(0, authService.logoutCalledTimes) + assertEquals(defaultTestUser, authService.getCurrentUserIfAny()) + viewModel.logout() + yieldMultiple(3) + assertEquals(1, authService.logoutCalledTimes) + assertNull(authService.getCurrentUserIfAny()) + + } + + @Test + fun test_list_contains_20_items() = testOnUIThread { + assertEquals(20, viewModel.scrollableItems.size) + } + + override val createTestContext: suspend (scope: CoroutineScope) -> KoinContext + get() = { KoinContext() } + +} \ No newline at end of file From 1a5149839eef6bc0505a4def12ad583405dca56d Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 27 Sep 2023 09:22:08 +0200 Subject: [PATCH 21/32] Update feature files --- android/src/androidTest/assets/features/Home.feature | 3 ++- ios/CucumberTests/Features/Home.feature | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/android/src/androidTest/assets/features/Home.feature b/android/src/androidTest/assets/features/Home.feature index e2e6bb0..b6f0e26 100644 --- a/android/src/androidTest/assets/features/Home.feature +++ b/android/src/androidTest/assets/features/Home.feature @@ -4,5 +4,6 @@ Feature: Home And I am in the "Home" screen Then I see "test@test.com" text And I see the "Logout" button + And I see "15" in the scrollview When I press the "Logout" button - Then I see the "Login" screen + Then I see the "Login" screen \ No newline at end of file diff --git a/ios/CucumberTests/Features/Home.feature b/ios/CucumberTests/Features/Home.feature index e2e6bb0..f551517 100644 --- a/ios/CucumberTests/Features/Home.feature +++ b/ios/CucumberTests/Features/Home.feature @@ -4,5 +4,6 @@ Feature: Home And I am in the "Home" screen Then I see "test@test.com" text And I see the "Logout" button + And I see "15" in the scrollview When I press the "Logout" button Then I see the "Login" screen From 32171a54fdde1849a1941f5e8f282ba60e0a907d Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 27 Sep 2023 09:22:40 +0200 Subject: [PATCH 22/32] Update ui test files --- .../test/StepDefinitions.kt | 31 +++++++++++++------ ios/CucumberTests/CucumberTests.swift | 26 +++++++++++----- .../cucumberplayground/models/Strings.kt | 6 ++++ 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt index 373a150..8631e5f 100644 --- a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt +++ b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt @@ -1,9 +1,14 @@ package com.corrado4eyes.cucumberplayground.test +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performScrollToIndex import androidx.test.core.app.ActivityScenario +import com.corrado4eyes.cucumberplayground.models.Strings +import com.corrado4eyes.cucumbershared.tests.AppDefinitions import com.corrado4eyes.cucumbershared.tests.Definitions -import com.corrado4eyes.cucumbershared.tests.SealedDefinitions import com.corrado4eyes.pistakio.DefaultApplicationAdapter import io.cucumber.java8.En import io.cucumber.junit.WithJunitRule @@ -24,36 +29,44 @@ class StepDefinitions : En { val definitionString = it.definition.definitionString when (it) { Definitions.I_AM_IN_THE_EXPECT_VALUE_STRING_SCREEN -> Given(definitionString) { screenName: String -> - val assertions = SealedDefinitions.IAmInScreen(application, listOf(screenName)).runAndGetAssertions() + val assertions = AppDefinitions.CrossPlatform.IAmInScreen(application, listOf(screenName)).runAndGetAssertions() application.assertAll(assertions) } Definitions.I_SEE_EXPECT_VALUE_STRING_TEXT -> Then(definitionString) { textViewTitle: String -> - val assertions = SealedDefinitions.ISeeText(application, listOf(textViewTitle)).runAndGetAssertions() + val assertions = AppDefinitions.CrossPlatform.ISeeText(application, listOf(textViewTitle)).runAndGetAssertions() application.assertAll(assertions) } Definitions.I_SEE_THE_EXPECT_VALUE_STRING_BUTTON -> Then(definitionString) { buttonTitle: String -> - val assertions = SealedDefinitions.ISeeButton(application, listOf(buttonTitle)).runAndGetAssertions() + val assertions = AppDefinitions.CrossPlatform.ISeeButton(application, listOf(buttonTitle)).runAndGetAssertions() application.assertAll(assertions) } Definitions.I_SEE_THE_EXPECT_VALUE_STRING_SCREEN -> Then(definitionString) { screenTitle: String -> - val assertions = SealedDefinitions.ISeeScreen(application, listOf(screenTitle)).runAndGetAssertions() + val assertions = AppDefinitions.CrossPlatform.ISeeScreen(application, listOf(screenTitle)).runAndGetAssertions() application.assertAll(assertions) } Definitions.I_SEE_THE_EXPECT_VALUE_STRING_TEXT_FIELD_WITH_TEXT_EXPECT_VALUE_STRING -> Then(definitionString) { textFieldTag: String, textFieldText: String -> - val assertions = SealedDefinitions.ISeeTextFieldWithText(application, listOf(textFieldTag, textFieldText)).runAndGetAssertions() + val assertions = AppDefinitions.CrossPlatform.ISeeTextFieldWithText(application, listOf(textFieldTag, textFieldText)).runAndGetAssertions() application.assertAll(assertions) } Definitions.I_TYPE_EXPECT_VALUE_STRING_IN_THE_EXPECT_VALUE_STRING_TEXT_FIELD -> When(definitionString) { textInput: String, tag: String, -> - val assertions = SealedDefinitions.ITypeTextIntoTextField(application, listOf(textInput, tag)).runAndGetAssertions() + val assertions = AppDefinitions.CrossPlatform.ITypeTextIntoTextField(application, listOf(textInput, tag)).runAndGetAssertions() application.assertAll(assertions) } Definitions.I_PRESS_THE_EXPECT_VALUE_STRING_BUTTON -> When(definitionString) { buttonTag: String -> - val assertions = SealedDefinitions.IPressTheButton(application, listOf(buttonTag)).runAndGetAssertions() + val assertions = AppDefinitions.CrossPlatform.IPressTheButton(application, listOf(buttonTag)).runAndGetAssertions() application.assertAll(assertions) } Definitions.EMAIL_IS_EXPECT_VALUE_STRING -> Given(definitionString) { loggedInEmail: String -> - application.assertAll(SealedDefinitions.SetLoggedInUserEmail(application, listOf(loggedInEmail)).runAndGetAssertions()) + application.assertAll(AppDefinitions.CrossPlatform.SetLoggedInUserEmail(application, listOf(loggedInEmail)).runAndGetAssertions()) + } + + Definitions.I_SEE_EXPECT_VALUE_STRING_IN_THE_SCROLLVIEW -> When(definitionString) { index: String -> + testRule + .onNodeWithTag(Strings.ScrollView.Tag.homeScrollView) + .performScrollToIndex(index.toInt()) + + testRule.onNodeWithText(index).assertIsDisplayed() } } } diff --git a/ios/CucumberTests/CucumberTests.swift b/ios/CucumberTests/CucumberTests.swift index 540c20e..fe96f11 100644 --- a/ios/CucumberTests/CucumberTests.swift +++ b/ios/CucumberTests/CucumberTests.swift @@ -36,7 +36,7 @@ import Cucumberish let definitionString = test.definition.definitionString switch test { case .iAmInTheExpectValueStringScreen: Given(definitionString) { args, userInfo in - let assertions = SealedDefinitions.IAmInScreen( + let assertions = AppDefinitions.CrossPlatformIAmInScreen( application: applicationAdapter, args: args ).runAndGetAssertions() @@ -44,7 +44,7 @@ import Cucumberish assertAll(assertions: assertions) } case .iSeeExpectValueStringText: Then(definitionString) { args, userInfo in - let assertions = SealedDefinitions.ISeeText( + let assertions = AppDefinitions.CrossPlatformISeeText( application: applicationAdapter, args: args ).runAndGetAssertions() @@ -52,7 +52,7 @@ import Cucumberish assertAll(assertions: assertions) } case .iSeeTheExpectValueStringButton: Then(definitionString) { args, userInfo in - let assertions = SealedDefinitions.ISeeButton( + let assertions = AppDefinitions.CrossPlatformISeeButton( application: applicationAdapter, args: args ).runAndGetAssertions() @@ -60,7 +60,7 @@ import Cucumberish assertAll(assertions: assertions) } case .iSeeTheExpectValueStringScreen: Then(definitionString) { args, userInfo in - let assertions = SealedDefinitions.ISeeScreen( + let assertions = AppDefinitions.CrossPlatformISeeScreen( application: applicationAdapter, args: args ).runAndGetAssertions() @@ -68,7 +68,7 @@ import Cucumberish assertAll(assertions: assertions) } case .iSeeTheExpectValueStringTextFieldWithTextExpectValueString: Then(definitionString) { args, userInfo in - let assertions = SealedDefinitions.ISeeTextFieldWithText( + let assertions = AppDefinitions.CrossPlatformISeeTextFieldWithText( application: applicationAdapter, args: args ).runAndGetAssertions() @@ -76,18 +76,28 @@ import Cucumberish assertAll(assertions: assertions) } case .iTypeExpectValueStringInTheExpectValueStringTextField: When(definitionString) { args, userInfo in - let assertions = SealedDefinitions.ITypeTextIntoTextField(application: applicationAdapter, args: args).runAndGetAssertions() + let assertions = AppDefinitions.CrossPlatformITypeTextIntoTextField(application: applicationAdapter, args: args).runAndGetAssertions() assertAll(assertions: assertions) } case .iPressTheExpectValueStringButton: When(definitionString) { args, userInfo in - let assertions = SealedDefinitions.IPressTheButton(application: applicationAdapter, args: args).runAndGetAssertions() + let assertions = AppDefinitions.CrossPlatformIPressTheButton(application: applicationAdapter, args: args).runAndGetAssertions() assertAll(assertions: assertions) } case .emailIsExpectValueString: Given(definitionString) { args, userInfo in - let assertions = SealedDefinitions.SetLoggedInUserEmail(application: applicationAdapter, args: args).runAndGetAssertions() + let assertions = AppDefinitions.CrossPlatformSetLoggedInUserEmail(application: applicationAdapter, args: args).runAndGetAssertions() assertAll(assertions: assertions) } + case .iSeeExpectValueStringInTheScrollview: Then(definitionString) { args, userInfo in + guard let scrollViewItemIndex = args?[0] as? String else { return } + app.descendants(matching: .any) + .matching(identifier: Strings.ScrollViewTag.shared.homeScrollView) + .element.swipeUp() + let scrollItem = app.staticTexts[scrollViewItemIndex] + XCTAssert(scrollItem.waitForExistence(timeout: 0.1)) + } + + default: XCTFail("unrecognised test case.") } diff --git a/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/models/Strings.kt b/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/models/Strings.kt index 63307c4..f68a9ea 100644 --- a/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/models/Strings.kt +++ b/shared/src/commonMain/kotlin/com/corrado4eyes/cucumberplayground/models/Strings.kt @@ -12,6 +12,12 @@ object Strings { } } + object ScrollView { + object Tag { + val homeScrollView = "Home ScrollView" + } + } + object TextField { object Placeholder { val email = "Email" From 910952911c9a2445fe907dfd393e41abbd8c7fef Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Wed, 27 Sep 2023 09:23:11 +0200 Subject: [PATCH 23/32] Update mock auth service and LoginVMTest --- .../cucumber/login/AuthServiceMock.kt | 15 +++++++++++---- .../cucumber/viewModels/LoginViewModelTest.kt | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/login/AuthServiceMock.kt b/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/login/AuthServiceMock.kt index c3936c2..3d16da5 100644 --- a/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/login/AuthServiceMock.kt +++ b/shared/src/commonTest/kotlin/com/corrado4eyes/cucumberplayground/cucumber/login/AuthServiceMock.kt @@ -1,5 +1,6 @@ package com.corrado4eyes.cucumberplayground.cucumber.login +import com.corrado4eyes.cucumberplayground.cucumber.login.DummyUsers.defaultTestUser import com.corrado4eyes.cucumberplayground.login.model.AuthResponse import com.corrado4eyes.cucumberplayground.models.User import com.corrado4eyes.cucumberplayground.services.AuthService @@ -11,13 +12,14 @@ import kotlinx.coroutines.flow.asStateFlow /** * Mock is the same as impl but here for the sake of proper interfacing */ -class AuthServiceMock : AuthService { +class AuthServiceMock(loggedInUser: User?) : AuthService { - private var currentUser: MutableStateFlow = MutableStateFlow(null) + private var currentUser: MutableStateFlow = MutableStateFlow(loggedInUser) private val users = mutableListOf( User("alex@alex.com", "1234"), - User("corrado@corrado.com", "1234") + User("corrado@corrado.com", "1234"), + defaultTestUser ) override suspend fun login(email: String, pass: String): AuthResponse { @@ -31,8 +33,9 @@ class AuthServiceMock : AuthService { } ?: AuthResponse.Error("Invalid credentials") } + var logoutCalledTimes = 0 override suspend fun logout() { - delay(1000) + logoutCalledTimes++ currentUser.value = null } @@ -56,3 +59,7 @@ class AuthServiceMock : AuthService { return currentUser.asStateFlow().value } } + +object DummyUsers { + val defaultTestUser = User("test@test.com", "1234") +} 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 3c73ba3..3ce708c 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 @@ -15,7 +15,7 @@ import org.koin.dsl.module class LoginViewModelTest : KoinUIThreadViewModelTest() { class KoinContext : KoinViewModelTestContext( module { - single { AuthServiceMock() } + single { AuthServiceMock(getOrNull()) } } ) { val authService = get() as AuthServiceMock From 223e2896e220d951714ae1933cc1083ab53eb5a5 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Fri, 29 Sep 2023 08:11:51 +0200 Subject: [PATCH 24/32] Add `launchScreen` paramenter. Put arguments in different lines --- .../corrado4eyes/cucumberplayground/test/StepDefinitions.kt | 6 +++++- .../com/corrado4eyes/cucumbershared/tests/TestCase.kt | 3 ++- ios/CucumberTests/CucumberTests.swift | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt index 8631e5f..8df4d34 100644 --- a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt +++ b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt @@ -29,7 +29,11 @@ class StepDefinitions : En { val definitionString = it.definition.definitionString when (it) { Definitions.I_AM_IN_THE_EXPECT_VALUE_STRING_SCREEN -> Given(definitionString) { screenName: String -> - val assertions = AppDefinitions.CrossPlatform.IAmInScreen(application, listOf(screenName)).runAndGetAssertions() + val assertions = AppDefinitions.CrossPlatform.IAmInScreen( + "MainActivity", + application, + listOf(screenName) + ).runAndGetAssertions() application.assertAll(assertions) } Definitions.I_SEE_EXPECT_VALUE_STRING_TEXT -> Then(definitionString) { textViewTitle: String -> 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 5fa37ab..e9a03a1 100644 --- a/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt +++ b/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt @@ -56,6 +56,7 @@ sealed class AppDefinitions : TestCase { protected val args = args ?: emptyList() class IAmInScreen( + private val launchScreenName: String? = null, application: ApplicationAdapter, args: List? ) : CrossPlatform(application, args) { @@ -76,7 +77,7 @@ sealed class AppDefinitions : TestCase { } else -> throw UIElementException.Screen.NotFound(screenName) } - application.launch("MainActivity", application.applicationArguments) + application.launch(launchScreenName, application.applicationArguments) val element = application.findView(screenTitleTag) return listOf( element.waitExists(TimeoutDuration.SHORT) diff --git a/ios/CucumberTests/CucumberTests.swift b/ios/CucumberTests/CucumberTests.swift index fe96f11..9da71cc 100644 --- a/ios/CucumberTests/CucumberTests.swift +++ b/ios/CucumberTests/CucumberTests.swift @@ -37,6 +37,7 @@ import Cucumberish switch test { case .iAmInTheExpectValueStringScreen: Given(definitionString) { args, userInfo in let assertions = AppDefinitions.CrossPlatformIAmInScreen( + launchScreenName: nil, application: applicationAdapter, args: args ).runAndGetAssertions() From 1d0af2c153a89c2325d0b683520c66f4de68e809 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Fri, 29 Sep 2023 08:14:27 +0200 Subject: [PATCH 25/32] Add APIs to scroll scroll views --- .../kotlin/com/corrado4eyes/pistakio/Node.kt | 50 +++++++++++++ .../pistakio/ApplicationAdapter.kt | 8 ++ .../kotlin/com/corrado4eyes/pistakio/Node.kt | 14 +++- .../corrado4eyes/pistakio/mocks/StubNode.kt | 33 +++++++++ .../pistakio/ApplicationAdapter.kt | 2 +- .../kotlin/com/corrado4eyes/pistakio/Node.kt | 74 ++++++++++++++++++- 6 files changed, 178 insertions(+), 3 deletions(-) diff --git a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/Node.kt b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/Node.kt index 54d93ba..91872b8 100644 --- a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/Node.kt +++ b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/Node.kt @@ -5,7 +5,13 @@ import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToIndex +import androidx.compose.ui.test.performScrollToKey import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown +import androidx.compose.ui.test.swipeUp +import kotlin.time.DurationUnit actual class DefaultNode(internal val node: SemanticsNodeInteraction) : Node { override fun exists(): AssertionResult = assertionResultFor(node::assertExists) @@ -30,6 +36,50 @@ actual class DefaultNode(internal val node: SemanticsNodeInteraction) : Node { override fun tap() { node.performClick() } + + override fun swipeUntilIndex(index: Int, velocity: Float?) { + node.performScrollToIndex(index) + } + + override fun swipeUntilKey(key: Any, velocity: Float?) { + node.performScrollToKey(key) + } + + override fun swipeUp() { + node.performTouchInput { + swipeUp() + } + } + + override fun swipeUp(swipeDuration: SwipeDuration) { + node.performTouchInput { + swipeUp(durationMillis = swipeDuration.duration.toLong(DurationUnit.MILLISECONDS)) + } + } + + override fun swipeUp(startY: Float, endY: Float) { + node.performTouchInput { + swipeUp(startY = startY, endY = endY) + } + } + + override fun swipeDown() { + node.performTouchInput { + swipeDown() + } + } + + override fun swipeDown(swipeDuration: SwipeDuration) { + node.performTouchInput { + swipeDown(durationMillis = swipeDuration.duration.toLong(DurationUnit.MILLISECONDS)) + } + } + + override fun swipeDown(startY: Float, endY: Float) { + node.performTouchInput { + swipeDown(startY = startY, endY = endY) + } + } } actual typealias AssertionBlockReturnType = SemanticsNodeInteraction diff --git a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt index e14b2f2..3bedc09 100644 --- a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt @@ -1,6 +1,7 @@ package com.corrado4eyes.pistakio import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds class AppLaunchedAlreadyException : Throwable() { @@ -17,6 +18,13 @@ enum class TimeoutDuration(val duration: Duration) { LONG(60.seconds) } +sealed class SwipeDuration(val duration: Duration) { + object Short : SwipeDuration(50.milliseconds) + object Medium : SwipeDuration(100.milliseconds) + object Long : SwipeDuration(200.milliseconds) + class Custom(duration: Duration) : SwipeDuration(duration) +} + typealias ApplicationArguments = Map interface ApplicationAdapter { diff --git a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt index f0b0cf8..4d4762c 100644 --- a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt @@ -5,7 +5,6 @@ sealed class AssertionResult { class Failure(val exception: AssertionError) : AssertionResult() } - interface Node { fun typeText(text: String) fun tap() @@ -51,6 +50,19 @@ interface Node { */ fun isHintEqualTo(value: String, contains: Boolean): AssertionResult + fun swipeUp() + fun swipeUp(swipeDuration: SwipeDuration) + fun swipeUp(startY: Float, endY: Float) + + fun swipeDown() + fun swipeDown(swipeDuration: SwipeDuration) + fun swipeDown(startY: Float, endY: Float) + + + fun swipeUntilIndex(index: Int, velocity: Float? = null) + + fun swipeUntilKey(key: Any, velocity: Float? = null) + // fun doubleTap() // fun press() } diff --git a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubNode.kt b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubNode.kt index 980653e..bbdc4c0 100644 --- a/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubNode.kt +++ b/pistakio/src/commonTest/kotlin/com/corrado4eyes/pistakio/mocks/StubNode.kt @@ -2,6 +2,7 @@ package com.corrado4eyes.pistakio.mocks import com.corrado4eyes.pistakio.AssertionResult import com.corrado4eyes.pistakio.Node +import com.corrado4eyes.pistakio.SwipeDuration import com.corrado4eyes.pistakio.TimeoutDuration class StubNode : Node { @@ -36,4 +37,36 @@ class StubNode : Node { override fun isHintEqualTo(value: String, contains: Boolean): AssertionResult { TODO("Not yet implemented") } + + override fun swipeUp() { + TODO("Not yet implemented") + } + + override fun swipeUp(swipeDuration: SwipeDuration) { + TODO("Not yet implemented") + } + + override fun swipeUp(startY: Float, endY: Float) { + TODO("Not yet implemented") + } + + override fun swipeDown() { + TODO("Not yet implemented") + } + + override fun swipeDown(swipeDuration: SwipeDuration) { + TODO("Not yet implemented") + } + + override fun swipeDown(startY: Float, endY: Float) { + TODO("Not yet implemented") + } + + override fun swipeUntilIndex(index: Int, velocity: Float?) { + TODO("Not yet implemented") + } + + override fun swipeUntilKey(key: Any, velocity: Float?) { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt index 9a86a3e..f77a6ee 100644 --- a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt +++ b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt @@ -19,7 +19,7 @@ actual class DefaultApplicationAdapter(app: XCUIApplication?) : BaseApplicationA .descendantsMatchingType(XCUIElementTypeAny) .matchingIdentifier(tag) .element - return DefaultNode(element) + return DefaultNode(app, element) } /** diff --git a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/Node.kt b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/Node.kt index 4fccc49..4efbe0a 100644 --- a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/Node.kt +++ b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/Node.kt @@ -5,12 +5,21 @@ import platform.Foundation.NSPredicate import platform.XCTest.XCTWaiter import platform.XCTest.XCTWaiterResultCompleted import platform.XCTest.XCTestCase +import platform.XCTest.XCUIApplication import platform.XCTest.XCUIElement +import platform.XCTest.XCUIElementTypeAny import platform.XCTest.expectationForPredicate +import platform.XCTest.swipeDown +import platform.XCTest.swipeDownWithVelocity +import platform.XCTest.swipeUp +import platform.XCTest.swipeUpWithVelocity import platform.XCTest.tap import platform.XCTest.typeText -actual class DefaultNode(private val element: XCUIElement) : Node { +actual class DefaultNode( + private val app: XCUIApplication, + private val element: XCUIElement +) : Node { override fun exists(): AssertionResult { val predicate = NSPredicate.predicateWithFormat("exists == true") @@ -44,6 +53,61 @@ actual class DefaultNode(private val element: XCUIElement) : Node { override fun isHintEqualTo(value: String, contains: Boolean): AssertionResult = assertionResultFor { isTextEqualOrContains(value, contains) } + override fun swipeUntilIndex(index: Int, velocity: Float?) { + val listElement = listItemAt(index) + while (!listElement.isHittable()) { + element.swipeUpWithVelocity( + velocity = velocity?.toDouble() ?: 20.0 + ) + } + } + + override fun swipeUntilKey(key: Any, velocity: Float?) { + val listElement = listItemWithTag(key.toString()).element + while (!listElement.isHittable()) { + element.swipeUpWithVelocity( + velocity = velocity?.toDouble() ?: 20.0 + ) + } + } + + override fun swipeUp() { + element.swipeUp() + } + + override fun swipeUp(swipeDuration: SwipeDuration) { + element.swipeUpWithVelocity(swipeDuration.duration.toDouble(DurationUnit.MILLISECONDS)) + } + + override fun swipeUp(startY: Float, endY: Float) { + TODO("Unstable API, needs more investigation") +// val scrollViewCoordinate = element.coordinateWithNormalizedOffset( +// CGVectorMake(0.0, startY.toDouble()) +// ) +// +// scrollViewCoordinate.pressForDuration( +// 0.1, +// thenDragToCoordinate = app.coordinateWithNormalizedOffset( +// CGVectorMake(0.0, endY.toDouble().unaryMinus()) +// ), +// withVelocity = 200.0, +// thenHoldForDuration = 0.1 +// ) + } + + + override fun swipeDown() { + element.swipeDown() + } + + override fun swipeDown(swipeDuration: SwipeDuration) { + element.swipeDownWithVelocity(swipeDuration.duration.toDouble(DurationUnit.MILLISECONDS)) + } + + override fun swipeDown(startY: Float, endY: Float) { + TODO("Unstable API, needs more investigation") + } + override fun typeText(text: String) { element.tap() element.typeText(text) @@ -58,6 +122,14 @@ actual class DefaultNode(private val element: XCUIElement) : Node { } else { element.value == value } + + private fun listItemAt(index: Int) = element + .descendantsMatchingType(XCUIElementTypeAny) + .elementAtIndex(index.toULong()) + + private fun listItemWithTag(tag: String) = element + .descendantsMatchingType(XCUIElementTypeAny) + .matchingIdentifier(tag) } actual typealias AssertionBlockReturnType = Boolean From 521bf85ed37a91b8a557d03d0d927cb3449723dc Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Mon, 2 Oct 2023 09:18:41 +0200 Subject: [PATCH 26/32] Add SKIE to convert kotlin sealed classes into exhaustive enums in swift. Remove enum on kotlin side and update test runners --- .../test/StepDefinitions.kt | 23 ++- build.gradle.kts | 1 + cucumberShared/build.gradle.kts | 13 ++ .../cucumbershared/tests/TestCase.kt | 91 ++++++---- ios/CucumberTests/CucumberTests.swift | 157 ++++++++++-------- settings.gradle.kts | 2 + 6 files changed, 168 insertions(+), 119 deletions(-) diff --git a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt index 8df4d34..b96cf95 100644 --- a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt +++ b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.test.performScrollToIndex import androidx.test.core.app.ActivityScenario import com.corrado4eyes.cucumberplayground.models.Strings import com.corrado4eyes.cucumbershared.tests.AppDefinitions -import com.corrado4eyes.cucumbershared.tests.Definitions import com.corrado4eyes.pistakio.DefaultApplicationAdapter import io.cucumber.java8.En import io.cucumber.junit.WithJunitRule @@ -22,13 +21,13 @@ class StepDefinitions : En { @get:Rule(order = 0) val testRule = createComposeRule() - val application = DefaultApplicationAdapter(testRule) + private val application = DefaultApplicationAdapter(testRule) init { - Definitions.values().forEach { + AppDefinitions.allCases.forEach { val definitionString = it.definition.definitionString when (it) { - Definitions.I_AM_IN_THE_EXPECT_VALUE_STRING_SCREEN -> Given(definitionString) { screenName: String -> + is AppDefinitions.CrossPlatform.IAmInScreen -> Given(definitionString) { screenName: String -> val assertions = AppDefinitions.CrossPlatform.IAmInScreen( "MainActivity", application, @@ -36,36 +35,36 @@ class StepDefinitions : En { ).runAndGetAssertions() application.assertAll(assertions) } - Definitions.I_SEE_EXPECT_VALUE_STRING_TEXT -> Then(definitionString) { textViewTitle: String -> + is AppDefinitions.CrossPlatform.ISeeText -> Then(definitionString) { textViewTitle: String -> val assertions = AppDefinitions.CrossPlatform.ISeeText(application, listOf(textViewTitle)).runAndGetAssertions() application.assertAll(assertions) } - Definitions.I_SEE_THE_EXPECT_VALUE_STRING_BUTTON -> Then(definitionString) { buttonTitle: String -> + is AppDefinitions.CrossPlatform.ISeeButton -> Then(definitionString) { buttonTitle: String -> val assertions = AppDefinitions.CrossPlatform.ISeeButton(application, listOf(buttonTitle)).runAndGetAssertions() application.assertAll(assertions) } - Definitions.I_SEE_THE_EXPECT_VALUE_STRING_SCREEN -> Then(definitionString) { screenTitle: String -> + is AppDefinitions.CrossPlatform.ISeeScreen -> Then(definitionString) { screenTitle: String -> val assertions = AppDefinitions.CrossPlatform.ISeeScreen(application, listOf(screenTitle)).runAndGetAssertions() application.assertAll(assertions) } - Definitions.I_SEE_THE_EXPECT_VALUE_STRING_TEXT_FIELD_WITH_TEXT_EXPECT_VALUE_STRING -> Then(definitionString) { textFieldTag: String, textFieldText: String -> + is AppDefinitions.CrossPlatform.ISeeTextFieldWithText -> Then(definitionString) { textFieldTag: String, textFieldText: String -> val assertions = AppDefinitions.CrossPlatform.ISeeTextFieldWithText(application, listOf(textFieldTag, textFieldText)).runAndGetAssertions() application.assertAll(assertions) } - Definitions.I_TYPE_EXPECT_VALUE_STRING_IN_THE_EXPECT_VALUE_STRING_TEXT_FIELD -> When(definitionString) { textInput: String, tag: String, -> + is AppDefinitions.CrossPlatform.ITypeTextIntoTextField -> When(definitionString) { textInput: String, tag: String, -> val assertions = AppDefinitions.CrossPlatform.ITypeTextIntoTextField(application, listOf(textInput, tag)).runAndGetAssertions() application.assertAll(assertions) } - Definitions.I_PRESS_THE_EXPECT_VALUE_STRING_BUTTON -> When(definitionString) { buttonTag: String -> + is AppDefinitions.CrossPlatform.IPressTheButton -> When(definitionString) { buttonTag: String -> val assertions = AppDefinitions.CrossPlatform.IPressTheButton(application, listOf(buttonTag)).runAndGetAssertions() application.assertAll(assertions) } - Definitions.EMAIL_IS_EXPECT_VALUE_STRING -> Given(definitionString) { loggedInEmail: String -> + is AppDefinitions.CrossPlatform.SetLoggedInUserEmail -> Given(definitionString) { loggedInEmail: String -> application.assertAll(AppDefinitions.CrossPlatform.SetLoggedInUserEmail(application, listOf(loggedInEmail)).runAndGetAssertions()) } - Definitions.I_SEE_EXPECT_VALUE_STRING_IN_THE_SCROLLVIEW -> When(definitionString) { index: String -> + is AppDefinitions.Platform.ISeeValueInScrollView -> When(definitionString) { index: String -> testRule .onNodeWithTag(Strings.ScrollView.Tag.homeScrollView) .performScrollToIndex(index.toInt()) diff --git a/build.gradle.kts b/build.gradle.kts index deac9e0..79637a2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,7 @@ plugins { buildscript { repositories { + mavenLocal() mavenCentral() } diff --git a/cucumberShared/build.gradle.kts b/cucumberShared/build.gradle.kts index 1c3c7c1..45453b2 100644 --- a/cucumberShared/build.gradle.kts +++ b/cucumberShared/build.gradle.kts @@ -1,3 +1,4 @@ +import co.touchlab.skie.configuration.SealedInterop import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.konan.file.File import org.jetbrains.kotlin.konan.properties.Properties @@ -7,6 +8,7 @@ import org.jetbrains.kotlin.konan.properties.loadProperties plugins { kotlin("multiplatform") id("com.android.library") + id("co.touchlab.skie") version "0.5.0" } kotlin { @@ -170,4 +172,15 @@ fun org.jetbrains.kotlin.gradle.plugin.mpp.NativeBinary.linkFrameworkSearchPaths linkerOpts("-rpath", it) } } +} + +skie { + analytics { + disableUpload.set(true) + } + features { + group { + SealedInterop.Enabled(true) + } + } } \ No newline at end of file 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 e9a03a1..4bccc4e 100644 --- a/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt +++ b/cucumberShared/src/commonMain/kotlin/com/corrado4eyes/cucumbershared/tests/TestCase.kt @@ -3,50 +3,71 @@ 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.GherkinTestCase import com.corrado4eyes.cucumberplayground.models.Strings import com.corrado4eyes.pistakio.ApplicationAdapter +import com.corrado4eyes.pistakio.ApplicationArguments import com.corrado4eyes.pistakio.AssertionResult +import com.corrado4eyes.pistakio.Node import com.corrado4eyes.pistakio.TestCase import com.corrado4eyes.pistakio.TimeoutDuration import com.corrado4eyes.pistakio.errors.UIElementException -enum class Definitions(override val definition: Definition): GherkinTestCase { - I_AM_IN_THE_EXPECT_VALUE_STRING_SCREEN( - CucumberDefinition.Step.Given("I am in the $EXPECT_VALUE_STRING screen") - ), - I_SEE_EXPECT_VALUE_STRING_TEXT( - CucumberDefinition.Step.Then("I see $EXPECT_VALUE_STRING text") - ), - I_SEE_THE_EXPECT_VALUE_STRING_BUTTON( - CucumberDefinition.Step.Then("I see the $EXPECT_VALUE_STRING button") - ), - I_SEE_THE_EXPECT_VALUE_STRING_SCREEN( - CucumberDefinition.Step.Then("I see the $EXPECT_VALUE_STRING screen") - ), - I_SEE_THE_EXPECT_VALUE_STRING_TEXT_FIELD_WITH_TEXT_EXPECT_VALUE_STRING( - CucumberDefinition.Step.Then("I see the $EXPECT_VALUE_STRING textfield with text $EXPECT_VALUE_STRING") - ), - I_TYPE_EXPECT_VALUE_STRING_IN_THE_EXPECT_VALUE_STRING_TEXT_FIELD( - CucumberDefinition.Step.When("I type $EXPECT_VALUE_STRING in the $EXPECT_VALUE_STRING textfield") - ), - I_PRESS_THE_EXPECT_VALUE_STRING_BUTTON( - CucumberDefinition.Step.When("I press the $EXPECT_VALUE_STRING button") - ), - EMAIL_IS_EXPECT_VALUE_STRING( - CucumberDefinition.Step.Given("Email is $EXPECT_VALUE_STRING") - ), - I_SEE_EXPECT_VALUE_STRING_IN_THE_SCROLLVIEW( - CucumberDefinition.Step.Then("I see $EXPECT_VALUE_STRING in the scrollview") - ); +sealed class AppDefinitions : TestCase { + abstract val definition: Definition companion object { - val allCases: List = Definitions.values().toList() - } -} + val defaultApplicationAdapter = object : ApplicationAdapter { + override val applicationArguments: ApplicationArguments + get() = TODO("Not yet implemented") -sealed class AppDefinitions : TestCase { - abstract val definition: Definition + override fun launch(identifier: String?, arguments: Map) { + TODO("Not yet implemented") + } + + override fun findView(tag: String): Node { + TODO("Not yet implemented") + } + + override fun tearDown() { + TODO("Not yet implemented") + } + + override fun assert(assertionResult: AssertionResult) { + TODO("Not yet implemented") + } + + override fun assertUntil( + timeout: TimeoutDuration, + blockAssertionResult: () -> AssertionResult + ) { + TODO("Not yet implemented") + } + + override fun assertAll(assertions: List) { + TODO("Not yet implemented") + } + + override fun get(key: String): String? { + TODO("Not yet implemented") + } + + override fun set(key: String, value: String) { + TODO("Not yet implemented") + } + } + + val allCases = listOf( + CrossPlatform.IAmInScreen("", defaultApplicationAdapter, listOf()), + CrossPlatform.IPressTheButton(defaultApplicationAdapter, listOf()), + CrossPlatform.ISeeButton(defaultApplicationAdapter, listOf()), + CrossPlatform.ISeeScreen(defaultApplicationAdapter, listOf()), + CrossPlatform.ISeeText(defaultApplicationAdapter, listOf()), + CrossPlatform.ISeeTextFieldWithText(defaultApplicationAdapter, listOf()), + CrossPlatform.ITypeTextIntoTextField(defaultApplicationAdapter, listOf()), + CrossPlatform.SetLoggedInUserEmail(defaultApplicationAdapter, listOf()), + Platform.ISeeValueInScrollView + ) + } sealed class CrossPlatform( protected val application: ApplicationAdapter, @@ -210,7 +231,7 @@ sealed class AppDefinitions : TestCase { } sealed class Platform : TestCase.Platform, AppDefinitions() { - class ISeeValueInScrollView : Platform() { + object ISeeValueInScrollView : Platform() { override val definition: Definition = CucumberDefinition.Step.Then( "I see $EXPECT_VALUE_STRING in the scrollview" ) diff --git a/ios/CucumberTests/CucumberTests.swift b/ios/CucumberTests/CucumberTests.swift index 9da71cc..7519909 100644 --- a/ios/CucumberTests/CucumberTests.swift +++ b/ios/CucumberTests/CucumberTests.swift @@ -32,79 +32,92 @@ import Cucumberish applicationAdapter.tearDown() } - for test in Definitions.companion.allCases { - let definitionString = test.definition.definitionString - switch test { - case .iAmInTheExpectValueStringScreen: Given(definitionString) { args, userInfo in - let assertions = AppDefinitions.CrossPlatformIAmInScreen( - launchScreenName: nil, - application: applicationAdapter, - args: args - ).runAndGetAssertions() - - assertAll(assertions: assertions) - } - case .iSeeExpectValueStringText: Then(definitionString) { args, userInfo in - let assertions = AppDefinitions.CrossPlatformISeeText( - application: applicationAdapter, - args: args - ).runAndGetAssertions() - - assertAll(assertions: assertions) - } - case .iSeeTheExpectValueStringButton: Then(definitionString) { args, userInfo in - let assertions = AppDefinitions.CrossPlatformISeeButton( - application: applicationAdapter, - args: args - ).runAndGetAssertions() - - assertAll(assertions: assertions) - } - case .iSeeTheExpectValueStringScreen: Then(definitionString) { args, userInfo in - let assertions = AppDefinitions.CrossPlatformISeeScreen( - application: applicationAdapter, - args: args - ).runAndGetAssertions() - - assertAll(assertions: assertions) - } - case .iSeeTheExpectValueStringTextFieldWithTextExpectValueString: Then(definitionString) { args, userInfo in - let assertions = AppDefinitions.CrossPlatformISeeTextFieldWithText( - application: applicationAdapter, - args: args - ).runAndGetAssertions() - - assertAll(assertions: assertions) - } - case .iTypeExpectValueStringInTheExpectValueStringTextField: When(definitionString) { args, userInfo in - let assertions = AppDefinitions.CrossPlatformITypeTextIntoTextField(application: applicationAdapter, args: args).runAndGetAssertions() - assertAll(assertions: assertions) - } - - case .iPressTheExpectValueStringButton: When(definitionString) { args, userInfo in - let assertions = AppDefinitions.CrossPlatformIPressTheButton(application: applicationAdapter, args: args).runAndGetAssertions() - assertAll(assertions: assertions) - } - case .emailIsExpectValueString: Given(definitionString) { args, userInfo in - let assertions = AppDefinitions.CrossPlatformSetLoggedInUserEmail(application: applicationAdapter, args: args).runAndGetAssertions() - assertAll(assertions: assertions) - } - case .iSeeExpectValueStringInTheScrollview: Then(definitionString) { args, userInfo in - guard let scrollViewItemIndex = args?[0] as? String else { return } - app.descendants(matching: .any) - .matching(identifier: Strings.ScrollViewTag.shared.homeScrollView) - .element.swipeUp() - let scrollItem = app.staticTexts[scrollViewItemIndex] - XCTAssert(scrollItem.waitForExistence(timeout: 0.1)) - } - - - default: - XCTFail("unrecognised test case.") + + + for testCase in AppDefinitions.companion.allCases { + let definitionString = testCase.definition.definitionString + switch onEnum(of: testCase) { + + case .crossPlatform(let crossPlatformTestCase): + switch onEnum(of: crossPlatformTestCase) { + case .iAmInScreen(_): + Given(definitionString) { args, userInfo in + let assertions = AppDefinitions.CrossPlatformIAmInScreen( + launchScreenName: nil, + application: applicationAdapter, + args: args + ).runAndGetAssertions() + + assertAll(assertions: assertions) + } + case .iPressTheButton(_): + When(definitionString) { args, userInfo in + let assertions = AppDefinitions.CrossPlatformIPressTheButton(application: applicationAdapter, args: args).runAndGetAssertions() + assertAll(assertions: assertions) + } + case .iSeeButton(_): + Then(definitionString) { args, userInfo in + let assertions = AppDefinitions.CrossPlatformISeeButton( + application: applicationAdapter, + args: args + ).runAndGetAssertions() + + assertAll(assertions: assertions) + } + case .iSeeScreen(_): + Then(definitionString) { args, userInfo in + let assertions = AppDefinitions.CrossPlatformISeeScreen( + application: applicationAdapter, + args: args + ).runAndGetAssertions() + + assertAll(assertions: assertions) + } + case .iSeeText(_): + Then(definitionString) { args, userInfo in + let assertions = AppDefinitions.CrossPlatformISeeText( + application: applicationAdapter, + args: args + ).runAndGetAssertions() + + assertAll(assertions: assertions) + } + case .iSeeTextFieldWithText(_): + Then(definitionString) { args, userInfo in + let assertions = AppDefinitions.CrossPlatformISeeTextFieldWithText( + application: applicationAdapter, + args: args + ).runAndGetAssertions() + + assertAll(assertions: assertions) + } + case .iTypeTextIntoTextField(_): + When(definitionString) { args, userInfo in + let assertions = AppDefinitions.CrossPlatformITypeTextIntoTextField(application: applicationAdapter, args: args).runAndGetAssertions() + assertAll(assertions: assertions) + } + case .setLoggedInUserEmail(_): + Given(definitionString) { args, userInfo in + let assertions = AppDefinitions.CrossPlatformSetLoggedInUserEmail(application: applicationAdapter, args: args).runAndGetAssertions() + assertAll(assertions: assertions) + } + } + case .platform(let platformTestCase): + switch onEnum(of: platformTestCase) { + case .iSeeValueInScrollView(_): + Then(definitionString) { args, userInfo in + guard let scrollViewItemIndex = args?[0] as? String else { return } + app.descendants(matching: .any) + .matching(identifier: Strings.ScrollViewTag.shared.homeScrollView) + .element.swipeUp() + let scrollItem = app.staticTexts[scrollViewItemIndex] + XCTAssert(scrollItem.waitForExistence(timeout: 0.1)) + } + } } - - let bundle = Bundle(for: CucumberishInitializer.self) - Cucumberish.executeFeatures(inDirectory: "Features", from: bundle, includeTags: nil, excludeTags: ["ignore"]) } + + let bundle = Bundle(for: CucumberishInitializer.self) + Cucumberish.executeFeatures(inDirectory: "Features", from: bundle, includeTags: nil, excludeTags: ["ignore"]) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index d38d778..f2021a5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,7 @@ pluginManagement { repositories { google() gradlePluginPortal() + mavenLocal() mavenCentral() } } @@ -9,6 +10,7 @@ pluginManagement { dependencyResolutionManagement { repositories { google() + mavenLocal() mavenCentral() } } From dbfa64e6d55c7be2f188c86a6d78931b959c8246 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Mon, 2 Oct 2023 09:19:41 +0200 Subject: [PATCH 27/32] Remove empty lines --- ios/ios/Screens/Login/LoginView.swift | 1 - pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/ios/ios/Screens/Login/LoginView.swift b/ios/ios/Screens/Login/LoginView.swift index 7b0f6f4..b46ce58 100644 --- a/ios/ios/Screens/Login/LoginView.swift +++ b/ios/ios/Screens/Login/LoginView.swift @@ -49,7 +49,6 @@ struct LoginView: SwiftUI.View { .accessibilityLabel(Strings.TextFieldTag.shared.password) Text(passwordErrorText.value) .foregroundColor(Color.red) - Text(formFooterErrorText.value) .foregroundColor(Color.red) diff --git a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt index 4d4762c..4f5ffd8 100644 --- a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt @@ -60,7 +60,6 @@ interface Node { fun swipeUntilIndex(index: Int, velocity: Float? = null) - fun swipeUntilKey(key: Any, velocity: Float? = null) // fun doubleTap() From 9507f10498330517d95198ee66dc4e36bd9c3841 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Mon, 2 Oct 2023 09:46:26 +0200 Subject: [PATCH 28/32] Add example for cross platform code --- .../cucumberplayground/test/StepDefinitions.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt index b96cf95..ce0dead 100644 --- a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt +++ b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt @@ -5,7 +5,6 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performScrollToIndex -import androidx.test.core.app.ActivityScenario import com.corrado4eyes.cucumberplayground.models.Strings import com.corrado4eyes.cucumbershared.tests.AppDefinitions import com.corrado4eyes.pistakio.DefaultApplicationAdapter @@ -15,10 +14,6 @@ import org.junit.Rule @WithJunitRule class StepDefinitions : En { - - private val arguments = mutableMapOf() - private var scenario: ActivityScenario<*>? = null - @get:Rule(order = 0) val testRule = createComposeRule() private val application = DefaultApplicationAdapter(testRule) @@ -65,11 +60,18 @@ class StepDefinitions : En { } is AppDefinitions.Platform.ISeeValueInScrollView -> When(definitionString) { index: String -> + + // Platform implementation testRule .onNodeWithTag(Strings.ScrollView.Tag.homeScrollView) .performScrollToIndex(index.toInt()) testRule.onNodeWithText(index).assertIsDisplayed() + + // Cross-platform implementation +// val element = application.findView(Strings.ScrollView.Tag.homeScrollView) +// element.swipeUntilIndex(index.toInt()) +// application.assert(element.isVisible()) } } } From 57ffde99babdaf8b71ccaf0540b2da1b45a9ca77 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Mon, 2 Oct 2023 11:32:39 +0200 Subject: [PATCH 29/32] Surround call by try/catches and add 25 retries on scrolls --- .../kotlin/com/corrado4eyes/pistakio/Node.kt | 14 ++++++++++++-- .../kotlin/com/corrado4eyes/pistakio/Node.kt | 8 ++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/Node.kt b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/Node.kt index 91872b8..f090d4f 100644 --- a/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/Node.kt +++ b/pistakio/src/androidMain/kotlin/com/corrado4eyes/pistakio/Node.kt @@ -38,11 +38,21 @@ actual class DefaultNode(internal val node: SemanticsNodeInteraction) : Node { } override fun swipeUntilIndex(index: Int, velocity: Float?) { - node.performScrollToIndex(index) + try { + node.performScrollToIndex(index) + } catch (e: IllegalArgumentException) { + // catch error thrown if the index is out of bounds. + // test should fail on the visibility assertion + } } override fun swipeUntilKey(key: Any, velocity: Float?) { - node.performScrollToKey(key) + try { + node.performScrollToKey(key) + } catch (e: IllegalArgumentException) { + // catch error thrown if the index is out of bounds. + // test should fail on the visibility assertion + } } override fun swipeUp() { diff --git a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/Node.kt b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/Node.kt index 4efbe0a..9e3737f 100644 --- a/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/Node.kt +++ b/pistakio/src/iosMain/kotlin/com/corrado4eyes/pistakio/Node.kt @@ -55,19 +55,23 @@ actual class DefaultNode( override fun swipeUntilIndex(index: Int, velocity: Float?) { val listElement = listItemAt(index) - while (!listElement.isHittable()) { + var retries = 0 + while (!listElement.isHittable() && retries < 25) { element.swipeUpWithVelocity( velocity = velocity?.toDouble() ?: 20.0 ) + retries++ } } override fun swipeUntilKey(key: Any, velocity: Float?) { val listElement = listItemWithTag(key.toString()).element - while (!listElement.isHittable()) { + var retries = 0 + while (!listElement.isHittable() && retries < 25) { element.swipeUpWithVelocity( velocity = velocity?.toDouble() ?: 20.0 ) + retries++ } } From bd05f28af1f10767bd617ec9e32eef77d8d9edf5 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Mon, 2 Oct 2023 11:36:39 +0200 Subject: [PATCH 30/32] Update ui tests --- .../cucumberplayground/test/StepDefinitions.kt | 5 +++-- ios/CucumberTests/CucumberTests.swift | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt index ce0dead..5c778d7 100644 --- a/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt +++ b/android/src/androidTest/kotlin/com/corrado4eyes/cucumberplayground/test/StepDefinitions.kt @@ -58,7 +58,6 @@ class StepDefinitions : En { is AppDefinitions.CrossPlatform.SetLoggedInUserEmail -> Given(definitionString) { loggedInEmail: String -> application.assertAll(AppDefinitions.CrossPlatform.SetLoggedInUserEmail(application, listOf(loggedInEmail)).runAndGetAssertions()) } - is AppDefinitions.Platform.ISeeValueInScrollView -> When(definitionString) { index: String -> // Platform implementation @@ -71,8 +70,10 @@ class StepDefinitions : En { // Cross-platform implementation // val element = application.findView(Strings.ScrollView.Tag.homeScrollView) // element.swipeUntilIndex(index.toInt()) -// application.assert(element.isVisible()) +// val text = application.findView(index) +// application.assert(text.isVisible()) } + } } } diff --git a/ios/CucumberTests/CucumberTests.swift b/ios/CucumberTests/CucumberTests.swift index 7519909..5770edc 100644 --- a/ios/CucumberTests/CucumberTests.swift +++ b/ios/CucumberTests/CucumberTests.swift @@ -107,11 +107,18 @@ import Cucumberish case .iSeeValueInScrollView(_): Then(definitionString) { args, userInfo in guard let scrollViewItemIndex = args?[0] as? String else { return } + // Platform implementation app.descendants(matching: .any) .matching(identifier: Strings.ScrollViewTag.shared.homeScrollView) .element.swipeUp() let scrollItem = app.staticTexts[scrollViewItemIndex] XCTAssert(scrollItem.waitForExistence(timeout: 0.1)) + + // Cross-platform implementation +// let element = applicationAdapter.findView(tag: Strings.ScrollViewTag.shared.homeScrollView) +// element.swipeUntilIndex(index: Int32(scrollViewItemIndex)!, velocity: 200.0) +// let text = applicationAdapter.findView(tag: scrollViewItemIndex) +// assertAll(assertions: [text.exists()]) } } } From 61b37da30b3fdaac5513e64bc4c36db7df4f4760 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Mon, 2 Oct 2023 11:38:09 +0200 Subject: [PATCH 31/32] Add logout call if the test configuration states that the user is logged out --- .../cucumberplayground/viewModels/main/MainViewModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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..a9707c8 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 @@ -1,8 +1,8 @@ package com.corrado4eyes.cucumberplayground.viewModels.main -import com.corrado4eyes.cucumberplayground.services.AuthService import com.corrado4eyes.cucumberplayground.models.TestConfiguration import com.corrado4eyes.cucumberplayground.models.User +import com.corrado4eyes.cucumberplayground.services.AuthService import com.splendo.kaluga.architecture.observable.toInitializedObservable import com.splendo.kaluga.architecture.viewmodel.BaseLifecycleViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -33,6 +33,8 @@ class MainViewModel( if (testConfiguration != null) { if (testConfiguration.isLoggedIn) { authService.login(testConfiguration.testEmail, "1234") + } else { + authService.logout() } } authService.observeUser.collect { user -> From da1690cbebf1d08147c105cf66424e143909e5d6 Mon Sep 17 00:00:00 2001 From: corrado4eyes Date: Mon, 2 Oct 2023 13:45:37 +0200 Subject: [PATCH 32/32] Add documentation --- .../pistakio/ApplicationAdapter.kt | 14 +++++++ .../kotlin/com/corrado4eyes/pistakio/Node.kt | 39 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt index 3bedc09..c3d6fb0 100644 --- a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/ApplicationAdapter.kt @@ -36,8 +36,22 @@ interface ApplicationAdapter { */ val applicationArguments: ApplicationArguments + /** + * Launches the app on the given [identifier] and with the given [arguments] + * @param identifier On android it represents the name of the Activity. + * @param arguments A map passed to the Activity intent or on iOS the ProcessInfo.processInfo.environment. + */ fun launch(identifier: String? = null, arguments: Map) + + /** + * Returns a [Node] of an element with the given [tag] + * @param tag Tag or text of a UI element. + */ fun findView(tag: String): Node + + /** + * Method to be called when you want to tear down the app. + */ fun tearDown() fun assert(assertionResult: AssertionResult) diff --git a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt index 4f5ffd8..6906be2 100644 --- a/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt +++ b/pistakio/src/commonMain/kotlin/com/corrado4eyes/pistakio/Node.kt @@ -50,16 +50,55 @@ interface Node { */ fun isHintEqualTo(value: String, contains: Boolean): AssertionResult + /** + * Does a swipeUp action on an element. + */ + fun swipeUp() + /** + * Does a swipeUp action for a given [SwipeDuration] duration. + * @param swipeDuration The duration of the swipe. + */ fun swipeUp(swipeDuration: SwipeDuration) + + /** + * Does a swipeUp action from [startY] to [endY] coordinates on the view. + * @param startY Start coordinate + * @param endY End coordinate + */ fun swipeUp(startY: Float, endY: Float) + /** + * Does a swipeDown action on an element. + */ fun swipeDown() + + /** + * Does a swipeDown action for a given [SwipeDuration] duration. + * @param swipeDuration The duration of the swipe. + */ fun swipeDown(swipeDuration: SwipeDuration) + + /** + * Does a swipeDown action from [startY] to [endY] coordinates on the view. + * @param startY Start coordinate + * @param endY End coordinate + */ fun swipeDown(startY: Float, endY: Float) + /** + * Swipe the [Node] until the View with the given index is visible. + * @param index Index of the view inside a scrollable view + * @param velocity Speed of the swiping action + */ fun swipeUntilIndex(index: Int, velocity: Float? = null) + + /** + * Swipe the [Node] until the View with the given key is visible. + * @param index Index of the view inside a scrollable view + * @param velocity Speed of the swiping action + */ fun swipeUntilKey(key: Any, velocity: Float? = null) // fun doubleTap()