Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 249/compose support #307

Merged
merged 30 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
438bd46
#249. Kakao Compose is included
matzuk Oct 25, 2021
0db17b6
#249. fixed some mistakes in Robolectric wiki
matzuk Nov 3, 2021
ed71945
#249. libraries update and including
matzuk Nov 3, 2021
56b727c
#249. main compose semantics interceptors are included to Kaspresso
matzuk Nov 4, 2021
c75259d
#249. Jetpack Compose Main page (template) in the sample is ready
matzuk Nov 4, 2021
6df379b
#249. The first test with Jetpack Compose in Kaspresso is ready
matzuk Nov 4, 2021
7211a8b
#249. Kautomator refactoring. loadView logic was removed because this…
matzuk Nov 5, 2021
2b90a5f
#249. Flaky defence is ready. Copy of ComposeOperations typealiases i…
matzuk Nov 6, 2021
61a7b45
#249. Improved stability of Kautomator due to changed logic of UiObje…
matzuk Nov 6, 2021
7a77c87
#249. Improved stability of Kautomator due to changed logic of UiObje…
matzuk Nov 6, 2021
49f1adb
#249. static onUiScreen method for Kamulator screens
matzuk Nov 6, 2021
3d7da93
#249. light changes in ComposeSimpleFlakyTest
matzuk Nov 6, 2021
a2094e1
#249. FailureLogging interceptors come back
matzuk Nov 6, 2021
d08bf45
#249. KakaoCompose 0.0.3 is included
matzuk Nov 7, 2021
e3a7a32
#249. Kaspresso compose is available for Jetpack Compose. ElementLoad…
matzuk Nov 7, 2021
0f028fa
#249. Robolectric support for Jetpack Compose
matzuk Nov 7, 2021
068f0b0
#249. small improvements in build.gradle
matzuk Nov 7, 2021
67860e2
#249. AutoScroll interceptor is ready
matzuk Nov 7, 2021
aeb5e5a
#249. detekt fixes
matzuk Nov 7, 2021
7b626a1
#249. Jetpack Compose readme
matzuk Nov 8, 2021
c8aa36b
#249 Jetpack Compose doc
matzuk Nov 8, 2021
44f3bbd
#249. Reverted the previous behavior of ObjectAutoScrollProviderImpl
matzuk Nov 14, 2021
1165160
#249. Changed the order of behavior interceptors to improve stability
matzuk Nov 14, 2021
dc905e6
#249. Up Kakao-Compose lib version
matzuk Nov 14, 2021
abf3983
#249. ElementLoaderParams
matzuk Nov 14, 2021
2edacd0
#249. reFindUiObject again
matzuk Nov 14, 2021
795dd5a
#249. provide libs.composeUiTest as api dependency
matzuk Nov 14, 2021
387549a
#249. renaming in VM
matzuk Nov 14, 2021
22aaa15
#249. up Kakao-Compose library version
matzuk Nov 15, 2021
60f586d
#249. readme changes
matzuk Nov 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

