30

I am trying to test using Espresso if my TextInputLayout views have specific hint. I'd used a code as below:

Espresso.onView(ViewMatchers.withId(R.id.edit_text_email))
    .check(ViewAssertions.matches(
        ViewMatchers.withHint(R.string.edit_text_email_hint)))

This works fine for the normal EditText views, not wrapped in TextInputLayout. However when it wraps around, it no longer works.

I tried to use solution from Android Espresso - How to check EditText hint?, but it still does not working.

I also looked into: https://code.google.com/p/android/issues/detail?id=191261 that reported the issue, it says the workaround is quite easy by pointing to the current withHint code, but I can't get it to work.

Any ideas to fix this issue?

Zoe
  • 27,060
  • 21
  • 118
  • 148
Elye
  • 53,639
  • 54
  • 212
  • 474

10 Answers10

43

Here's my custom matcher:

public static Matcher<View> hasTextInputLayoutHintText(final String expectedErrorText) {
        return new TypeSafeMatcher<View>() {

            @Override
            public boolean matchesSafely(View view) {
                if (!(view instanceof TextInputLayout)) {
                    return false;
                }

                CharSequence error = ((TextInputLayout) view).getHint();

                if (error == null) {
                    return false;
                }

                String hint = error.toString();

                return expectedErrorText.equals(hint);
            }

            @Override
            public void describeTo(Description description) {
            }
        };
    }
}

and here's how to use:

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

    @Rule
    public ActivityTestRule<MainActivity> mRule = new ActivityTestRule<>(MainActivity.class);

    @Test
    public void testMyApp() {
        onView(withId(R.id.textInputLayout)).check
                (matches(hasTextInputLayoutErrorText(mRule.getActivity().getString(R.string
                        .app_name))));

    }

If you would like to check errorText of TextInputLayout, change this line:

     CharSequence error = ((TextInputLayout) view).getHint();

with

     CharSequence error = ((TextInputLayout) view).getError();

Hope it will help

piotrek1543
  • 19,130
  • 7
  • 81
  • 94
  • Where I can put custom matcher hasTextInputLayoutHintText ? – aleksandrbel Jun 20 '17 at 12:55
  • Since in my code I set the error to TextInputEditText, I had to change TextInputLayout to TextInputEditText in the matcher. – Rodrigo Venancio Feb 22 '18 at 18:26
  • You could avoid having to check for `instanceof` while matching if you'd otherwise inherit from [BoundedMatcher](https://developer.android.com/reference/androidx/test/espresso/matcher/BoundedMatcher) – tinsukE Jan 07 '19 at 13:32
21

Kotlin version of piotrek1543's answer:

fun hasTextInputLayoutHintText(expectedErrorText: String): Matcher<View> = object : TypeSafeMatcher<View>() {

    override fun describeTo(description: Description?) { }

    override fun matchesSafely(item: View?): Boolean {
        if (item !is TextInputLayout) return false
        val error = item.hint ?: return false
        val hint = error.toString()
        return expectedErrorText == hint
    }
}
Phil
  • 4,730
  • 1
  • 41
  • 39
3

More generic solution that would work with any View that has "getHint" method:

public static Matcher<View> withCustomHint(final Matcher<String> stringMatcher) {
    return new BaseMatcher<View>() {
        @Override
        public void describeTo(Description description) {
        }

        @Override
        public boolean matches(Object item) {
            try {
                Method method = item.getClass().getMethod("getHint");
                return stringMatcher.matches(method.invoke(item));
            } catch (NoSuchMethodException e) {
            } catch (InvocationTargetException e) {
            } catch (IllegalAccessException e) {
            }
            return false;
        }
    };
}

Usage:

onView(withId(R.id.SomeLayout)).check(matches(withCustomHint(is("SomeString"))));
Anton Tananaev
  • 2,458
  • 1
  • 25
  • 48
2

A much more simple solution is to check if the error text is visible, for example:

val text = mTestRule.getActivity().getString(R.string.error_text)
onView(withText(text)).check(matches(isDisplayed()))
RobertoAllende
  • 8,744
  • 4
  • 30
  • 49
2

you have two solutions

-First

    onView(withText(errorMessage)).check(matches(isDisplayed()))

