Giter Site home page Giter Site logo

agoda-com / kakao Goto Github PK

View Code? Open in Web Editor NEW
1.1K 54.0 101.0 1.29 MB

This repo is no longer supported. Please visit a https://github.com/KakaoCup/Kakao

License: Apache License 2.0

Kotlin 100.00%
espresso kotlin dsl ui-testing testing-framework testing-tool testing-library android android-ui android-test

kakao's Introduction

Kakao project is now being developed here: KakaoCup/Kakao.

Kudos to all contributors and maintainers!

This repo is no longer supported.

Archived versions are available in maven central, for new releases please follow a new repo.

Kakao

Download CircleCI Kotlin version badge Android Arsenal

Nice and simple DSL for Espresso in Kotlin

Introduction

At Agoda, we have more than 1000 automated tests to ensure our application's quality and give our best experience to the user. All of them are written with Espresso from Google. Even though Espresso is working really well with our test, the code readability is quite low. Let's look at some of the examples of how we write the test.

onView(allOf(withId(R.id.price_item), hasDescendant(withText("Standard Rate"))))
        .check(matches(withEffectiveVisibility(Visibility.VISIBLE)));

This is an example just to check the visibility and you can see that it's not looking that good. As Agoda developers, we want to improve not just our codebase quality, but also our implementation of tests as well. This is why we are introducing Kakao. The library that will make you enjoy writing automated tests like you are drinking a hot chocolate.

coco

Benefits

  • Readability
  • Reusability
  • Extensible DSL

How to use it

Create Screen

Create your entity Screen where you will add the views involved in the interactions of the tests:

class FormScreen : Screen<FormScreen>()

Screen can represent the whole user interface or a portion of UI. If you are using Page Object pattern you can put the interactions of Kakao inside the Page Objects.

Create KView

Screen contains KView, these are the Android Framework views where you want to do the interactions:

class FormScreen : Screen<FormScreen>() {
    val phone = KEditText { withId(R.id.phone) }
    val email = KEditText { withId(R.id.email) }
    val submit = KButton { withId(R.id.submit) }
}

Kakao provides different types depending on the type of view:

  • KView
  • KEditText
  • KTextView
  • KButton
  • KImageView
  • KWebView
  • KCheckbox
  • KViewPager
  • KSeekBar
  • KSwitch
  • and more

Every KView contains matchers to retrieve the view involved in the ViewInteraction. Some examples of matchers provided by Kakao:

  • withId
  • withText
  • withContentDescription
  • withDrawable
  • withBackgroundColor
  • and more

Like in Espresso you can combine different matchers:

val email = KEditText { 
    withId(R.id.email)
    withText(R.string.email)
}

And you can use your custom matchers:

val email = KEditText { 
    withId(R.id.email)
    matches { MyCustomMatcher.matches(position) }
}

Write the interaction.

The syntax of the test with Kakao is very easy, once you have the Screen and the KView defined, you only have to apply the actions or assertions like in Espresso:

onScreen<FormScreen> {
    phone {
       hasText("971201771")
    }
    button {
       click()
    }
}

Kakao provides multiple actions/assertions based on Espresso. Furthermore, you can combine them, just like the matchers. You can use your custom assertions or your custom actions too:

onScreen<FormScreen> {
    phone {
       assert { MyCustomAssertion.isThaiNumber() }
    }
    button {
       act { MyCustomAction.clickOnTheCorner() }
    }
}

Advanced

ListView/RecyclersView

Kakao offers an easy way to interact with your RecyclerView and ListView

Create the KListView/KRecyclerView

Inside your Screen create the KView matching with your view:

For KListView:

val list = KListView { builder = { withId(R.id.list) } }

For KRecyclerView:

val recycler = KRecyclerView { builder = { withId(R.id.recycler_view) } }

You can combine different matchers to retrieve your view.

Create KAdapterItem/KRecyclerItem

Every adapter contains different Items, Kakao provides an easy way to define the different items of your adapter with KAdapterItem and KRecyclerItem. If your adapter contains multiple Items but your interactions in your tests only work with one is not required to create all of them.

KAdapterItem

class Item(i: DataInteraction) : KAdapterItem<Item>(i) {
    val title = KTextView(i) { withId(R.id.title) }
    val subtitle = KTextView(i) { withId(R.id.subtitle) }
    val button = KButton(i) { withId(R.id.button) }
}

KRecyclerItem

class Item(parent: Matcher<View>) : KRecyclerItem<Item>(parent) {
    val title: KTextView = KTextView(parent) { withId(R.id.title) }
    val subtitle: KTextView = KTextView(parent) { withId(R.id.subtitle) }
}

The KView defined in the Item corresponds views used on the Item. You can assign the KItems to the KListView/ KRecyclerView like:

val recycler: KRecyclerView = KRecyclerView({
    withId(R.id.recycler_view)
}, itemTypeBuilder = {
    itemType(::Item)
})

And finally your final interaction will be:

onScreen<RecyclerScreen> {
    recycler {
        firstChild<TestRecyclerScreen.Item> {
            isVisible()
            title { hasText("Title 1") }
        }
    }
}

Kakao provides different accessors in the adapter:

  • childAt
  • firstChild
  • lastChild
  • childWith
Custom KView

If you have custom Views in your tests and you want to create your own KView, we have KBaseView. Just extend this class and implement as much additional Action/Assertion interfaces as you want. You also need to override constructors that you need.