Kaspresso is a great framework for UI testing. Based on [Espresso](https://developer.android.com/training/testing/espresso) and [UI Automator](https://developer.android.com/training/testing/ui-automator), Kaspresso provides a wide range of additional amazing features, such as:
* 100% stability, no flakiness.
* *[WIP] Jetpack Compose support.*
* Significantly faster execution of UI Automator commands.
With Kaspresso, some UI Automator commands run **10 times faster**!
* Excellent readability due to human DSL.
Expand Down Expand Up @@ -217,6 +218,12 @@ Kaspresso can generate very detailed Allure-reports for each test:
![](https://habrastorage.org/webt/tq/t7/ch/tqt7chcdczrgduhoukqhx1ertfc.png)
More information is available [here](/wiki/09_Kaspresso-Allure.md).

### Jetpack Compose support

Now, you can write your Kaspresso tests for Jetpack Compose screens! DSL and all principles are the same.
So, you will not see any difference between tests for View screens and for Compose screens.
More information is available [here](/wiki/10_Jetpack-Compose.md).

## Philosophy

The tool itself, even the perfect one, can not solve all the problems in writing UI tests. It’s important to know how to write tests and how to organize the entire process. Our team has great experience in introducing autotests in different companies. We shared our knowledge on [writing autotests](/wiki/04_How_to_write_autotests.md).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,25 @@ configure<BaseExtension> {
named("androidTest").configure { java.srcDir("src/androidTest/kotlin") }
}

buildToolsVersion("29.0.3")
compileSdkVersion(29)
// temp solution of the following bug
// https://stackoverflow.com/questions/67358179/android-espresso-test-error-no-static-method-loadsingleserviceornull
// in the Kaspresso, it is caused by androidx.compose.ui:ui-test-junit4:1.0.3 in Kakao-compose that uses androidx.test.espresso:espresso-core:3.3.0 under the hood
// but we expect using of androidx.test.espresso:espresso-core:3.4.0
configurations.all {
resolutionStrategy {
force("androidx.test:monitor:1.4.0")
}
}

// temp solution
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RuslanMingaliev need your help here

// appeared with kakao-compose library including
// similar to https://github.com/Kotlin/kotlinx.coroutines/issues/2023
packagingOptions {
exclude("META-INF/*")
}

buildToolsVersion("30.0.3")
compileSdkVersion(31)

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ android {
}
}

@Suppress("UnstableApiUsage")
onVariants {
androidTest {
enabled = false
}
}
// todo doesn't work on AGP 4.2.2
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RuslanMingaliev such api is not available now

// @Suppress("UnstableApiUsage")
// onVariants {
// androidTest {
// enabled = false
// }
// }
}
46 changes: 34 additions & 12 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
[versions]
kotlin = "1.4.32"
kotlin = "1.5.21"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.5.31 causes weird problems, postponed

detekt = "1.18.1"
espresso = "3.3.0"
allure = "2.2.6"
espresso = "3.4.0"
allure = "2.2.7"
compose = "1.0.2"
activityCompose = "1.3.1"

