Nice and simple DSL for Espresso in Kotlin

Overview

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

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

Comments
  • ListPopupWindow and Toast intergation

    ListPopupWindow and Toast intergation

    Hi guys!

    First of all - thanks for your work! Can you take a look on KToast and KListPopupWindow and give me some hints (maybe it's just an overkill and it could be done with KView and KListView?

    https://gist.github.com/Chesteer89/767ad1b1584e2920a9758bd1dfe95b36 https://gist.github.com/Chesteer89/d6bfe7383c287935ce938e9dfa98c0a1

    opened by Chesteer89 32
  • View interaction wrapper

    View interaction wrapper

    Introduce ViewInteractionWrapper and ViewInteractionWrapperFactory to be able to proxy and intercept calls to Espresso ViewInteraction made by Kakao.

    Possible use cases:

    • Logging
    • Implementing retry wrappers
    opened by dkostyrev 16
  • hasDrawable doesn't work when ImageView has android:tint

    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.

    🐞 Bug 
    opened by tdekoning 14
  • feat: Add KSpinner (issue #118)

    feat: Add KSpinner (issue #118)

    This pull request adds Kspinner as discussed in issue #118 and pull request #119. @Unlimity do you mind reviewing the code. I still want to add more assertions such as isOpened(), isClosed()

    🦄 Enhancement 🔜 Ready to merge 
    opened by michaelbukachi 12
  • ActionBar - Support homeAsUpIndicator

    ActionBar - Support homeAsUpIndicator

    Hi, I've been using Kakao for a few weeks now and I noticed that recently you added ActionBar support (#188). However, I saw that currently there's no support to match the up indicator drawable.

    At my company, in our current UI tests we're basically declaring two different KViews as a workaround, a KToolbar and a KImageView for the up indicator button:

     private val toolbar = KToolbar { withId(R.id.toolbar) }
     private val toolbarButton = KImageView { withContentDescription("Navigate up") }
    

    and then perform the checks with

    // ...
    toolbar {
        hasTitle(R.string.some_string)
    }
    toolbarButton {
        hasDrawable(R.drawable.some_drawable)
    }
    

    However I'd like to know if it's possible to have an implementation of KToolbar that allows what we're trying to achieve.

    I forked the repo and tried to play around a bit (please ignore #215 as I was comparing and accidentally opened the PR) and everything seems to be working fine, the changes I had in mind were:

    • Allow providing an additional - nullable - builder in the KToolbar constructor that helps locate the homeAsUpIndicator via its contentDescription (by default it should be either "Navigate up"/"Navigate home", customizable via setHomeActionContentDescription) - and define it as KImageView already (?)
    KToolbar = KToolbar(builder = { withId(R.id.action_bar) }, homeAsUpIndicatorBuilder = { withContentDescription("Navigate up")})
    
    
    • That way we can invoke hasDrawable on it by passing the expected drawable of the indicator
    // This is from the TestActivityTest file changed
    toolbar {
        hasTitle("Test Activity")
        hasSubtitle("Subtitle")
        hasHomeAsUpIndicatorDrawable(R.drawable.ic_arrow_back_24)
    }
    

    In case if hasNavigationIconDrawable is invoked but there's no view builder provided, it throws an AssertionError.

    For just checking if the homeAsUpIndicator is present there could be another method (e.g. hasHomeAsUpIndicator) as well to allow checking if it's just visible when a custom icon is not set.

    Note that I haven't been able to find the "default" back indicator drawable ID for matching it so I was planning to add a drawable to the sample project and set it programmatically.

    I was also trying to see if the icon could be checked as well (the one set via setIcon) but I couldn't find anything that allowed to match it properly.

    What do you think about it? I'd like to discuss this with you and see if it can be implemented, in that case I'll rework the branch I have on my fork with an actual decent commit history and submit a (new) PR.

    Thanks!

    EDIT: I did some changes and pushed a new branch, it's here if you want to take a look.

    🦄 Enhancement 💻 In progress 
    opened by nick0602 10
  • RecyclerView AmbiguousViewMatcherException with ONE SINGLE MATCH 0_o

    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

    📌 Question 
    opened by iamthevoid 10
  • Cannot click items in nested RecyclerViews

    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?

    📌 Question 
    opened by Cotel 10
  • Support GridLayout

    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.

    opened by lucas34 9
  • Create AutoCompleteTextView sample

    Create AutoCompleteTextView sample

    With reference to issue #42 this provides a sample demo for the AutoCompleteTextView usage with a DecorPopupView which is currently unsuccessful.

    Stack trace indicates that the platform popup view is not present in the view hierarchy, or that assertions are being made against the wrong window.

    android.support.test.espresso.NoMatchingViewException: No views in hierarchy found matching: (an instance of android.widget.ListView)
    
    View Hierarchy:
    +>DecorView{id=-1, visibility=VISIBLE, width=1080, height=1920, has-focus=true, 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) ty=BASE_APPLICATION wanim=0x10302f8
    fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SPLIT_TOUCH HARDWARE_ACCELERATED 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=1080, height=1813, has-focus=true, 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@db771, 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@621b956, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0}
    |
    +-->FrameLayout{id=-1, visibility=VISIBLE, width=1080, height=1760, has-focus=true, 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@59ad6c4, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=53.0, child-count=1}
    |
    +--->ActionBarOverlayLayout{id=2131230774, res-name=decor_content_parent, visibility=VISIBLE, width=1080, height=1760, has-focus=true, 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@ff559ad, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
    |
    +---->ContentFrameLayout{id=16908290, res-name=content, visibility=VISIBLE, width=1080, height=1635, has-focus=true, 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.support.v7.widget.ActionBarOverlayLayout$LayoutParams@e9f073, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=125.0, child-count=1}
    |
    +----->AppCompatAutoCompleteTextView{id=2131230753, res-name=auto_complete_view, visibility=VISIBLE, width=1080, height=100, has-focus=true, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=true, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@1269d2e, tag=null, root-is-layout-requested=false, has-input-connection=true, editor-info=[inputType=0x30001 imeOptions=0x40000006 privateImeOptions=null actionLabel=null actionId=0 initialSelStart=5 initialSelEnd=5 initialCapsMode=0x0 hintText=null label=null packageName=null fieldId=0 fieldName=null extras=null hintLocales=null contentMimeTypes=null ], x=0.0, y=0.0, text=Title, input-type=196609, ime-target=true, has-links=false}
    |
    +---->ActionBarContainer{id=2131230729, res-name=action_bar_container, visibility=VISIBLE, width=1080, height=125, 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.support.v7.widget.ActionBarOverlayLayout$LayoutParams@2351165, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
    |
    +----->Toolbar{id=2131230727, res-name=action_bar, visibility=VISIBLE, width=1080, height=125, 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@5035e3a, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
    |
    +------>AppCompatTextView{id=-1, visibility=VISIBLE, width=447, height=61, 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.support.v7.widget.Toolbar$LayoutParams@400faeb, tag=null, root-is-layout-requested=false, has-input-connection=false, x=35.0, y=32.0, text=Autocomplete Activity, input-type=0, ime-target=false, has-links=false}
    |
    +------>ActionMenuView{id=-1, visibility=VISIBLE, width=0, height=125, 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.support.v7.widget.Toolbar$LayoutParams@da0a048, tag=null, root-is-layout-requested=false, has-input-connection=false, x=1080.0, y=0.0, child-count=0}
    |
    +----->ActionBarContextView{id=2131230735, res-name=action_context_bar, 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.FrameLayout$LayoutParams@dea5ee1, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=0}
    |
    +->View{id=16908336, res-name=navigationBarBackground, visibility=VISIBLE, width=1080, height=107, 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@8b92406, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=1813.0}
    |
    +->View{id=16908335, res-name=statusBarBackground, visibility=VISIBLE, width=1080, height=53, 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@2725ac7, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0}
    |
    at dalvik.system.VMStack.getThreadStackTrace(Native Method)
    at java.lang.Thread.getStackTrace(Thread.java:1538)
    at android.support.test.espresso.base.DefaultFailureHandler.getUserFriendlyError(DefaultFailureHandler.java:90)
    at android.support.test.espresso.base.DefaultFailureHandler.handle(DefaultFailureHandler.java:52)
    at android.support.test.espresso.ViewInteraction.waitForAndHandleInteractionResults(ViewInteraction.java:312)
    at android.support.test.espresso.ViewInteraction.desugaredPerform(ViewInteraction.java:167)
    at android.support.test.espresso.ViewInteraction.perform(ViewInteraction.java:110)
    at android.support.test.espresso.DataInteraction$DisplayDataMatcher$1.apply(DataInteraction.java:206)
    at android.support.test.espresso.DataInteraction$DisplayDataMatcher$1.apply(DataInteraction.java:203)
    at android.support.test.espresso.DataInteraction$DisplayDataMatcher.<init>(DataInteraction.java:223)
    at android.support.test.espresso.DataInteraction$DisplayDataMatcher.<init>(DataInteraction.java:198)
    at android.support.test.espresso.DataInteraction$DisplayDataMatcher.displayDataMatcher(DataInteraction.java:241)
    at android.support.test.espresso.DataInteraction.makeTargetMatcher(DataInteraction.java:143)
    at android.support.test.espresso.DataInteraction.check(DataInteraction.java:137)
    at com.agoda.kakao.KAdapterItem.<init>(Views.kt:830)
    at com.agoda.sample.screen.AutoCompleteActivityScreen$Item.<init>(AutoCompleteActivityScreen.kt:22)
    at com.agoda.sample.screen.AutoCompleteActivityScreen$list$2$1.invoke(AutoCompleteActivityScreen.kt:20)
    at com.agoda.sample.screen.AutoCompleteActivityScreen$list$2$1.invoke(AutoCompleteActivityScreen.kt:12)
    at com.agoda.sample.AutoCompleteTest$testContentItemsListView$1$2.invoke(AutoCompleteTest.kt:81)
    at com.agoda.sample.AutoCompleteTest$testContentItemsListView$1$2.invoke(AutoCompleteTest.kt:11)
    at com.agoda.kakao.KListView.invoke(Views.kt:485)
    at com.agoda.sample.AutoCompleteTest$testContentItemsListView$1.invoke(AutoCompleteTest.kt:26)
    at com.agoda.sample.AutoCompleteTest$testContentItemsListView$1.invoke(AutoCompleteTest.kt:11)
    at com.agoda.kakao.Screen.invoke(Screen.kt:21)
    at com.agoda.sample.AutoCompleteTest.testContentItemsListView(AutoCompleteTest.kt:20)
    at java.lang.reflect.Method.invoke(Native Method)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at android.support.test.rule.ActivityTestRule$ActivityStatement.evaluate(ActivityTestRule.java:433)
    at org.junit.rules.RunRules.evaluate(RunRules.java:20)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runners.Suite.runChild(Suite.java:128)
    at org.junit.runners.Suite.runChild(Suite.java:27)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
    at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:58)
    at android.support.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:375)
    at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2145)
    

    It can also be noted that introducing the following to the KListView builder is also unsuccessful.

    withParent {
        isRoot()
    }
    
    opened by ashdavies 9
  • KRecyclerView multiple childs assert not success

    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") }
                  }
              }
          }
    
    opened by alorma 9
  • Migrate away from JCenter

    Migrate away from JCenter

    JFrog has just announced that it will be shutting down JCenter on May 1st. JCenter is the sole repository hosting this library, so it will become unavailable after this date unless migrated to an alternative service.

    bug 
    opened by steffandroid 8