class KMyView : KBaseView<KView>, MyActions, MyAssertions {
    constructor(function: ViewBuilder.() -> Unit) : super(function)
    constructor(parent: Matcher<View>, function: ViewBuilder.() -> Unit) : super(parent, function)
    constructor(parent: DataInteraction, function: ViewBuilder.() -> Unit) : super(parent, function)
}
Intercepting

If you need to add custom logic during the Kakao -> Espresso call chain (for example, logging) or if you need to completely change the ViewAssertion or ViewAction that are being sent to Espresso during runtime in some cases, you can use the intercepting mechanism.

Interceptors are lambdas that you pass to a configuration DSL that will be invoked before ViewInteraction, DataInteraction or Web.WebInteraction classes' perform and check calls happening from inside Kakao.

You have the ability to provide interceptors at 3 different levels: Kakao runtime, your 'Screen' classes and any individual KView instance.

On each invocation of Espresso function that can be intercepted, Kakao will aggregate all available interceptors for this particular call and invoke them in descending order: KView interceptor -> Active Screens interceptors -> Kakao interceptor.

Each of the interceptors in the chain can break the chain call by setting isOverride to true during configuration. In that case Kakao will not only stop invoking remaining interceptors in the chain, but will not perform the Espresso call. It means that in such case, the responsibility to actually invoke Espresso lies on the shoulders of the developer.

Here's the examples of intercepting configurations:

class SomeTest {
    @Before
    fun setup() {
        Kakao { // Kakao runtime
            intercept {
                onViewInteraction { // Intercepting calls on ViewInteraction classes across whole runtime
                    onPerform { interaction, action -> // Intercept perform() call
                        Log.d("KAKAO", "$interaction is performing $action")
                    }
                }
            }
        }
    }
    
    @Test
    fun test() {
        onScreen<MyScreen> {
            intercept {
                onViewInteraction { // Intercepting calls on ViewInteraction classes while in the context of MyScreen
                    onCheck { interaction, assertion -> // Intercept check() call
                        Log.d("KAKAO", "$interaction is checking $assertion")
                    }
                }
            }
            
            myView {
                intercept { // Intercepting ViewInteraction calls on this individual view
                    onPerform(true) { interaction, action -> // Intercept perform() call and overriding the chain 
                        // When performing actions on this view, Kakao level interceptor will not be called
                        // and we have to manually call Espresso now.
                        Log.d("KAKAO_VIEW", "$interaction is performing $action")
                        interaction.perform(action)
                    }
                }
            }
        }
    }
}

For more detailed info please refer to the documentation.

Setup

Following archived versions are available in maven central:

  • 2.4.0
  • 2.3.4
  • 2.2.0
  • 2.1.0
  • 2.1.0-support
  • 1.4.0-androidx

Maven

<dependency>
  <groupId>com.agoda.kakao</groupId>
  <artifactId>kakao</artifactId>
  <version><latest version></version>
  <type>pom</type>
</dependency>

or Gradle:

repositories {
    jcenter()
}

dependencies {
    androidTestImplementation 'com.agoda.kakao:kakao:<latest version>'
}

AndroidX

Default artifact starting from 2.0.0 includes AndroidX libraries to build upon. If you're still using old support libraries, please use 2.X.X-support artifact.

dependencies {
    androidTestImplementation 'com.agoda.kakao:kakao:2.1.0-support'
}

IMPORTANT: We stopped the development for the -support artifact and version 2.1.0-support is the latest version available with usage of support libraries. Please consider migrating to AndroidX.

Contribution Policy

Kakao is an open source project, and depends on its users to improve it. We are more than happy to find you interested in taking the project forward.

Kindly refer to the Contribution Guidelines for detailed information.

Code of Conduct

Please refer to Code of Conduct document.

License

Kakao is open source and available under the Apache License, Version 2.0.

Thanks to

kakao's People

Contributors

aafanasev avatar ahampe avatar almozavr avatar anton46 avatar antonbatishchev avatar ashdavies avatar carlochum avatar cdsap avatar darran-kelinske-fivestars avatar dazza5000 avatar disssection avatar ghostbuster91 avatar igorwojda avatar jkaan avatar judrummer avatar k-kagurazaka avatar keineantwort avatar mark-mark-mark avatar michaelbukachi avatar ming13 avatar nutjane avatar peerapon01 avatar psh avatar sebastienrouif avatar unlimity avatar v-rodionov avatar vacxe avatar verachadw avatar yapkolotilov avatar zkffkah avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

kakao's Issues

Support Toolbar

Have any plans to add toolbar support?
Some think to has a title, subtitle click in the back button

WithIndex is not working properly

Steps to reproduce:

  1. Create a layout with views with the same id
  2. Write a simple test using withIndex method
  3. Execute test

Observed Results:

Error message - Espresso could not found a matching view.

android.support.test.espresso.NoMatchingViewException: No views in hierarchy found matching: (0th view with: (with id: com.agoda.sample:id/input_layout))

Expected Results:

It should work.

Relevant Code:

Sample project with error

Cannot click items in nested RecyclerViews

Steps to reproduce:

