Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Matching in RecyclerView not working with different ViewTypes #547

Closed
cee-dee opened this issue Jul 25, 2023 · 7 comments
Closed

Matching in RecyclerView not working with different ViewTypes #547

cee-dee opened this issue Jul 25, 2023 · 7 comments
Assignees
Labels
bug Something isn't working
Projects

Comments

@cee-dee
Copy link

cee-dee commented Jul 25, 2023

I'm having issues operating a RecyclerView with different ViewTypes in my UI tests, although I'm using the latest Kaspresso 1.5.2:

The RecyclerView is used for an Auto Completer and has two ViewTypes:

  1. regular results (auto_completer_item_root)
  2. special item for vicinity search (gps_search_item_root)

What I want to achieve: click on the first item in the RecyclerView which has a View matching my "regular" items. Caveat: the first item in the RecyclerView does not have this type but the above mentioned "special item" type.

The view xmls look like:

<androidx.constraintlayout.widget.ConstraintLayout 
    ...
    android:id="@+id/auto_completer_item_root" ...>

    <TextView
        android:id="@+id/destination_title"
        ...
    />
    ...
</androidx.constraintlayout.widget.ConstraintLayout>

and

<androidx.constraintlayout.widget.ConstraintLayout 
    ...
    android:id="@+id/gps_search_item_root" ...>

    <TextView
        android:id="@+id/gps_title"
        ...
    />
    ...
</androidx.constraintlayout.widget.ConstraintLayout>

I've got a screen object for the RecyclerView:

class AutoCompleterScreen : KScreen<AutoCompleterScreen>() {

    override val layoutId: Int? = null
    override val viewClass: Class<*>? = null

    val resultList = KRecyclerView(
        builder = { withId(R.id.destinationList) },
        itemTypeBuilder = {
            itemType(::GpsSearch)
            itemType(::AutoCompleterResult)
        }
    )

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

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

I tried different approaches in my test, I was hoping that this will work:

onScreen<AutoCompleterScreen> {
    resultList {
        firstChild<AutoCompleterScreen.AutoCompleterResult> {
            title {
                click()
            }
        }
    }
}

But it wasn't.

I kept on experimenting and this one worked once, but I couldn't repeat it, so it might be related to the app not being freshly installed:

onScreen<AutoCompleterScreen> {
    resultList {
        childWith<AutoCompleterScreen.AutoCompleterResult> {
            withId(R.id.auto_completer_item_root)
            isFirst()
        } perform {
            click()
        }
    }
}

On the other hand, I managed to come up with some Espresso code that actually works -- but as I'm using Kaspresso, I'd like to learn how to leverage that framework:

onView(withId(R.id.destinationList))
  .perform(actionOnFirstItemWithId(R.id.auto_completer_item_root, click()))

using

fun actionOnFirstItemWithId(@IdRes viewId: Int, action: ViewAction): ViewAction {

    return object : ViewAction {
        override fun getConstraints(): Matcher<View> {
            return Matchers.allOf(ViewMatchers.isAssignableFrom(RecyclerView::class.java), ViewMatchers.isDisplayed())
        }

        override fun getDescription(): String {
            return "Perform action on the first item that has a view with the given ID."
        }

        override fun perform(uiController: UiController, view: View) {
            val recyclerView = view as RecyclerView
            for (i in 0 until recyclerView.adapter!!.itemCount) {
                uiController.loopMainThreadForAtLeast(500)
                val viewHolder = recyclerView.findViewHolderForAdapterPosition(i)
                if (viewHolder?.itemView?.findViewById<View>(viewId) != null) {
                    if (i != 0) {
                        recyclerView.scrollToPosition(i)
                        uiController.loopMainThreadForAtLeast(500)
                    }
                    val targetView = viewHolder.itemView.findViewById<View>(viewId)
                    action.perform(uiController, targetView)
                    return
                }
            }
        }
    }
}

Maybe, Kakao/Kaspresso has problems with the looping of the MainThread, but it should support going without that because of its internal flakysafe implementations.

@cee-dee cee-dee added the bug Something isn't working label Jul 25, 2023
@Nikitae57 Nikitae57 self-assigned this Aug 10, 2023
@Nikitae57 Nikitae57 added this to In progress in Kaspresso Aug 10, 2023
@Nikitae57
Copy link
Collaborator

Hi, @cee-dee! firstChild method returns literally the first child in the list. The child type you provide to this method serves only for casting item to exact type you expect. It's not used as a matcher. Method isFirst is a check that serves as an assertion. Again, it's not used as a matcher. I suggest you to use childWith method. It would look someting like this:

onScreen<AutoCompleterScreen> {
    resultList {
        childWith<AutoCompleterScreen.AutoCompleterResult> { withDescendant { withId(R.id.destination_title) } } perform {
    // your check here
        }
    }
}

Please, check kakao samples if you need more info

@Nikitae57 Nikitae57 moved this from In progress to Done in Kaspresso Aug 14, 2023
@cee-dee
Copy link
Author

cee-dee commented Aug 14, 2023

Thank you for the suggestion! I've tried this and it gives me

androidx.test.espresso.AmbiguousViewMatcherException: 'view holder: ((view is an instance of android.view.ViewGroup and has descendant matching (view.getId() is <2131428706/de.android.homes.alpha:id/destination_title>)))of recycler view: (view.getId() is <2131428702/de.android.homes.alpha:id/destinationList>)' matches 5 views in the hierarchy:
...

Actually, I need to match the first of these views on tap on it, like I implemented the Espresso matcher doing that. That was the reason why I tried also using first. Is there a way to achieve this combination?

@cee-dee
Copy link
Author

cee-dee commented Aug 15, 2023

I found the solution. So, it's possible using Kaspresso/Kakao ootb without usingEspresso code.

Actually, the code

onScreen<AutoCompleterScreen> {
    resultList {
        childWith<AutoCompleterScreen.AutoCompleterResult> {
            withId(R.id.auto_completer_item_root)
            isFirst()
        } perform {
            click()
        }
    }
}

had two flaws:

a) auto_completer_item_root seems to not be part of the RecyclerView item, I need to exchange it against destination_title. In the end, I could even remove the withId check since the Kakao did that check using childWith.

b) In my scenario, the RecyclerView was pre-populated with data from a previous search, meaning that already items matching the above criteria were in the RecyclerView, but clicking on them wasn't intended until the RecyclerView is updated with the currently running search (which is issued earlier in the test). Thus I needed to add a check that ensures the search results are for the current keyword.

This is the working code:

onScreen<AutoCompleterScreen> {
    resultList {
        childWith<AutoCompleterScreen.AutoCompleterResult> {
            containsText("current keyword")
            isFirst()
        } perform {
            click()
        }
    }
}

@Pirokar
Copy link

Pirokar commented Sep 26, 2023

@cee-dee do you know, that your childWith works wrong? :) For example, I have this recyclerView:

image

and this check:

recyclerView {
         childWith<ChatMainScreen.ChatRecyclerItem> {
           containsText("Добро пожаловать в наш чат")
           isFirst()
     }
 }

And test is passed! But it's not the first, it's index is "3" in the list! Also we can check it, for example, for visibility, and it will be visible and invisible in the same time :)

@cee-dee
Copy link
Author

cee-dee commented Sep 26, 2023

No, it doesn't work wrong. That is exactly the behaviour I intended: first item in the Recycler view that contains the text.

@Pirokar
Copy link

Pirokar commented Sep 27, 2023

For my tests it's not true. I described it above

@cee-dee
Copy link
Author

cee-dee commented Sep 27, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
Development

No branches or pull requests

3 participants