Releases(2.3.4)
  • 2.3.4(Jul 29, 2020)

  • 2.3.3(Jun 18, 2020)

  • 2.3.2(May 10, 2020)

  • 2.3.1(Apr 28, 2020)

  • 2.3.0(Feb 25, 2020)

    • Kotlin updated to 1.3.61
    • appcompat artifact from AndroidX updated to 1.1.0
    • recyclerview artifact from AndroidX updated to 1.1.0
    • swiperefreshlayout artifact from AndroidX has been added at version 1.0.0
    • ViewPager2 support added with KViewPager2 class and viewpager2 artifact from AndroidX at version 1.0.0
    • Material design artifact updated to 1.1.0
    • IndexMatcher (withIndex()) has become reusable
    • ChipGroup support added with KChipGroup class
    • Spinner support added with KSpinner class
    • Tint support added to KImageView, though it does not work 100% of the time

    Huge thanks to @michaelbukachi and @yapkolotilov for contributing into this release.

    Source code(tar.gz)
    Source code(zip)
  • 2.2.0(Oct 10, 2019)

  • 2.1.0(Jul 10, 2019)

    New features/components:

    • Interceptors
    • KDatePicker, KTimePicker, KDatePickerDialog and KTimePickerDialog
    • Root view inside Screens to auto check if the screen is actually displayed (optional)
    • KScrollView has been added
    • Position assertions has been added to BaseAssertions

    Changes:

    • hasHint assertion has moved to TextViewAssertions
    • KListView has been renamed to KAbsListView and support of AbsListView has been added

    Fixes:

    • Snackbar compatibility fixed for AndroidX
    • DrawableMatcher has been improved

    Please note that 2.1.0-support release includes only Interceptors feature

    Source code(tar.gz)
    Source code(zip)
  • 2.0.0(Feb 25, 2019)

    At last, we are publishing 2.0.0 after a long waiting period. What has been done:

    • Huge library's classes and packages restructure. Now every class/interface/object is stored in different file and is packaged by component. Thanks to @psh for this
    • Master branch has migrated to latest AndroidX artifacts. If you still are using support versions, there' s support artifact for you: 2.0.0-support
    • DslMarker annotations are now fixed and lambda scoping is working as intended
    • idle() function has been moved to Screen's companion object to grant access to it on any layer of lambda nesting
    • onScreen inline function that will help you use your screens without holding an instance of it in your test thus reducing the boilerplate
    • withTag matcher added to builders
    • KRecyclerView and KListView items can now be accessible even if corresponding view is locating in different window (root matchers are now propagated to each item)
    • Kotlin version has been upgraded to 1.3.21 and Android Gradle plugin has been upgraded to 3.3.1

    Thanks to all of the contributors. Your help made this release happen. Cheers!

    Source code(tar.gz)
    Source code(zip)
  • 1.4.0(Jul 17, 2018)

  • 1.3.0(Apr 17, 2018)

    • Kotlin version upgraded to 1.2.31
    • Support library upgraded to 27.1.1
    • Gradle plugin upgraded to 3.1.1
    • minSdk is decreased to 14 (Ice Cream Sandwich)
    • hasTextColor assertion introduced for KTextView
    • hasChildCount assertion introduced for KRecyclerView and KListView
    • Swipe actions are added to KRecyclerView and KListView
    • Nesting is implemented for KRecyclerView and KListView. Now you can declare lists inside lists.
    • Documentation improved on withIndex matcher
    Source code(tar.gz)
    Source code(zip)
  • 1.2.1(Jan 30, 2018)

    • Kotlin version updated to 1.2.21
    • Bug fix for pressImeAction. Not it's moved to BaseActions and cannot be accessed on the Screen level
    • Added withContentDescription for string resources
    • Documentation updated in repository
    Source code(tar.gz)
    Source code(zip)
  • 1.2.0(Dec 19, 2017)

    • Intent extensions added
    • SeekBar support added
    • Compile and target SDK version increased to 27
    • Support libraries updated to 27.0.2
    • Kotlin version increased to 1.2.10
    Source code(tar.gz)
    Source code(zip)
  • 1.1.0(Nov 16, 2017)

    • A lot of new KView classes has been added:
      • KSwipeRefreshLayout
      • KTextInputLayout
      • KTabLayout
      • KSnackbar
      • KNavigationView
      • KProgressBar
      • KBottomNavigationView
    • isRoot matcher added
    • All missing standard actions from Espresso added:
      • pressImeAction
      • pressKey
      • pressMenuKey
      • repeatUntil
      • openUri
    • Android Gradle plugin updated to version 3.0.0
    • Support libraries versions updated to 26.1.0, build tools to 26.0.2
    Source code(tar.gz)
    Source code(zip)
  • 1.0.1(Oct 12, 2017)

    • hasBackgroundColor assertions and withBackgroundColor matcher added
    • isCliclable matcher added
    • KRatingBar, it's actions, assertions and matchers added
    • minor issue fixes
    Source code(tar.gz)
    Source code(zip)
  • 1.0.0(Oct 10, 2017)

    Initial Release with the core functionality of Kakao

    • Added generic KViews
    • Support KListView and KRecyclerView
    • BaseActions / BaseAssertions
    • Provided ViewBuilder for ViewMatching
    • Samples
    Source code(tar.gz)
    Source code(zip)