-second

    onView(withId(R.id.textinput_error)).check(matches(withText(errorMessage)))
Fahad Alotaibi
  • 416
  • 3
  • 9
1

Java reflection can be avoided. Also, hints are supported by TextView and all it's descendants (including TextInputEditText) or TextInputLayout. Thus the

fun withHint(expected: String) = object : TypeSafeMatcher<View>() {
    override fun describeTo(description: Description) {
        description.appendText("TextView or TextInputLayout with hint '$expected'")
    }

    override fun matchesSafely(item: View?) =
        item is TextInputLayout && expected == item.hint || item is TextView && expected == item.hint
}

And can be used like this:

onView(withId(R.id.exampleView)).check(matches(withHint("example text")))
saschpe
  • 865
  • 8
  • 8
1

Some of the adobe solutions are correct, but I wanted to add a kotlin version that I think is simpler than the rest:

fun hasNoErrorText() = object : TypeSafeMatcher<View>() {
    override fun describeTo(description: Description) {
        description.appendText("has no error text ")
    }

    override fun matchesSafely(view: View?) = view is TextInputLayout && view.error == null
}
Kuruchy
  • 1,330
  • 1
  • 14
  • 26
0

The solutions above didn't work for my use case. I wanted to find the TextInputEditText and type text into it. Here is my solution:

    @VisibleForTesting
class WithTextInputLayoutHintMatcher @RemoteMsgConstructor
constructor(@field:RemoteMsgField(order = 0)
            private val stringMatcher: Matcher<String>) : TypeSafeMatcher<View>() {

    override fun describeTo(description: Description) {
        description.appendText("with TextInputLayout hint: ")
        stringMatcher.describeTo(description)
    }

    public override fun matchesSafely(textInputEditText: View): Boolean {
        if (textInputEditText !is TextInputEditText) return false

        return stringMatcher.matches((textInputEditText.parent.parent as? TextInputLayout)?.hint)
    }
}

/**
 * Returns a matcher that matches [TextInputEditText] based on it's hint property value.
 *
 *
 * **Note:** View's sugar for `withHint(is("string"))`.
 *
 * @param hintText [String] with the hint text to match
 */
fun withTextInputHint(hintText: String): Matcher<View> {
    return withTextInputHint(Matchers.`is`(checkNotNull(hintText)))
}

/**
 * Returns a matcher that matches a descendant of [TextInputEditText] that is displaying the hint
 * associated with the given resource id.
 *
 * @param resourceId the string resource the text view is expected to have as a hint.
 */
fun withTextInputHint(resourceId: Int): Matcher<View> {
    return withTextInputHint(getString(resourceId))
}

/**
 * Returns a matcher that matches [TextView]s based on hint property value.
 *
 *
 * **Note:** View's hint property can be `null`, to match against it use `
 * withHint(nullValue(String.class)`
 *
 * @param stringMatcher [`Matcher
`](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html) *  of [String] with text to match
 */
fun withTextInputHint(stringMatcher: Matcher<String>): Matcher<View> {
    return WithTextInputLayoutHintMatcher(checkNotNull(stringMatcher))
}

Usage:

onView(withTextInputHint(R.string.hint)).perform(ViewActions.typeText("Type text here"))

Peter Keefe
  • 1,095
  • 14
  • 22
0

Using BoundedMatcher you can get rid of the type check:

fun withHint(@StringRes hintId: Int?) =
    object : BoundedMatcher<View, TextInputLayout>(TextInputLayout::class.java) {

        override fun matchesSafely(item: TextInputLayout?): Boolean =
            when {
                item == null -> false
                hintId == null -> item.hint == null
                else -> item.hint?.toString() == item.context.getString(hintId)
            }

        override fun describeTo(description: Description?) {
            description?.appendText("with hint id $hintId")
        }
    }
Eva
  • 4,397
  • 5
  • 43
  • 65
-1

If you're trying to check the error in a Material TextInputLayout, try something like this:

onView(withId(viewId)).check(matches(textInputLayoutErrorTextMatcher(getString(stringId))))

Make sure to supply the id of the TextInputLayout and not its child (i.e. TextInputEditText).

Ashton Coulson
  • 275
  • 3
  • 4