This is my Screen definition:

  class DetailsScreen : Screen<DetailsScreen>() {
    val categoryList = KRecyclerView(
      { withId(R.id.menuCategoriesRecyclerView) },
      { itemType(::CategoryItem) }
    )
  }

  class CategoryItem(parent: Matcher<View>) : KRecyclerItem<CategoryItem>(parent) {
    val platesList = KRecyclerView(
      { withId(R.id.categoryRecyclerView) },
      { itemType(::PlateItem) }
    )
  }

  class PlateItem(parent: Matcher<View>) : KRecyclerItem<PlateItem>(parent) {
    val incrementButton = KButton(parent) { withId(R.id.plateIncrementImageButton) }
    val decrementButton = KButton(parent) { withId(R.id.plateDecrementImageButton) }
    val quantityLabel = KTextView(parent) { withId(R.id.plateQuantityTextView) }
  }

And this is the assertion I am trying to do:

  val screen = DetailsScreen()
    screen {
      categoryList {
        firstChild<CategoryItem> {
          platesList {
            isVisible()

            firstChild<PlateItem> {
              quantityLabel { hasText("0") }

              perform { incrementButton { click() } }

              quantityLabel { hasText("1") }

              perform { decrementButton { click() } }

              quantityLabel { hasText("0") }
            }
          }
        }
      }
    }

Observed Results:

The following exception is thrown:

java.lang.NoSuchMethodError: No direct method <init>(Landroid/support/test/espresso/action/Tapper;Landroid/support/test/espresso/action/CoordinatesProvider;Landroid/support/test/espresso/action/PrecisionDescriber;II)V in class Landroid/support/test/espresso/action/GeneralClickAction; or its super classes (declaration of 'android.support.test.espresso.action.GeneralClickAction' appears in /data/app/com.myapp.example-xxx==/base.apk)

Could this be because I cannot set parent to the nested KRecyclerView at CategoryItem?

Is it possible to use RecyclerViewItems that have no container layout?

Steps to reproduce:

  1. Create a RecycleView with a layout containing only a single element
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <data />
    <SomeView android:id="@+id/some_id"/>
</layout>
  1. Setup a KRecyclerView with an item containing only someView
class MyScreen : Screen<MyScreen>() {
    val recyclerView= KRecyclerView({ withId(R.id.myRV) }, (::Item) })
    class Item(parent: Matcher<View>) : KRecyclerItem<Item>(parent) {
        val someView= KView(parent) { withId(R.id.some_id) }
    }
}

  1. Try to use it
MyScreen(){
    recyclerView {
        firstChild<MyScreen.Item> {
            someView { doStuff()}
        }
    }
}

Observed Results:

android.support.test.espresso.NoMatchingViewException: No views in hierarchy found matching: (is descendant of a: (view holder at 0 position of recycler view: (with id: myapp:id/myrecyclerview)) and with id: 2131363029)

Layout contains a recyclerview with 9 direct children of SomeView Type with matching Ids. I think the problem is that it assumes that the item i'm looking for is a grandchild of the recycler view rather than a child. If I wrap someView in a LinearLayout, things works as expected

Expected Results:

I expected being able to use recycler views with only direct children. Am I doing it wrong or is this a bug? Is it possible to setup the screen so that someView IS the KRecyclerItem?

I hope the quality of this was enough, I'm new with both kotlin and android.

Unable to click on a view inside a recyclerview viewholder

Steps to reproduce:

  1. Add a screen with a recyclerview
  2. Add a viewholder with a button to the recyclerview
  3. In the tests, go to the screen
  4. Get the first item inside the recyclerview
  5. Click on the button of the first item inside the recyclerview

Observed Results:

  • Instead of clicking on the button, the test runner clicks on the viewholder itself

Expected Results:

  • The test runner should click on the button inside the view holder

Relevant Code:

class ScreenA : Screen<ScreenA>() {

    val recycler by lazy {
        KRecyclerView({
            this.withId(R.id.recyclerview)
        }, {
            itemType(::ViewHolder)
        })
    }

    class ViewHolder(parent: Matcher<View>) : KRecyclerItem<ViewHolder>(parent) {
        val title = KView(parent) { withId(R.id.title) }
        val moreOptions = KView { withId(R.id.more)  }
    }
}
Screen.onScreen<ScreenA> {
    recycler.firstChild<ScreenA.ViewHolder> { 
        moreOptions.click()
    }
}

Document access to root of viewHolder

Accessing the root view of an item in a recyclerView is not straightforward and is not documented.
As said in #99 , create an example in the sample app to showcase how to access the root view.

Add KSwitch

Android Switch definition :
A Switch is a two-state toggle switch widget that can select between two options. The user may drag the "thumb" back and forth to choose the selected option, or simply tap to toggle as if it were a checkbox.

Adding a KSwitch would allow developers to test both click and swipe gesture and make sure they implemented the OnCheckedChangeListener

Wrong @DslMarker annotations

Hi guys!
I have some problem

Right now i can write something like that

cardsRecyclerView {
    cardsRecyclerView {
        cardsRecyclerView {
            hasDescendant {
                hasDescendant {
                    hasDescendant {  }
                }
            }
        }
    }
}

This code is totally wrong
The construction does not make sense (the list in the same list in the same list checks something there and 2 more levels of checks). This is impossible to create in a real ui

This is because the kakao dsl used different @DskMarker annotations
I think you should choose one marker annotation for all dsl and the problem will gone

KAdapterItem with multiple layout types

Hi guys!