[libraries]
# plugins
kotlinPlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
androidPlugin = "com.android.tools.build:gradle:4.1.0"
androidPlugin = "com.android.tools.build:gradle:4.2.2"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Мб нам его до 7 поднять?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried but I got a lot of errors with our plugins =(


# libraries
kotlinCli = "org.jetbrains.kotlinx:kotlinx-cli:0.3"
Expand All @@ -17,28 +19,47 @@ detektFormatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", v
kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
espressoCore = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
espressoWeb = { module = "androidx.test.espresso:espresso-web", version.ref = "espresso" }
testCore = "androidx.test:core:1.3.0"
testCore = "androidx.test:core:1.4.0"
uiAutomator = "androidx.test.uiautomator:uiautomator:2.2.0"
fragmentTesting = "androidx.fragment:fragment-testing:1.3.2"
fragmentTesting = "androidx.fragment:fragment-testing:1.3.6"
# upgrading to 4.6.1 is blocked by https://github.com/robolectric/robolectric/issues/6593
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a bug, there is a solution in the comments

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure that you are right. Ok, can you try to fix it?

robolectric = "org.robolectric:robolectric:4.5.1"

androidXCore = "androidx.core:core:1.3.0"
androidXRules = "androidx.test:rules:1.3.0"
androidXTest = "androidx.test.ext:junit:1.1.2"
androidXTestKtx = "androidx.test.ext:junit-ktx:1.1.2"
androidXCore = "androidx.core:core:1.7.0"

androidXRules = "androidx.test:rules:1.4.0"
androidXTest = "androidx.test.ext:junit:1.1.3"
androidXTestKtx = "androidx.test.ext:junit-ktx:1.1.3"

composeActivity = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
composeUiTooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
composeMaterial = { module = "androidx.compose.material:material", version.ref = "compose" }
composeTestManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" }
composeCompiler = { module = "androidx.compose.compiler:compiler", version.ref = "compose" }

composeUiTest = { module = "androidx.compose.ui:ui-test", version.ref = "compose" }

composeJunit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" }

lifecycleViewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
lifecycleLiveDataKtx = "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
composeNavigation = "androidx.navigation:navigation-compose:2.4.0-beta02"
lifecycleViewModelComposeKtx = "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0"
composeRuntimeLiveData = "androidx.compose.runtime:runtime-livedata:1.0.5"

appcompat = "androidx.appcompat:appcompat:1.3.0"
material = "com.google.android.material:material:1.2.0"
constraint = "androidx.constraintlayout:constraintlayout:2.0.0"

kakao = "io.github.kakaocup:kakao:3.0.2"
kakao = "io.github.kakaocup:kakao:3.0.5"
kakaoCompose = "io.github.kakaocup:compose:0.0.5"
gson = "com.google.code.gson:gson:2.8.6"

junit = "junit:junit:4.13"
junitJupiter = "org.junit.jupiter:junit-jupiter:5.6.2"
truth = "com.google.truth:truth:1.0"
runner = "androidx.test:runner:1.3.0"
orchestrator = "androidx.test:orchestrator:1.3.0"
orchestrator = "androidx.test:orchestrator:1.4.0"

okhttp = "com.squareup.okhttp3:okhttp:4.9.1"
kotson = "com.github.salomonbrys.kotson:kotson:2.5.0"
Expand All @@ -51,3 +72,4 @@ allureKotlinAndroid = { module = "io.qameta.allure:allure-kotlin-android", versi
[bundles]
espresso = ["espressoCore", "espressoWeb"]
allure = ["allureKotlinModel", "allureKotlinCommons", "allureKotlinJunit4", "allureKotlinAndroid"]
compose = ["composeActivity", "composeUiTooling", "composeMaterial", "composeTestManifest", "composeCompiler"]
2 changes: 2 additions & 0 deletions kaspresso/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ publish {
dependencies {
api(projects.kautomator)
api(libs.kakao)
api(libs.kakaoCompose)
api(libs.bundles.espresso)
api(libs.uiAutomator)
api(libs.androidXCore)
api(libs.androidXRules)
api(libs.composeUiTest)

implementation(libs.kotlinStdlib)
implementation(libs.gson)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,23 @@ class ObjectAutoScrollProviderImpl(
* Scrolls to the bottom and looks for the given view. Invokes the action if the view was found.
*/
do {
if (interaction.tryToFindUiObject()) {
logger.i("UiView autoscroll successfully performed.")
if (interaction.uiObject2 != null) {
logger.i("UiObject autoscroll to the bottom successfully performed.")
return action.invoke()
} else {
interaction.reFindUiObject()
}
} while (scrollable.scrollForward())

/**
matzuk marked this conversation as resolved.
Show resolved Hide resolved
* Scrolls to the beginning and looks for the given view. Invokes the action if the view was found.
*/
do {
if (interaction.tryToFindUiObject()) {
logger.i("UiView autoscroll successfully performed.")
if (interaction.uiObject2 != null) {
logger.i("UiObject autoscroll to the beginning successfully performed.")
return action.invoke()
} else {
interaction.reFindUiObject()
}
} while (scrollable.scrollBackward())

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.kaspersky.kaspresso.autoscroll

import androidx.compose.ui.test.performScrollTo
import com.kaspersky.kaspresso.internal.extensions.other.isAllowed
import com.kaspersky.kaspresso.logger.UiTestLogger
import com.kaspersky.kaspresso.params.AutoScrollParams
import io.github.kakaocup.compose.intercept.interaction.ComposeInteraction

class SemanticsAutoScrollProviderImpl(
private val logger: UiTestLogger,
private val autoScrollParams: AutoScrollParams
) : AutoScrollProvider<ComposeInteraction> {

/**
* Invokes the given [action] and calls [scroll] if it fails. Helps in cases when test fails because of the
* need to scroll to interacted view.
*
* @param interaction the instance of [ComposeInteraction] interface to perform actions and assertions.
* @param action the actual action on the interacted view.
*
* @throws Throwable if the exception caught while invoking [action] is not allowed via [ALLOWED_EXCEPTIONS].
* @return the result of [action] invocation.
*/
override fun <T> withAutoScroll(interaction: ComposeInteraction, action: () -> T): T {
return try {
action.invoke()
} catch (error: Throwable) {
if (error.isAllowed(autoScrollParams.allowedExceptions)) {
return scroll(interaction, action, error)
}
throw error
}
}

/**
* Performs the autoscrolling functionality. Performs scroll and re-invokes the given [action].
*
* @param interaction the instance of [ComposeInteraction] interface to perform actions and assertions.
* @param action the actual action on the interacted view.
* @param cachedError the error to be thrown if autoscroll would not help.
*
* @throws cachedError if autoscroll action did not help.
* @return the result of [action] invocation.
*/
override fun <T> scroll(interaction: ComposeInteraction, action: () -> T, cachedError: Throwable): T {
return try {
interaction.semanticsNodeInteraction.performScrollTo()
logger.i("SemanticsNodeInteraction autoscroll successfully performed.")
action.invoke()
} catch (error: Throwable) {
logger.i("SemanticsNodeInteraction autoscroll did not help. Throwing exception.")
throw cachedError
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import com.kaspersky.components.kautomator.intercept.operation.UiObjectAction
import com.kaspersky.components.kautomator.intercept.operation.UiObjectAssertion
import com.kaspersky.kaspresso.compose.pack.branch.ComplexComposeBranch
import com.kaspersky.kaspresso.compose.pack.branch.ComplexComposeBranchBuilder
import io.github.kakaocup.compose.intercept.delegate.ComposeInterceptable
import io.github.kakaocup.compose.node.action.NodeActions
import io.github.kakaocup.compose.node.action.TextActions
import io.github.kakaocup.compose.node.assertion.NodeAssertions

/**
* The builder class for parameters of [com.kaspersky.kaspresso.compose.ComposeProvider.compose] method.
Expand Down Expand Up @@ -50,6 +54,20 @@ class ActionsOnElementsPack {
.also { complexComposeBranchBuilders += it }
}

/**
* Adds the [element] of type [Type] and the [action] to [complexComposeBranchBuilders] and [action] for future composing
* where [Type] is bounding by UiBaseView (Kautomator)
*
* @param element the interacted view.
* @param action actions or assertions on the interacted view.
*/
fun <Type> or(element: Type, action: Type.() -> Unit): ComplexComposeBranchBuilder<Type>
where Type : NodeActions, Type : NodeAssertions,
Type : TextActions, Type : ComposeInterceptable {
return ComplexComposeBranchBuilder(element, { action.invoke(element) })
.also { complexComposeBranchBuilders += it }
}

/**
* @return the built parameters for [com.kaspersky.kaspresso.compose.ComposeProvider.compose] method.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.kaspersky.kaspresso.elementloader

/**
* The interface to provide element loader functionality.
*/
interface ElementLoaderProvider {

/**
* Invokes the given [action] and calls [elementLoader] if it fails. Helps in cases when test fails because
* the element is outdated and must be reloaded using its selectors/matchers.
*
* @param elementLoader the lambda to reload the element.
* @param action the actual action on the interacted view.
*
* @return the result of [action] invocation.
*/
fun <ActionType> passAction(
elementLoader: () -> Unit,
action: () -> ActionType
): ActionType
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.kaspersky.kaspresso.elementloader

import com.kaspersky.kaspresso.internal.extensions.other.isAllowed
import com.kaspersky.kaspresso.logger.UiTestLogger
import com.kaspersky.kaspresso.params.ElementLoaderParams

/**
* The implementation of the [ElementLoaderProvider] interface
*/
class ElementLoaderProviderImpl(
private val logger: UiTestLogger,
private val params: ElementLoaderParams,
) : ElementLoaderProvider {

/**
* Invokes the given [action] and calls [elementLoader] if it fails. Helps in cases when test fails because
* the element is outdated and must be reloaded using its selectors/matchers.
*
* @param elementLoader the lambda to reload the element.
* @param action the actual action on the interacted view.
*
* @return the result of [action] invocation.
*/
override fun <ActionType> passAction(
elementLoader: () -> Unit,
action: () -> ActionType
): ActionType {
return try {
action.invoke()
} catch (error: Throwable) {
matzuk marked this conversation as resolved.
Show resolved Hide resolved
if (error.isAllowed(params.allowedExceptions)) {
logger.i("Reloading of the element is started")
elementLoader.invoke()
logger.i("Reloading of the element is finished")
logger.i("Repeat action again with the reloaded element")
action.invoke()
} else {
throw error
}
}
}
}
Loading