Owner
Agoda Company Pte Ltd.
Agoda is one of the fastest-growing online hotel platforms worldwide, listing hundreds of thousands of hotels and providing services in 38 different languages.
Agoda Company Pte Ltd.
Here - a nice DeepL API client library written in Kotlin

Here - a nice DeepL API client library written in Kotlin! The supported Java runtimes are any OpenJDK distributions and Android runtime.

Kazuhiro Sera 6 Sep 26, 2022
A nice weather that helps you get all information including: current weather, hourly weather and also forecasts for 16 days

WeatherForecast This is an ongoing project where I fetch all the weather data using Retrofit and Kotlin Coroutines over two APIs containing both curre

null 2 Jul 26, 2022
A simple, classic Kotlin MVI implementation based on coroutines with Android support, clean DSL and easy to understand logic

A simple, classic Kotlin MVI implementation based on coroutines with Android support, clean DSL and easy to understand logic

Nek.12 4 Oct 31, 2022
The most complete and powerful data-binding library and persistence infra for Kotlin 1.3, Android & Splitties Views DSL, JavaFX & TornadoFX, JSON, JDBC & SQLite, SharedPreferences.

Lychee (ex. reactive-properties) Lychee is a library to rule all the data. ToC Approach to declaring data Properties Other data-binding libraries Prop

