arkascha
arkascha

Reputation: 42959

AmbiguousViewMatcherException in instrumented espresso tests matching two views due to filter clause not having the intended effect

I struggle with the implementation of an instrumented test based on androidx and espresso.

I am trying to prepare a ViewInteraction in a generic way, since I require the same in multiple actions performed by a robot using the UI. Most of those actions have to interact with a specific view element inside a hierarchy. That view hierarchy is created by a library (a vertical stepper), so I do not really have an influence on that. A number of views in that hierarchy do not have IDs unfortunately, which is why I have to dig around a bit in the hierarchy ...

This is the (shortened) test case:

@Test
fun testFlow_ABC() {
    <some preparations>

    createRobot<ABCServiceRobot>()
        .<some robot actions>
        .checkThatFailMessageIsShownInFirstStep()
        .<some robot actions>

    verify(activityService).<some method>(any())
}

The robot method checkThatFailMessageIsShownInFirstStep() in the ABCServiceRobot class (just the relevant methods):

fun checkThatFailMessageIsShownInFirstStep(): ABCServiceRobot {
    awaitInStepperStep(
        1,
        withId(R.id.step_error_container),
        hasDescendant(
            allOf(
                withText(R.string.abc_failed_reason_unknown),
                isDisplayed(),
            )
        )
    )
    return this
}

private fun awaitInStepperStep(stepNumber: Int, viewMatcher: Matcher<View>, matchToAwait: Matcher<View>) {
    onStepperStep(stepNumber, viewMatcher).perform(waitFor(matchToAwait))
}

private fun onStepperStep(stepNumber: Int, viewMatcher: Matcher<View>): ViewInteraction {
    return onView(
        allOf(
            // a view as specified
            viewMatcher,
            // inside the stepper form
            isDescendantOfA(
                withId(R.id.authorization_service_vertical_stepper_form),
            ),
            // inside the specified stepper step
            isDescendantOfA(
                allOf(
                    // unfortunately this does not have an ID
                    instanceOf(LinearLayout::class.java),
                    // limit to two LinearLayouts inside each stepper step
                    withParent(
                        withId(R.id.steps_scroll)
                    ),
                    // pick the step with the specified number in the header
                    hasDescendant(
                        allOf(
                            withId(R.id.step_header),
                            hasDescendant(
                                allOf(
                                    withId(R.id.step_number),
                                    withText("$stepNumber"),
                                )
                            ),
                        ),
                    ),
                ),
            ),
        )
    )
}

Here is a screenshot of the view structure (sorry, no way to represent as text in a sane manner):

structure of view hierarchy

This is the error message I receive about the two matched view elements:

androidx.test.espresso.AmbiguousViewMatcherException: '(view.getId() is <2131231925/xxx.yyy.zzz:id/step_error_container> and is descendant of a view matching (an instance of android.widget.LinearLayout and (view is an instance of android.view.ViewGroup and has descendant matching (view.getId() is <2131231929/xxx.yyy.zzz:id/step_number> and an instance of android.widget.TextView and view.getText() with or without transformation to match: is "1"))))' matches 2 views in the hierarchy:
- [1] LinearLayout{id=2131231925, res-name=step_error_container, visibility=VISIBLE, width=544, height=81, 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@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
- [2] LinearLayout{id=2131231925, res-name=step_error_container, 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@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
Problem views are marked with '****MATCHES****' below.

This is the textual representation of the clause interpretation as espresso logs it, which read correct to me (as intended):

(view.getId() is <2131231925/xxx.yyy.zzz:id/step_error_container> 
  and is descendant of a view matching (
    an instance of android.widget.LinearLayout 
    and (
      view is an instance of android.view.ViewGroup 
      and has descendant matching (
        view.getId() is <2131231929/xxx.yyy.zzz:id/step_number>
        and an instance of android.widget.TextView 
        and view.getText() with or without transformation to match: is "1"
      )
    )
  )
)

My issue and question here:

I fail to understand why this results in two matching views. Yes, there are two view elements with the ID I look for, one in each step. Which is why I have the clause that is meant to limit the result set to the match in the specified, the first step (with "1" in the step header). That clause appears to have no effect. Why is what? What can I do to achieve the desired result to limit the matches to those inside the view hierarchy of the specified step?

Upvotes: 0

Views: 59

Answers (0)

Related Questions