Is it possible to "ignore" some fields in KAdapterItem? I mean could it be nullable (and don't throw exception) when matching view is not found?

The problem is, i've got ListView with multiple view types, some contain TextView, some do not. I'd like to run some methods on them, like:

item?.apply {
  hasText("Sample text")
}

Question about the minSdkVersion

Any particular reason for using the minSdkVersion =16, because our project is using a minSdkVersion 14 and have some history problem to remain it there. It would be lovely that we can decrease it to 14, thank you.

DrawableMatcher should correctly match provided Drawable objects

Steps to reproduce:

  1. Attempt to match a drawable in a screen with hasDrawable(@DrawableRes resId: Int) and observe test success.
  2. Attempt to match same drawable with hasDrawable(drawable: Drawable), passing in a loaded drawable from the previous resource ID. The test will fail.

Observed Results:

Failure with exception android.content.res.Resources$NotFoundException: Resource ID #0xffffffff

It seems like DrawableMatcher.matchesSafely mixes up the assignment of the drawable parameter. It should probably be assigned to the expectedDrawable, not the convertDrawable.

Add support for components from support design library

It would be good to have support for components from android-support-design library.
Below is a list of actions/assertions which I found missing:

  • TextInputEditText
    • has hint
    • has error
  • NavigationDrawer
    • open/close
  • SwipeRefreshLayout
    • swipeToRefresh
    • isLoading

Also it would be good to know how to interact with snackbar.

Bundled support libraries

Would it be possible to bundle latest support library versions, instead of 25.4.0? It would be awesome to import those from the project, if they are good enough.

Can't be called in this context by implicit receiver

On a new 2.0.0 AndroidX version of the lib, the following code gives a compilation error:
val lol Can't be called in this context by implicit receiver. Use explicit one if necessary

val lol = "text"
val view = KTextView { containsText(lol) }

It is fixable by adding [email protected] to a lol val.
But I'm kinda missing what has changed since 2.0.0 that I can't use an implicit receiver anymore.
I have dozens of similar cases using the ViewBuilder, and after migration to AndroidX and 2.0.0 all of them require adding an explicit receiver.

Can you please clarify what's happening here and potentially fix it?

KRecyclerView multiple childs assert not success

Steps to reproduce:

  1. Build Screen with recycler view
  2. On your code, set more than one item at recyclerview adapter
  3. Try to do assert with more than one children

Observed Results:

https://gist.github.com/alorma/4b2cf664f6f92c6ba4f9abc8416c335e

+---------->AppCompatTextView{id=2131361887, res-name=addressView, visibility=VISIBLE, width=954, height=126, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.support.constraint.ConstraintLayout$LayoutParams@fff2b2b, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=Calle Llull nº 113 - 119, 08025 Barcelona, input-type=0, ime-target=false, has-links=false} ****MATCHES****

As you can see, the item shows a ****MATCHES****

Expected Results:

Two assertions success, as you can see, the second assert ("Calle Llull nº 113 - 119, 08025 Barcelona") is on the output of the view hierarchy

Relevant Code:

      screen {
          address {
              firstChild<AddressItem> {
                  name { hasText("Calle falsa nº 123, 08112 Springfield") }
              }
              lastChild<AddressItem> {
                  name { hasText("Calle Llull nº 113 - 119, 08025 Barcelona") }
              }
          }
      }

Cannot click on item in RecyclerView

Hi! I've some problems with the KRecyclerView. Every item in a recycler contains a linear layout with two or three TextViews. All I need is to click on item which contains a particular text. How can I do that?

myKRecyclerView.childAt<MyKRecyclerItem>(withText(particularText));
myKRecyclerView.childAt<MyKRecyclerItem>(withMatcher(ViewMatchers.withChild(ViewMatchers.withText(particularText))));

Both variants throw NoMatchingViewException, but I see in the stack trace that the view with the text is contained in the recycler.

Add DslMarker annotations

To improve experience of writing tests, we need to correctly annotate all classes that we have with corresponding @DslMarker marked annotations.
This will allow auto-completion and compiler implicitly infer receiver inside DSL call stack thus making usage of Kakao more easier and pleasant.

Get view matcher from screen context

Hey,
I trying to refactor tests written with Espresso to Kakao
Can't figure out how to refactor this line.
onView(titleTextMatcher).check(isCompletelyAbove(googleBtnMatcher))

Tried something like that.. but isCompletelyAbove() expect to receive googleButton matcher that I can't retrieve.

    signinScreen {
        googleButton {}
        title {
            isCompletelyDisplayed()
            matches { isCompletelyAbove([email protected]) }
        }
    }

Thanks.

hasDrawable doesn't work when ImageView has android:tint

Steps to reproduce:

  1. Create a layout with an
<ImageView android:src="R.drawable.my_icon" android:tint="R.color.white" android:layout_width="40dp" android:layout_height="40dp"/>
  1. Create a test that uses hasDrawable(R.drawable.my_icon)

Observed Results:

The method hasDrawable does not see the drawables as equal

Expected Results:

I expected the drawables to be equal, or provide a way to add the expected tint with the hasDrawable call

Possible workaround:

If i would comment out lines 86 until 88 (the canvas drawing) in DrawableMatcher.kt the comparing works, but i'm not sure if that will break anything.

AndroidX migration

Prior to releasing 2.0 we need to decide how we want to go with the androidx artifacts. The popular opinion I heard is to make a branch at current state and release it as 2.0.0-support, migrate master branch to androidx and release it as 2.0.0.

What are your thoughts on this?

Support GridLayout

I'm changing one component from RecyclerView to GridLayout. This critical component have lot of UI Test using kakao. However I don't see the equivalent of GridLayout for Kakao.

Error with click on tabs

More Than a bug is a question, Im creating a Kview in a tab with this code,
KView { withParent { withClassName(endsWith("TabView")) } withText(sectionName) } after that I perform a scrollTo and a click, this actions happens but then I get an error Error performing 'single click - At Coordinates: 922, 272 and precision: 16, 16' on view '(has parent matching: (with class name: a string ending with "TabView") and with text: is "Test Tab")' , any idea in what is the problem.

Kakao use kotlin reflection api?

I have found some error with RecyclerView when forgot to add itemType to KRecyclerView

 val provideItem = itemTypes.getOrElse(T::class) {
            throw IllegalStateException("${T::class.qualifiedName} did not register to KRecyclerView")
        }.provideItem 

On this snippet (In Views.kt line 176), when itemType not found, It will throw kotlin.jvm.KotlinReflectionNotSupportedError: Kotlin reflection implementation is not found at runtime. Make sure you have kotlin-reflect.jar in the classpath because we use qualifiedName.

Should we add kotlinReflection into testCompile dependency or change qualifiedName to anotherName (ex. simpleName?

Refactor current code base

We have segregated all current code in several files, and these files are becoming really huge.
We need to separate these files. I propose following:

  • Keep each view class and it's actions and assertions interfaces in separate files
  • Move Builders.kt classes to com.agoda.kakao.builder package and separate by class name
  • Move Matchers.kt classes to com.agoda.kakao.matcher package and separate by class name
  • Create com.agoda.kakao.action package and put there all custom action that we have created
  • Create com.agoda.kakao.assertion package and put there all custom assertions that we have created

Add KDatePicker view

Right not DatePicker from Android SDK is not supported by Kakao.
We need to add KDatePicker class to Kakao and add support for basic operations (like set date, get date, etc.)

RecyclerView AmbiguousViewMatcherException with ONE SINGLE MATCH 0_o

Steps to reproduce:

I have object like

 object CatalogItemsScreen : Screen<CatalogItemsScreen>() {

    val recycler = KRecyclerView(
        builder = { withId(R.id.recyclerView) },
        itemTypeBuilder = { itemType(::Item) }
    )

    class Item(parent: Matcher<View>) : KRecyclerItem<Item>(parent) {
        val clickable = KView {
            withParent { withMatcher(parent) }
            withId(R.id.clickableView)
        }
    }
}

and use it like

            CatalogItemsScreen {
                recycler.childAt<CatalogItemsScreen.Item>(1) {
                    clickable.click()
                }
            }  

Observed Results:

at this place I got exception (sorry for full listing, but it need for clearly show the issue)

Message in exception say "MULTIPLE views marked", but i found with simple text search ONLY ONE mark. How can it be?

androidx.test.espresso.AmbiguousViewMatcherException: '(has parent matching: (view holder at 1 position of recycler view: (with id: ru.butik.android.debug:id/recyclerView)) and with id: ru.butik.android.debug:id/clickableView)' matches multiple views in the hierarchy.
Problem views are marked with 'MATCHES' below.

View Hierarchy:
+>DecorView{id=-1, visibility=VISIBLE, width=768, height=1280, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params={(0,0)(fillxfill) sim={adjust=nothing} ty=BASE_APPLICATION wanim=0x10302f8
fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SPLIT_TOUCH HARDWARE_ACCELERATED TRANSLUCENT_STATUS DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=FORCE_DRAW_STATUS_BAR_BACKGROUND}, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=3}
|
+->LinearLayout{id=-1, visibility=VISIBLE, width=768, height=1184, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@1197392, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+-->ViewStub{id=16908682, res-name=action_mode_bar_stub, visibility=GONE, width=0, height=0, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=true, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@df8e163, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0}
|
+-->FrameLayout{id=-1, visibility=VISIBLE, width=768, height=1184, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@d365160, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=1}
|
+--->FitWindowsLinearLayout{id=2131230778, res-name=action_bar_root, visibility=VISIBLE, width=768, height=1184, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@ad736de, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+---->ViewStubCompat{id=2131230810, res-name=action_mode_bar_stub, visibility=GONE, width=0, height=0, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=true, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@e0e30bf, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0}
|
+---->ContentFrameLayout{id=16908290, res-name=content, visibility=VISIBLE, width=768, height=1184, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@19f238c, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=1}
|
+----->FrameLayout{id=-1, visibility=VISIBLE, width=768, height=1184, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@1be2ea, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=5}
|
+------>FrameLayout{id=2131231243, res-name=nav_host_fragment, visibility=VISIBLE, width=768, height=1184, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@5ff19db, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=1}
|
+------->FrameLayout{id=2131230978, res-name=contentContainer, visibility=VISIBLE, width=768, height=1184, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@eb55bb3, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=4}
|
+-------->FrameLayout{id=2131230977, res-name=content, visibility=VISIBLE, width=768, height=1024, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@49ac570, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=160.0, child-count=2}
|
+--------->RecyclerView{id=2131231337, res-name=recyclerView, desc=catalog-items, visibility=VISIBLE, width=768, height=1024, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@2ae60e9, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=5}
|
+---------->View{id=-1, visibility=VISIBLE, width=768, height=96, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=androidx.recyclerview.widget.GridLayoutManager$LayoutParams@2fc716e, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0}
|
+---------->LinearLayout{id=-1, desc=catalog-item, visibility=VISIBLE, width=383, height=804, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=androidx.recyclerview.widget.GridLayoutManager$LayoutParams@920000f, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=96.0, child-count=1}
|
+----------->FrameLayout{id=2131230956, res-name=clickableView, visibility=VISIBLE, width=381, height=802, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@d3ec89c, tag=null, root-is-layout-requested=false, has-input-connection=false, x=1.0, y=1.0, child-count=1} MATCHES
|
+------------>LinearLayout{id=-1, visibility=VISIBLE, width=381, height=802, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@98322a5, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+------------->FrameLayout{id=2131231157, res-name=imageContainer, visibility=VISIBLE, width=381, height=602, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@202367a, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=3}
|
+-------------->AppCompatImageView{id=2131231156, res-name=image, visibility=VISIBLE, width=383, height=602, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@3e68e2b, tag=com.bumptech.glide.request.ThumbnailRequestCoordinator@56a33ac, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0}
|
+-------------->AppCompatTextView{id=2131231310, res-name=priceOff, visibility=VISIBLE, width=83, height=56, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@8bdd288, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=New, input-type=0, ime-target=false, has-links=false}
|
+-------------->AppCompatImageView{id=2131231065, res-name=favorite, visibility=VISIBLE, width=80, height=80, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@c284421, tag=null, root-is-layout-requested=false, has-input-connection=false, x=301.0, y=0.0}
|
+------------->LinearLayout{id=-1, visibility=VISIBLE, width=381, height=202, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@46d4046, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=602.0, child-count=3}
|
+-------------->AppCompatTextView{id=2131230884, res-name=brand, visibility=VISIBLE, width=317, height=38, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@83de207, tag=null, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=16.0, text=ALISIA HIT, input-type=0, ime-target=false, has-links=false}
|
+-------------->AppCompatTextView{id=2131231494, res-name=title, visibility=VISIBLE, width=317, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@5a18f34, tag=null, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=60.0, text=Платье, input-type=0, ime-target=false, has-links=false}
|
+-------------->LinearLayout{id=-1, visibility=VISIBLE, width=317, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@5a4015d, tag=null, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=104.0, child-count=2}
|
+--------------->AppCompatTextView{id=2131231306, res-name=price, visibility=VISIBLE, width=113, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@a2d1ad2, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=8 750 ₽, input-type=0, ime-target=false, has-links=false}
|
+--------------->AppCompatTextView{id=2131231309, res-name=priceNew, visibility=VISIBLE, width=120, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@3cc97a3, tag=null, root-is-layout-requested=false, has-input-connection=false, x=121.0, y=0.0, text=7 000 ₽, input-type=0, ime-target=false, has-links=false}
|
+---------->LinearLayout{id=-1, desc=catalog-item, visibility=VISIBLE, width=383, height=804, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=androidx.recyclerview.widget.GridLayoutManager$LayoutParams@f626aa0, tag=null, root-is-layout-requested=false, has-input-connection=false, x=384.0, y=96.0, child-count=1}
|
+----------->FrameLayout{id=2131230956, res-name=clickableView, visibility=VISIBLE, width=381, height=802, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@1245659, tag=null, root-is-layout-requested=false, has-input-connection=false, x=1.0, y=1.0, child-count=1}
|
+------------>LinearLayout{id=-1, visibility=VISIBLE, width=381, height=802, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@9e121e, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+------------->FrameLayout{id=2131231157, res-name=imageContainer, visibility=VISIBLE, width=381, height=602, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@e070aff, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=3}
|
+-------------->AppCompatImageView{id=2131231156, res-name=image, visibility=VISIBLE, width=383, height=602, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@e9390cc, tag=com.bumptech.glide.request.ThumbnailRequestCoordinator@a58c00a, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0}
|
+-------------->AppCompatTextView{id=2131231310, res-name=priceOff, visibility=VISIBLE, width=83, height=56, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@4baff15, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=New, input-type=0, ime-target=false, has-links=false}
|
+-------------->AppCompatImageView{id=2131231065, res-name=favorite, visibility=VISIBLE, width=80, height=80, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@c95322a, tag=null, root-is-layout-requested=false, has-input-connection=false, x=301.0, y=0.0}
|
+------------->LinearLayout{id=-1, visibility=VISIBLE, width=381, height=202, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@74b581b, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=602.0, child-count=3}
|
+-------------->AppCompatTextView{id=2131230884, res-name=brand, visibility=VISIBLE, width=317, height=38, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@fceedb8, tag=null, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=16.0, text=ALISIA HIT, input-type=0, ime-target=false, has-links=false}
|
+-------------->AppCompatTextView{id=2131231494, res-name=title, visibility=VISIBLE, width=317, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@bd97791, tag=null, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=60.0, text=Платье, input-type=0, ime-target=false, has-links=false}
|
+-------------->LinearLayout{id=-1, visibility=VISIBLE, width=317, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@92c46f6, tag=null, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=104.0, child-count=2}
|
+--------------->AppCompatTextView{id=2131231306, res-name=price, visibility=VISIBLE, width=119, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@dbd5af7, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=11 250 ₽, input-type=0, ime-target=false, has-links=false}
|
+--------------->AppCompatTextView{id=2131231309, res-name=priceNew, visibility=VISIBLE, width=123, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@d612d64, tag=null, root-is-layout-requested=false, has-input-connection=false, x=127.0, y=0.0, text=9 000 ₽, input-type=0, ime-target=false, has-links=false}
|
+---------->LinearLayout{id=-1, desc=catalog-item, visibility=VISIBLE, width=383, height=804, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=androidx.recyclerview.widget.GridLayoutManager$LayoutParams@d8cfbcd, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=900.0, child-count=1}
|
+----------->FrameLayout{id=2131230956, res-name=clickableView, visibility=VISIBLE, width=381, height=802, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@74ddc82, tag=null, root-is-layout-requested=false, has-input-connection=false, x=1.0, y=1.0, child-count=1}
|
+------------>LinearLayout{id=-1, visibility=VISIBLE, width=381, height=802, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@3e2af93, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+------------->FrameLayout{id=2131231157, res-name=imageContainer, visibility=VISIBLE, width=381, height=602, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@7b5bbd0, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=3}
|
+-------------->AppCompatImageView{id=2131231156, res-name=image, visibility=VISIBLE, width=383, height=602, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@a7a87c9, tag=com.bumptech.glide.request.ThumbnailRequestCoordinator@7a0f80c, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0}
|
+-------------->AppCompatTextView{id=2131231310, res-name=priceOff, visibility=VISIBLE, width=96, height=56, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@a013ece, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=-30%, input-type=0, ime-target=false, has-links=false}
|
+-------------->AppCompatImageView{id=2131231065, res-name=favorite, visibility=VISIBLE, width=80, height=80, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@4feb1ef, tag=null, root-is-layout-requested=false, has-input-connection=false, x=301.0, y=0.0}
|
+------------->LinearLayout{id=-1, visibility=VISIBLE, width=381, height=202, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@882c4fc, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=602.0, child-count=3}
|
+-------------->AppCompatTextView{id=2131230884, res-name=brand, visibility=VISIBLE, width=317, height=38, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@e9ad785, tag=null, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=16.0, text=EA7, input-type=0, ime-target=false, has-links=false}
|
+-------------->AppCompatTextView{id=2131231494, res-name=title, visibility=VISIBLE, width=317, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@b7679da, tag=null, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=60.0, text=Пуховик, input-type=0, ime-target=false, has-links=false}
|
+-------------->LinearLayout{id=-1, visibility=VISIBLE, width=317, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@32e7e0b, tag=null, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=104.0, child-count=2}
|
+--------------->AppCompatTextView{id=2131231306, res-name=price, visibility=VISIBLE, width=135, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@cb534e8, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=29 990 ₽, input-type=0, ime-target=false, has-links=false}
|
+--------------->AppCompatTextView{id=2131231309, res-name=priceNew, visibility=VISIBLE, width=132, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@2b66701, tag=null, root-is-layout-requested=false, has-input-connection=false, x=143.0, y=0.0, text=20 993 ₽, input-type=0, ime-target=false, has-links=false}
|
+---------->LinearLayout{id=-1, desc=catalog-item, visibility=VISIBLE, width=383, height=804, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=androidx.recyclerview.widget.GridLayoutManager$LayoutParams@6d259a6, tag=null, root-is-layout-requested=false, has-input-connection=false, x=384.0, y=900.0, child-count=1}
|
+----------->FrameLayout{id=2131230956, res-name=clickableView, visibility=VISIBLE, width=381, height=802, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@b44efe7, tag=null, root-is-layout-requested=false, has-input-connection=false, x=1.0, y=1.0, child-count=1}
|
+------------>LinearLayout{id=-1, visibility=VISIBLE, width=381, height=802, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@11cb794, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+------------->FrameLayout{id=2131231157, res-name=imageContainer, visibility=VISIBLE, width=381, height=602, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@9a1723d, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=3}
|
+-------------->AppCompatImageView{id=2131231156, res-name=image, visibility=VISIBLE, width=383, height=602, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@2ba6a32, tag=com.bumptech.glide.request.ThumbnailRequestCoordinator@9b10b6a, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0}
|
+-------------->AppCompatTextView{id=2131231310, res-name=priceOff, visibility=VISIBLE, width=96, height=56, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@a66a383, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=-30%, input-type=0, ime-target=false, has-links=false}
|
+-------------->AppCompatImageView{id=2131231065, res-name=favorite, visibility=VISIBLE, width=80, height=80, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@dd7b900, tag=null, root-is-layout-requested=false, has-input-connection=false, x=301.0, y=0.0}
|
+------------->LinearLayout{id=-1, visibility=VISIBLE, width=381, height=202, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@537f539, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=602.0, child-count=3}
|
+-------------->AppCompatTextView{id=2131230884, res-name=brand, visibility=VISIBLE, width=317, height=38, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@8a0f77e, tag=null, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=16.0, text=EA7, input-type=0, ime-target=false, has-links=false}
|
+-------------->AppCompatTextView{id=2131231494, res-name=title, visibility=VISIBLE, width=317, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@165f4df, tag=null, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=60.0, text=Пуховик, input-type=0, ime-target=false, has-links=false}
|
+-------------->LinearLayout{id=-1, visibility=VISIBLE, width=317, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@c7f652c, tag=null, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=104.0, child-count=2}
|
+--------------->AppCompatTextView{id=2131231306, res-name=price, visibility=VISIBLE, width=133, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@f19abf5, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=23 990 ₽, input-type=0, ime-target=false, has-links=false}
|
+--------------->AppCompatTextView{id=2131231309, res-name=priceNew, visibility=VISIBLE, width=120, height=40, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@4d10d8a, tag=null, root-is-layout-requested=false, has-input-connection=false, x=141.0, y=0.0, text=16 793 ₽, input-type=0, ime-target=false, has-links=false}
|
+--------->FrameLayout{id=-1, visibility=VISIBLE, width=768, height=912, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@1defffb, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=1}
|
+---------->FrameLayout{id=-1, visibility=VISIBLE, width=768, height=912, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@513a818, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=1}
|
+----------->ProgressBar{id=2131231184, res-name=loading, visibility=INVISIBLE, width=80, height=80, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@1261271, tag=null, root-is-layout-requested=false, has-input-connection=false, x=344.0, y=416.0}
|
+-------->LinearLayout{id=2131231141, res-name=handler_holder, visibility=VISIBLE, width=768, height=100, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@113a0d7, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=160.0, child-count=2}
|
+--------->FrameLayout{id=2131231351, res-name=rubricator_recycler_holder, visibility=GONE, width=0, height=0, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=true, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@9b864ad, tag=null, root-i

Support for AndroidX Espresso

Steps to reproduce:

  1. Replace support lib Espresso dependency with AndroidX one
'com.android.support.test.espresso:espresso-core:3.0.2'
to
'androidx.test.espresso:espresso-core:3.1.0-alpha4'
  1. Write test which with click() assertion. e.g. screen.button.click()

Observed Results:

Compiler error:
Cannot access class 'android.support.test.espresso.action.GeneralLocation'. Check your module classpath for missing or conflicting dependencies

Expected Results:

Test code should compile

AndroidX library will be co-existing with supportLib for some time. Maybe would be possible to have separate artifact for AndroidX?

Cannot scroll to view in NestedScrollView

Steps to reproduce:

Define layout with NestedScrollView

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView>
    <LinearLayout>
        .
        .
        .
        <Button>
    </LinearLayout>
</android.support.v4.widget.NestedScrollView>

And scroll to view by scrollTo()

screen {
  snackbarButton {
      scrollTo()
      click()
  }

  snackbar {
      isDisplayed()
  }
}

Observed Results:

PerformException: Error performing 'scroll to' on view

Add ViewMatcher withContentDescription with resource identifier instead of String

Currently it's only possible to match using a String content description. I think

Steps to reproduce:

  1. Try to match a view based on it's content description id

Observed Results:

  • Compile error: Pass String instead of int

Expected Results:

  • Would like to be able to match on id, so that localisation doesn't give problems

Relevant Code:

val navigationIcon = KView {
      withContentDescription { R.string.cd_hamburger_icon }
  }

Add KSpinner view component and related functions

Kakao is an awesome library for reducing our test code. I'm using Kakao for testing my project but there's no spinner component to test spinner.
Instead of using Kakao KView, I just can use original onView or onData to implement test code.

I have a few functions as the following below

fun selectItem(position: Int)
fun selectItem(content: String)
fun withText(content: String)
fun withText(stringId: Int)

If there is any function should be added, please let me know.

Question: same class to be used with or without RecyclerView

In my project I have a collection of reusable custom views and I want to prepare a library of Kakao views for them. Each of the custom views can be used within any ViewGroup, or within RecyclerView. For now for each of the views I prepare two separate classes for that two use cases:

// To be used in regular cases, e.g. ViewGroups
class KMyView(function: ViewBuilder.() -> Unit) : KBaseView<KMyView>(function) {
    val child: KTextView = KTextView { 
        isDescendantOfA(function)
        withId(R.id.child)
    }
}

// To be used with KRecyclerView
class RMyView(parent: Matcher<View>) : KRecyclerItem<RMyView>(parent) {
    val child = KTextView(parent) { withId(R.id.child) }
}

Is there a way to have one class only, instead of two?

KTextInputLayout depends from TextInputEditText

Hi, we got a problem :)

In our layout files we have something like

    <com.google.android.material.textfield.TextInputLayout>
        <androidx.appcompat.widget.AppCompatEditText />
    </com.google.android.material.textfield.TextInputLayout>

In test

object TestScreen : Screen<TestScreen>() {
    val textInputLayout = KTextInputLayout { withId(R.id.textInputLayout) }
}

TestScreen {
    textInputLayout{ 
        edit { typeText("some text") }
    } 
}

Expected Results:

Test passed

Actual Results:

androidx.test.espresso.NoMatchingViewException: No views in hierarchy found matching: (is descendant of a: (with id: com.test.test:id/textInputLayout) and an instance of com.google.android.material.textfield.TextInputEditText)


It's because KTextInputLayout has check

isInstanceOf(TextInputEditText::class.java)

Do you have any reasons to check TextInputEditText ?
I think it's possible to replace TextInputEditText to AppCompatEditText or EditText
TextInputEditText extends AppCompatEditText

Add Matcher withTag(tagValue)

Hello, thanks for your library.
Can you add function for find views by it tag, for better testing-developer experince :))

For now, if you want find view by tag you need write code like this:

val tagValue: Any = "tag"
val view = KView { withMatcher(ViewMatchers.withTagValue(Matchers.`is`(tagValue))) }  

It would be great to reduce this code to:

val tagValue: Any = "tag"
val view = KView { withTag(tagValue) }  

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.