Mike 112 Dec 9, 2022
Android + Kotlin + Github Actions + ktlint + Detekt + Gradle Kotlin DSL + buildSrc = ❤️

kotlin-android-template ?? A simple Github template that lets you create an Android/Kotlin project and be up and running in a few seconds. This templa

Nicola Corti 1.5k Jan 3, 2023
Koin Annotations - help declare Koin definition in a very fast and intuitive way, and generate all underlying Koin DSL for you

The goal of Koin Annotations project is to help declare Koin definition in a very fast and intuitive way, and generate all underlying Koin DSL for you. The goal is to help developer experience to scale and go fast ?? , thanks to Kotlin Compilers.

insert-koin.io 68 Jan 6, 2023
A Kotlin DSL wrapper around the mikepenz/MaterialDrawer library.

MaterialDrawerKt Create navigation drawers in your Activities and Fragments without having to write any XML, in pure Kotlin code, with access to all t

Márton Braun 517 Nov 19, 2022
{ } Declarative Kotlin DSL for choreographing Android transitions

Transition X Kotlin DSL for choreographing Android Transitions TransitionManager makes it easy to animate simple changes to layout without needing to

Arunkumar 520 Dec 16, 2022
Kotlin Dsl for Android RecyclerView

KRecyclerDsl Kotlin Dsl for Android RecyclerView Exemple Sample project recyclerView.adapter = dataClassAdapter<MyView, MyDataClass>(R.layout.my_view,

Thomas Girard 14 Mar 31, 2019
DSL for constructing the drawables in Kotlin instead of in XML

Android Drawable Kotlin DSL DSL for constructing the drawables in Kotlin instead of in XML Examples Shape drawables <?xml version="1.0" encoding="utf-

Infotech Group 178 Dec 4, 2022
Lightweight Kotlin DSL dependency injection library

Warehouse DSL Warehouse is a lightweight Kotlin DSL dependency injection library this library has an extremely faster learning curve and more human fr

Osama Raddad 18 Jul 17, 2022
Code generation of Kotlin DSL for AWS CDK

Code generation of Kotlin DSL for AWS CDK

Semantic Configuration 5 Dec 24, 2022
Regular expression DSL on Kotlin

Examples Characters Construct Equivalent Matches x character(Char) The character x \\ character('\\') The backslash character \0n octal(OctalValue(7))

null 1 Oct 7, 2021
Kotlin Object Notation - Lightweight DSL to build fluid JSON trees

Kotlin Object Notation Lightweight kotlin MPP DSL for building JSON trees Setup Just drop the dependency in your commonMain sourceSet kotlin { sourc

Martynas Petuška 43 Dec 10, 2022
Kotlin parser library with an easy-to-use DSL

Pratt Library for parsing expressions and a beautiful Kotlin DSL Just define your operators and operands with the Kotlin DSL and the parser is ready!

furetur 9 Oct 17, 2022
Kotlin DSL for Junit5

Kupiter is Kotlin DSL for Junit5. Current API is only for dynamic tests. Get it repositories { maven { url 'https://jitpack.io' } } dependencies

Andrius Semionovas 14 Oct 3, 2022
Kotlin-dsl-sample - Preferences project on android

kotlin-dsl-example Sample preferences project on android. How to use val

null 1 Dec 30, 2021
GitHub Actions Kotlin DSL

GitHub Actions Kotlin DSL Work in progress! The goal is to be able to describe GH Actions in Kotlin with all its perks, like: workflow( name = "Te

Piotr Krzemiński 271 Dec 26, 2022
Kotlin DSL inspired by bhailang.js

Kotlin DSL inspired by bhailang.js

kaiwalya 1 Mar 22, 2022