Material Design Team c7fa5dc6e9 [TextField][A11y] Add tooltip support to TextInputLayout icons
- The icon's `contentDescription` is used as the tooltip text.
- Tooltips are only shown for icons that are interactive (focusable). Decorative-only icons will not display a tooltip.
- `CheckableImageButton` is updated to provide a callback for when its focusable state changes, which is used to trigger tooltip updates.
- API-level differences are handled to ensure that custom `OnLongClickListeners` are not overridden by the tooltip's long-press listener on older platforms (pre-API 26).

PiperOrigin-RevId: 794951524
2025-08-20 14:50:08 +00:00

1116 lines
45 KiB
Java

/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.material.textfield;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.accessibility.AccessibilityChecks.accessibilityAssertion;
import static androidx.test.espresso.action.ViewActions.clearText;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.hasFocus;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isFocusable;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static com.google.android.material.testutils.TestUtilsActions.setCompoundDrawablesRelative;
import static com.google.android.material.testutils.TestUtilsActions.waitFor;
import static com.google.android.material.testutils.TestUtilsMatchers.isLongClickable;
import static com.google.android.material.testutils.TestUtilsMatchers.withCompoundDrawable;
import static com.google.android.material.testutils.TestUtilsMatchers.withTooltipText;
import static com.google.android.material.testutils.TextInputLayoutActions.clickIcon;
import static com.google.android.material.testutils.TextInputLayoutActions.longClickIcon;
import static com.google.android.material.testutils.TextInputLayoutActions.setCustomEndIconContent;
import static com.google.android.material.testutils.TextInputLayoutActions.setEndIconContentDescription;
import static com.google.android.material.testutils.TextInputLayoutActions.setEndIconMinSize;
import static com.google.android.material.testutils.TextInputLayoutActions.setEndIconMode;
import static com.google.android.material.testutils.TextInputLayoutActions.setEndIconOnClickListener;
import static com.google.android.material.testutils.TextInputLayoutActions.setEndIconOnLongClickListener;
import static com.google.android.material.testutils.TextInputLayoutActions.setError;
import static com.google.android.material.testutils.TextInputLayoutActions.setErrorIconOnClickListener;
import static com.google.android.material.testutils.TextInputLayoutActions.setPrefixText;
import static com.google.android.material.testutils.TextInputLayoutActions.setStartIcon;
import static com.google.android.material.testutils.TextInputLayoutActions.setStartIconContentDescription;
import static com.google.android.material.testutils.TextInputLayoutActions.setStartIconMinSize;
import static com.google.android.material.testutils.TextInputLayoutActions.setStartIconOnClickListener;
import static com.google.android.material.testutils.TextInputLayoutActions.setStartIconOnLongClickListener;
import static com.google.android.material.testutils.TextInputLayoutActions.setStartIconTintList;
import static com.google.android.material.testutils.TextInputLayoutActions.setStartIconTintMode;
import static com.google.android.material.testutils.TextInputLayoutActions.setSuffixText;
import static com.google.android.material.testutils.TextInputLayoutActions.setTransformationMethod;
import static com.google.android.material.testutils.TextInputLayoutMatchers.doesNotShowEndIcon;
import static com.google.android.material.testutils.TextInputLayoutMatchers.doesNotShowStartIcon;
import static com.google.android.material.testutils.TextInputLayoutMatchers.endIconHasContentDescription;
import static com.google.android.material.testutils.TextInputLayoutMatchers.endIconIsChecked;
import static com.google.android.material.testutils.TextInputLayoutMatchers.endIconIsNotChecked;
import static com.google.android.material.testutils.TextInputLayoutMatchers.showsEndIcon;
import static com.google.common.truth.Truth.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.method.PasswordTransformationMethod;
import android.text.method.TransformationMethod;
import android.view.KeyEvent;
import android.view.View;
import android.widget.EditText;
import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.TintAwareDrawable;
import androidx.test.espresso.ViewAssertion;
import androidx.test.espresso.matcher.ViewMatchers.Visibility;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.google.android.material.testapp.R;
import com.google.android.material.testapp.TextInputLayoutWithIconsActivity;
import com.google.android.material.testapp.base.RecreatableAppCompatActivity;
import com.google.android.material.testutils.ActivityUtils;
import java.util.concurrent.atomic.AtomicBoolean;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class TextInputLayoutIconsTest {
@Rule
public final ActivityTestRule<TextInputLayoutWithIconsActivity> activityTestRule =
new ActivityTestRule<>(TextInputLayoutWithIconsActivity.class);
private static final String INPUT_TEXT = "Random input text";
@Test
public void testSetPasswordToggleProgrammatically() {
// Set edit text input type to be password
onView(withId(R.id.textinput_no_icon))
.perform(setTransformationMethod(PasswordTransformationMethod.getInstance()));
// Set end icon as the password toggle
onView(withId(R.id.textinput_no_icon))
.perform(setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE));
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayout =
activity.findViewById(R.id.textinput_no_icon);
// Assert the end icon is the password toggle icon
assertEquals(TextInputLayout.END_ICON_PASSWORD_TOGGLE, textInputLayout.getEndIconMode());
}
@Test
public void testPasswordToggleClick() {
// Type some text on the EditText
onView(withId(R.id.textinput_edittext_pwd))
.perform(typeText(INPUT_TEXT));
final Activity activity = activityTestRule.getActivity();
final EditText textInput =
activity.findViewById(R.id.textinput_edittext_pwd);
// Assert that the password is disguised
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
// Now click the toggle button
onView(withId(R.id.textinput_password)).perform(clickIcon(true));
// And assert that the password is not disguised
assertEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
}
@Test
public void testPasswordToggleDisable() {
final Activity activity = activityTestRule.getActivity();
final EditText textInput =
activity.findViewById(R.id.textinput_edittext_pwd);
// Set some text on the EditText
onView(withId(R.id.textinput_edittext_pwd))
.perform(typeText(INPUT_TEXT));
// Assert that the password is disguised
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
// Disable the password toggle
onView(withId(R.id.textinput_password))
.perform(setEndIconMode(TextInputLayout.END_ICON_NONE));
// Check that the password toggle view is not visible
onView(withId(R.id.textinput_password))
.check(matches(doesNotShowEndIcon()));
// ...and that the password is disguised still
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
}
@Test
public void testPasswordToggleDisableWhenVisible() {
final Activity activity = activityTestRule.getActivity();
final EditText textInput =
activity.findViewById(R.id.textinput_edittext_pwd);
// Type some text on the EditText
onView(withId(R.id.textinput_edittext_pwd))
.perform(typeText(INPUT_TEXT));
// Assert that the password is disguised
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
// Now click the toggle button
onView(withId(R.id.textinput_password)).perform(clickIcon(true));
// Disable the password toggle
onView(withId(R.id.textinput_password))
.perform(setEndIconMode(TextInputLayout.END_ICON_NONE));
// Check that the password is disguised again
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
}
@Test
public void testPasswordToggleIsHiddenAfterReenable() {
final Activity activity = activityTestRule.getActivity();
final EditText textInput =
activity.findViewById(R.id.textinput_edittext_pwd);
// Type some text on the EditText and then click the toggle button
onView(withId(R.id.textinput_edittext_pwd))
.perform(typeText(INPUT_TEXT));
onView(withId(R.id.textinput_password)).perform(clickIcon(true));
// Set end icon to none, and then set it to be the password toggle
onView(withId(R.id.textinput_password))
.perform(setEndIconMode(TextInputLayout.END_ICON_NONE))
.perform(setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE));
// Check that the password is disguised and the toggle button reflects the same state
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
onView(withId(R.id.textinput_password)).perform(clickIcon(true));
}
@Test
public void testPasswordToggleChangesWithTransformationMethod() {
// Assert password toggle is not checked.
onView(withId(R.id.textinput_password)).check(matches(endIconIsNotChecked()));
// Change the edit text transformation method to null.
onView(withId(R.id.textinput_password)).perform(setTransformationMethod(null));
// Assert password toggle is now checked.
onView(withId(R.id.textinput_password)).check(matches(endIconIsChecked()));
}
@Test
public void testPasswordToggleIsCheckedIfTransformationMethodNotPassword() {
// Set end icon as the password toggle
onView(withId(R.id.textinput_no_icon))
.perform(setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE));
// Assert password toggle is checked.
onView(withId(R.id.textinput_no_icon)).check(matches(endIconIsChecked()));
}
@Test
public void testPasswordToggleHasDefaultContentDescription() {
// Check that the TextInputLayout says that it has a content description and that the
// underlying toggle has content description as well
onView(withId(R.id.textinput_password)).check(matches(endIconHasContentDescription()));
}
/**
* Simple test that uses AccessibilityChecks to check that the password toggle icon is
* 'accessible'.
*/
@Test
public void testPasswordToggleIsAccessible() {
onView(
allOf(
withId(R.id.text_input_end_icon),
withContentDescription(R.string.password_toggle_content_description),
isDescendantOfA(withId(R.id.textinput_password))))
.check(accessibilityAssertion());
}
@Test
public void testFocusMovesToEditTextWithPasswordEnabled() {
// Focus the preceding EditText
onView(withId(R.id.textinput_edittext_no_icon)).perform(click()).check(matches(hasFocus()));
// Then send a TAB to focus the next view
getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
// And check that the EditText is focused
onView(withId(R.id.textinput_edittext_pwd)).check(matches(hasFocus()));
}
@Test
@LargeTest
public void testSaveAndRestorePasswordVisibility() throws Throwable {
// Type some text on the EditText
onView(withId(R.id.textinput_edittext_pwd)).perform(typeText(INPUT_TEXT));
onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(false));
// Toggle password to be shown as plain text
onView(withId(R.id.textinput_password)).perform(clickIcon(true));
onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(true));
RecreatableAppCompatActivity activity = activityTestRule.getActivity();
ActivityUtils.recreateActivity(activityTestRule, activity);
ActivityUtils.waitForExecution(activityTestRule);
// Check that the password is still toggled to be shown as plain text
onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(true));
}
@Test
public void testSetClearTextEndIconProgrammatically() {
// Set end icon as the clear button
onView(withId(R.id.textinput_no_icon))
.perform(setEndIconMode(TextInputLayout.END_ICON_CLEAR_TEXT));
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayout =
activity.findViewById(R.id.textinput_no_icon);
// Assert the end icon is the clear button icon
assertEquals(TextInputLayout.END_ICON_CLEAR_TEXT, textInputLayout.getEndIconMode());
}
@Test
public void testClearTextEndIconClick() {
// Type some text on the EditText
onView(withId(R.id.textinput_edittext_clear))
.perform(typeText(INPUT_TEXT));
final Activity activity = activityTestRule.getActivity();
final EditText textInput =
activity.findViewById(R.id.textinput_edittext_clear);
// Click clear button
onView(withId(R.id.textinput_clear)).perform(clickIcon(true));
// Wait for animation to finish
onView(isRoot()).perform(waitFor(200));
// Assert EditText was cleared
assertEquals(0, textInput.getLayout().getText().length());
// Check that the clear button view is not visible
onView(withId(R.id.textinput_clear))
.check(matches(doesNotShowEndIcon()));
}
@Test
public void testClearTextEndIconAppears() {
// Check that the clear button view is not visible
onView(withId(R.id.textinput_clear))
.check(matches(doesNotShowEndIcon()));
// Type some text on the EditText
onView(withId(R.id.textinput_edittext_clear))
.perform(typeText(INPUT_TEXT));
// Wait for animation to finish
onView(isRoot()).perform(waitFor(200));
// Check that the clear button is visible
onView(withId(R.id.textinput_clear))
.check(matches(showsEndIcon()));
}
@Test
public void testClearTextEndIconDisappears() {
// Type some text on the EditText
onView(withId(R.id.textinput_edittext_clear))
.perform(typeText(INPUT_TEXT));
// Delete text
onView(withId(R.id.textinput_edittext_clear))
.perform(clearText());
// Wait for animation to finish
onView(isRoot()).perform(waitFor(200));
// Check that the clear button view is not visible
onView(withId(R.id.textinput_clear))
.check(matches(doesNotShowEndIcon()));
}
@Test
public void testClearTextEndIconHasDefaultContentDescription() {
// Check that the TextInputLayout says that it has a content description and that the
// underlying toggle has content description as well
onView(withId(R.id.textinput_clear))
.check(matches(endIconHasContentDescription()));
}
@Test
public void testClearTextEndIconIsAccessible() {
onView(
allOf(
withId(R.id.text_input_end_icon),
withContentDescription(R.string.clear_text_end_icon_content_description),
isDescendantOfA(withId(R.id.textinput_clear))))
.check(accessibilityAssertion());
}
@Test
public void testSwitchEndIconFromPasswordToggleToClearText() {
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayoutPassword =
activity.findViewById(R.id.textinput_password);
final TextInputLayout textInputLayoutClear =
activity.findViewById(R.id.textinput_clear);
String clearTextContentDesc = textInputLayoutClear.getEndIconContentDescription().toString();
// Set end icon as the clear button on the text field that has the password toggle set
onView(withId(R.id.textinput_password))
.perform(setEndIconMode(TextInputLayout.END_ICON_CLEAR_TEXT));
// Assert the end icon mode is the clear button icon
assertEquals(TextInputLayout.END_ICON_CLEAR_TEXT, textInputLayoutPassword.getEndIconMode());
assertEquals(
clearTextContentDesc, textInputLayoutPassword.getEndIconContentDescription().toString());
assertFalse(textInputLayoutPassword.isEndIconCheckable());
// Assert the clear button is not displayed as there was no text
onView(withId(R.id.textinput_password))
.check(matches(doesNotShowEndIcon()));
// Type some text on the EditText
onView(withId(R.id.textinput_edittext_pwd))
.perform(typeText(INPUT_TEXT));
// Assert icon is now showing
onView(withId(R.id.textinput_password))
.check(matches(showsEndIcon()));
// Assert icon works as expected
onView(withId(R.id.textinput_password)).perform(clickIcon(true));
assertEquals(0, textInputLayoutPassword.getEditText().getText().length());
}
@Test
public void testSwitchEndIcon_clearTextToPasswordToggle_succeeds() {
final Activity activity = activityTestRule.getActivity();
final EditText editTextClearIcon = activity.findViewById(R.id.textinput_edittext_clear);
final TextInputLayout textFieldClearIcon = activity.findViewById(R.id.textinput_clear);
final TextInputLayout textFieldPasswordIcon = activity.findViewById(R.id.textinput_password);
String passwordToggleContentDesc =
textFieldPasswordIcon.getEndIconContentDescription().toString();
// Set end icon as the password toggle on text field that has the clear text icon set
onView(withId(R.id.textinput_clear))
.perform(setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE));
// Assert the end icon mode is the password toggle icon
assertEquals(TextInputLayout.END_ICON_PASSWORD_TOGGLE, textFieldClearIcon.getEndIconMode());
assertEquals(
passwordToggleContentDesc, textFieldClearIcon.getEndIconContentDescription().toString());
assertTrue(textFieldClearIcon.isEndIconCheckable());
// Assert icon is displayed
onView(withId(R.id.textinput_clear)).check(matches(showsEndIcon()));
// Assert icon is checked since edit text's transformation method isn't password
onView(withId(R.id.textinput_clear)).check(matches(endIconIsChecked()));
// Set some text on the edit text
onView(withId(R.id.textinput_edittext_clear)).perform(typeText(INPUT_TEXT));
// Assert icon disguises text once clicked
onView(withId(R.id.textinput_clear)).perform(clickIcon(true));
onView(withId(R.id.textinput_clear)).check(matches(not(endIconIsChecked())));
assertNotEquals(INPUT_TEXT, editTextClearIcon.getLayout().getText().toString());
}
@Test
public void testSetCustomEndIconProgrammatically() {
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputCustomEndIcon =
activity.findViewById(R.id.textinput_custom);
String expectedContentDesc = textInputCustomEndIcon.getEndIconContentDescription().toString();
final TextInputLayout textInputLayout =
activity.findViewById(R.id.textinput_no_icon);
// Set end icon mode to custom end icon
onView(withId(R.id.textinput_no_icon))
.perform(setEndIconMode(TextInputLayout.END_ICON_CUSTOM));
// Set the content of the custom end icon
onView(withId(R.id.textinput_no_icon))
.perform(setCustomEndIconContent());
// Assert the end icon is the custom icon
assertEquals(TextInputLayout.END_ICON_CUSTOM, textInputLayout.getEndIconMode());
assertEquals(expectedContentDesc, textInputLayout.getEndIconContentDescription().toString());
}
@Test
public void testCustomEndIconOnClickListener() {
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputCustomEndIcon =
activity.findViewById(R.id.textinput_custom);
// Set custom on click listener
onView(withId(R.id.textinput_custom))
.perform(
setEndIconOnClickListener(
v -> textInputCustomEndIcon.getEditText().setText("Custom icon on click.")));
// Click custom end icon
onView(withId(R.id.textinput_custom)).perform(clickIcon(true));
// Assert onClickListener worked as expected
assertEquals(
"Custom icon on click.", textInputCustomEndIcon.getEditText().getText().toString());
}
@Test
public void testCustomEndIconOnLongClickListener() {
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputCustomEndIcon = activity.findViewById(R.id.textinput_custom);
// Set custom on click listener
onView(withId(R.id.textinput_custom))
.perform(
setEndIconOnLongClickListener(
v -> {
textInputCustomEndIcon.getEditText().setText("Custom icon on long click.");
return true;
}));
// Click custom end icon
onView(withId(R.id.textinput_custom)).perform(longClickIcon(true));
// Assert onClickListener worked as expected
assertEquals(
"Custom icon on long click.", textInputCustomEndIcon.getEditText().getText().toString());
}
@Test
public void testErrorIconOnClickListener() {
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputErrorEndIcon = activity.findViewById(R.id.textinput_no_icon);
// Set error on click listener
onView(withId(R.id.textinput_no_icon))
.perform(
setErrorIconOnClickListener(
v -> textInputErrorEndIcon.getEditText().setText("Error icon on click.")));
// Show error
onView(withId(R.id.textinput_no_icon)).perform(setError("Error"));
// Click error icon
onView(
allOf(
withId(R.id.text_input_error_icon),
withContentDescription(R.string.error_icon_content_description),
isDescendantOfA(withId(R.id.textinput_no_icon))))
.perform(click());
// Assert onClickListener worked as expected
assertEquals("Error icon on click.", textInputErrorEndIcon.getEditText().getText().toString());
}
@Test
public void testEndIconMaintainsCompoundDrawables() {
// Set a known set of test compound drawables on the EditText
final Drawable start = new ColorDrawable(Color.RED);
final Drawable top = new ColorDrawable(Color.GREEN);
final Drawable end = new ColorDrawable(Color.BLUE);
final Drawable bottom = new ColorDrawable(Color.BLACK);
onView(withId(R.id.textinput_edittext_pwd))
.perform(setCompoundDrawablesRelative(start, top, end, bottom));
// Enable the password toggle and check that the start, top and bottom drawables are
// maintained
onView(withId(R.id.textinput_password))
.perform(setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE));
onView(withId(R.id.textinput_edittext_pwd))
.check(matches(withCompoundDrawable(0, start)))
.check(matches(withCompoundDrawable(1, top)))
.check(matches(not(withCompoundDrawable(2, end))))
.check(matches(withCompoundDrawable(3, bottom)));
// Now remove the end icon and check that all of the original compound drawables
// are set
onView(withId(R.id.textinput_password))
.perform(setEndIconMode(TextInputLayout.END_ICON_NONE));
onView(withId(R.id.textinput_edittext_pwd))
.check(matches(withCompoundDrawable(0, start)))
.check(matches(withCompoundDrawable(1, top)))
.check(matches(withCompoundDrawable(2, end)))
.check(matches(withCompoundDrawable(3, bottom)));
}
@Test
public void testSetStartIconProgrammatically() {
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayout = activity.findViewById(R.id.textinput_no_icon);
Drawable drawable = new ColorDrawable(Color.BLACK);
String contentDesc = "Start icon";
// Set start icon
onView(withId(R.id.textinput_no_icon)).perform(setStartIcon(drawable));
onView(withId(R.id.textinput_no_icon)).perform(setStartIconContentDescription(contentDesc));
onView(withId(R.id.textinput_no_icon)).perform(setStartIconTintList(null));
// Assert the start icon is set
assertNotNull(textInputLayout.getStartIconDrawable());
assertEquals(contentDesc, textInputLayout.getStartIconContentDescription().toString());
}
@Test
public void testSetStartIconTint() {
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayout = activity.findViewById(R.id.textinput_no_icon);
Drawable drawable = new TintCapturedDrawable();
// Set start icon
onView(withId(R.id.textinput_no_icon)).perform(
setStartIconTintList(ColorStateList.valueOf(Color.RED)));
onView(withId(R.id.textinput_no_icon)).perform(setStartIconTintMode(PorterDuff.Mode.MULTIPLY));
onView(withId(R.id.textinput_no_icon)).perform(setStartIcon(drawable));
// Assert the start icon's tint is set
assertNotNull(textInputLayout.getStartIconDrawable());
assertThat(textInputLayout.getStartIconDrawable()).isInstanceOf(TintCapturedDrawable.class);
assertEquals(
Color.RED,
((TintCapturedDrawable) textInputLayout.getStartIconDrawable())
.capturedTint.getDefaultColor());
assertEquals(
PorterDuff.Mode.MULTIPLY,
((TintCapturedDrawable) textInputLayout.getStartIconDrawable()).capturedTintMode);
}
@Test
public void testStartIconDisables() {
// Disable the start icon
onView(withId(R.id.textinput_starticon)).perform(setStartIcon(null));
// Check that the start icon view is not visible
onView(withId(R.id.textinput_starticon)).check(matches(doesNotShowStartIcon()));
}
@Test
public void testStartIconOnClickListener() {
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayout = activity.findViewById(R.id.textinput_starticon);
// Set click listener on start icon
onView(withId(R.id.textinput_starticon))
.perform(
setStartIconOnClickListener(
v -> textInputLayout.getEditText().setText("Start icon on click")));
// Click start icon
onView(withId(R.id.textinput_starticon)).perform(clickIcon(false));
// Assert OnClickListener worked as expected
assertEquals("Start icon on click", textInputLayout.getEditText().getText().toString());
}
@Test
public void testStartIconOnLongClickListener() {
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayout = activity.findViewById(R.id.textinput_starticon);
// Set click listener on start icon
onView(withId(R.id.textinput_starticon))
.perform(
setStartIconOnLongClickListener(
v -> {
textInputLayout.getEditText().setText("Start icon on long click");
return true;
}));
// Click start icon
onView(withId(R.id.textinput_starticon)).perform(longClickIcon(false));
// Assert OnClickListener worked as expected
assertEquals("Start icon on long click", textInputLayout.getEditText().getText().toString());
}
/**
* Simple test that uses AccessibilityChecks to check that the start icon is 'accessible'.
*/
@Test
public void testStartIconToggleIsAccessible() {
onView(
allOf(
withId(R.id.text_input_start_icon),
isDescendantOfA(withId(R.id.textinput_starticon))))
.check(accessibilityAssertion());
}
@Test
public void testErrorIconAppears() {
// Set error
onView(withId(R.id.textinput_no_icon)).perform(setError("Error"));
// Check the icon is visible
onView(
allOf(
withId(R.id.text_input_error_icon),
withContentDescription(R.string.error_icon_content_description),
isDescendantOfA(withId(R.id.textinput_no_icon))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)));
}
@Test
public void testErrorIconDisappears() {
// Set error
onView(withId(R.id.textinput_no_icon)).perform(setError("Error"));
// Unset error
onView(withId(R.id.textinput_no_icon)).perform(setError(null));
// Check there is no icon
onView(withId(R.id.textinput_no_icon)).check(matches(doesNotShowEndIcon()));
}
@Test
public void testErrorIconMaintainsCompoundDrawables() {
// Set a known set of test compound drawables on the EditText
final Drawable start = new ColorDrawable(Color.RED);
final Drawable top = new ColorDrawable(Color.GREEN);
final Drawable end = new ColorDrawable(Color.BLUE);
final Drawable bottom = new ColorDrawable(Color.BLACK);
onView(withId(R.id.textinput_edittext_no_icon))
.perform(setCompoundDrawablesRelative(start, top, end, bottom));
// Set error and check that the start, top and bottom drawables are maintained
onView(withId(R.id.textinput_no_icon)).perform(setError("Error"));
onView(withId(R.id.textinput_edittext_no_icon))
.check(matches(withCompoundDrawable(0, start)))
.check(matches(withCompoundDrawable(1, top)))
.check(matches(not(withCompoundDrawable(2, end))))
.check(matches(withCompoundDrawable(3, bottom)));
// Now remove error and check that all of the original compound drawables are set
onView(withId(R.id.textinput_no_icon)).perform(setError(null));
onView(withId(R.id.textinput_edittext_no_icon))
.check(matches(withCompoundDrawable(0, start)))
.check(matches(withCompoundDrawable(1, top)))
.check(matches(withCompoundDrawable(2, end)))
.check(matches(withCompoundDrawable(3, bottom)));
}
@Test
public void testErrorIconMaintainsEndIcon() {
// Set error on text field with password toggle icon
onView(withId(R.id.textinput_password)).perform(setError("Error"));
// Check icon showing is error icon only
onView(
allOf(
withId(R.id.text_input_error_icon),
withContentDescription(R.string.error_icon_content_description),
isDescendantOfA(withId(R.id.textinput_password))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)));
onView(
allOf(
withId(R.id.text_input_end_icon),
withContentDescription(R.string.password_toggle_content_description),
isDescendantOfA(withId(R.id.textinput_password))))
.check(matches(withEffectiveVisibility(Visibility.GONE)));
// Unset error
onView(withId(R.id.textinput_password)).perform(setError(null));
// Check end icon is back
onView(
allOf(
withId(R.id.text_input_error_icon),
withContentDescription(R.string.error_icon_content_description),
isDescendantOfA(withId(R.id.textinput_password))))
.check(matches(withEffectiveVisibility(Visibility.GONE)));
onView(
allOf(
withId(R.id.text_input_end_icon),
withContentDescription(R.string.password_toggle_content_description),
isDescendantOfA(withId(R.id.textinput_password))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)));
}
@Test
public void testErrorIconMaintainsDisguisedInputText() {
final Activity activity = activityTestRule.getActivity();
final EditText editText =
activity.findViewById(R.id.textinput_edittext_pwd);
// Set some text on the EditText
onView(withId(R.id.textinput_edittext_pwd)).perform(typeText(INPUT_TEXT));
// Assert that the password is disguised
assertNotEquals(INPUT_TEXT, editText.getLayout().getText().toString());
// Set error
onView(withId(R.id.textinput_password)).perform(setError("Error"));
// Check that the password is disguised still
assertNotEquals(INPUT_TEXT, editText.getLayout().getText().toString());
}
@Test
public void testSetPrefixProgrammatically() {
// Set prefix
onView(withId(R.id.textinput_no_icon)).perform(setPrefixText("$"));
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayout =
activity.findViewById(R.id.textinput_no_icon);
// Assert the prefix is set
assertEquals("$", textInputLayout.getPrefixText().toString());
}
@Test
public void testClearPrefix() {
// Set prefix
onView(withId(R.id.textinput_prefix)).perform(setPrefixText(null));
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayout =
activity.findViewById(R.id.textinput_prefix);
// Assert the prefix is null
assertNull(textInputLayout.getPrefixText());
}
@Test
public void testPrefixIsVisibleOnFocus() {
// Click on text field
onView(withId(R.id.textinput_prefix)).perform(click());
// Assert prefix is visible
onView(allOf(
withId(R.id.textinput_prefix_text),
isDescendantOfA(withId(R.id.textinput_prefix))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)));
}
@Test
public void testPrefixDisappearsIfEditTextIsEmpty() {
// Click on text field
onView(withId(R.id.textinput_prefix)).perform(click());
// Assert prefix is visible
onView(allOf(
withId(R.id.textinput_prefix_text),
isDescendantOfA(withId(R.id.textinput_prefix))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)));
// Click on another text field
onView(withId(R.id.textinput_no_icon)).perform(click());
// Assert prefix is not visible
onView(allOf(
withId(R.id.textinput_prefix_text),
isDescendantOfA(withId(R.id.textinput_prefix))))
.check(matches(withEffectiveVisibility(Visibility.GONE)));
}
@Test
public void testPrefixStaysVisibleWithInputText() {
// Type some text on the EditText
onView(withId(R.id.textinput_edittext_prefix)).perform(typeText(INPUT_TEXT));
// Click on another text field
onView(withId(R.id.textinput_no_icon)).perform(click());
// Assert suffix is still visible
onView(allOf(
withId(R.id.textinput_prefix_text),
isDescendantOfA(withId(R.id.textinput_prefix))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)));
}
@Test
public void testSetSuffixProgrammatically() {
// Set prefix
onView(withId(R.id.textinput_no_icon)).perform(setSuffixText("/100"));
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayout =
activity.findViewById(R.id.textinput_no_icon);
// Assert the prefix is set
assertEquals("/100", textInputLayout.getSuffixText().toString());
}
@Test
public void testClearSuffix() {
// Set prefix
onView(withId(R.id.textinput_suffix)).perform(setSuffixText(null));
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayout =
activity.findViewById(R.id.textinput_suffix);
// Assert the suffix is null
assertNull(textInputLayout.getSuffixText());
}
@Test
public void testSuffixIsVisibleOnFocus() {
// Click on text field
onView(withId(R.id.textinput_suffix)).perform(click());
// Assert suffix is visible
onView(allOf(
withId(R.id.textinput_suffix_text),
isDescendantOfA(withId(R.id.textinput_suffix))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)));
}
@Test
public void testSuffixDisappearsIfEditTextIsEmpty() {
// Click on text field
onView(withId(R.id.textinput_suffix)).perform(click());
// Assert prefix is visible
onView(allOf(
withId(R.id.textinput_suffix_text),
isDescendantOfA(withId(R.id.textinput_suffix))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)));
// Click on another text field
onView(withId(R.id.textinput_no_icon)).perform(click());
// Assert suffix is not visible
onView(allOf(
withId(R.id.textinput_suffix_text),
isDescendantOfA(withId(R.id.textinput_suffix))))
.check(matches(withEffectiveVisibility(Visibility.GONE)));
}
@Test
public void testSuffixStaysVisibleWithInputText() {
// Type some text on the EditText
onView(withId(R.id.textinput_edittext_suffix)).perform(typeText(INPUT_TEXT));
// Click on another text field
onView(withId(R.id.textinput_no_icon)).perform(click());
// Assert suffix is still visible
onView(allOf(
withId(R.id.textinput_suffix_text),
isDescendantOfA(withId(R.id.textinput_suffix))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)));
}
@Test
public void testSuffixMaintainsCompoundDrawables() {
// Set a known set of test compound drawables on the EditText
final Drawable start = new ColorDrawable(Color.RED);
final Drawable top = new ColorDrawable(Color.GREEN);
final Drawable end = new ColorDrawable(Color.BLUE);
final Drawable bottom = new ColorDrawable(Color.BLACK);
onView(withId(R.id.textinput_edittext_no_icon))
.perform(setCompoundDrawablesRelative(start, top, end, bottom));
// Set suffix and check that the start, top and bottom drawables are maintained
onView(withId(R.id.textinput_no_icon)).perform(setSuffixText("/100"));
onView(withId(R.id.textinput_edittext_no_icon))
.check(matches(withCompoundDrawable(0, start)))
.check(matches(withCompoundDrawable(1, top)))
.check(matches(not(withCompoundDrawable(2, end))))
.check(matches(withCompoundDrawable(3, bottom)));
// Now remove suffix and check that all of the original compound drawables are set
onView(withId(R.id.textinput_no_icon)).perform(setSuffixText(null));
onView(withId(R.id.textinput_edittext_no_icon))
.check(matches(withCompoundDrawable(0, start)))
.check(matches(withCompoundDrawable(1, top)))
.check(matches(withCompoundDrawable(2, end)))
.check(matches(withCompoundDrawable(3, bottom)));
}
@Test
public void testStartIconIconSize() {
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayout =
activity.findViewById(R.id.textinput_starticon);
onView(
withId(R.id.textinput_starticon)).perform(setStartIconMinSize(50));
assertEquals(50, textInputLayout.getStartIconMinSize());
}
@Test
public void testStartIconInvalidIconSize() {
assertThrows(IllegalArgumentException.class, () -> onView(
withId(R.id.textinput_starticon)).perform(setStartIconMinSize(-1)));
}
@Test
public void testEndIconIconSize() {
final Activity activity = activityTestRule.getActivity();
final TextInputLayout textInputLayout =
activity.findViewById(R.id.textinput_suffix);
onView(
withId(R.id.textinput_suffix)).perform(setEndIconMinSize(50));
assertEquals(50, textInputLayout.getEndIconMinSize());
}
@Test
public void testEndIconInvalidIconSize() {
assertThrows(IllegalArgumentException.class, () -> onView(
withId(R.id.textinput_suffix)).perform(setEndIconMinSize(-1)));
}
@Test
public void testEndIconTooltip() {
final String tooltip = "Tooltip text";
final int textInputLayoutId = R.id.textinput_no_icon;
final Matcher<View> endIconMatcher =
allOf(withId(R.id.text_input_end_icon), isDescendantOfA(withId(textInputLayoutId)));
setUpCustomEndIconWithTooltip(textInputLayoutId, tooltip);
final Matcher<View> hasTooltipMatcher =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
? withTooltipText(tooltip)
: isLongClickable();
final Matcher<View> hasNoTooltipMatcher =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
? withTooltipText(null)
: not(isLongClickable());
// The icon should not be focusable and have no tooltip by default.
onView(endIconMatcher).check(matches(allOf(not(isFocusable()), hasNoTooltipMatcher)));
// When an OnClickListener is set, the icon should become focusable and show a tooltip.
onView(withId(textInputLayoutId)).perform(setEndIconOnClickListener(v -> {}));
onView(endIconMatcher).check(matches(allOf(isFocusable(), hasTooltipMatcher)));
// When the OnClickListener is removed, the icon should no longer be focusable or have a
// tooltip.
onView(withId(textInputLayoutId)).perform(setEndIconOnClickListener(null));
onView(endIconMatcher).check(matches(allOf(not(isFocusable()), hasNoTooltipMatcher)));
}
@Test
public void testEndIconTooltip_withLongClickListener() {
final AtomicBoolean longClicked = new AtomicBoolean(false);
final int textInputLayoutId = R.id.textinput_no_icon;
setUpCustomEndIconWithTooltip(textInputLayoutId, "tooltip");
onView(withId(textInputLayoutId))
.perform(
setEndIconOnLongClickListener(
v -> {
longClicked.set(true);
return true;
}));
onView(withId(textInputLayoutId)).perform(longClickIcon(true));
assertTrue(longClicked.get());
}
@Test
public void testStartIconTooltip() {
final String tooltip = "Tooltip text";
final int textInputLayoutId = R.id.textinput_no_icon;
final Matcher<View> startIconMatcher =
allOf(withId(R.id.text_input_start_icon), isDescendantOfA(withId(textInputLayoutId)));
setUpStartIconWithTooltip(textInputLayoutId, tooltip);
final Matcher<View> hasTooltipMatcher =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
? withTooltipText(tooltip)
: isLongClickable();
final Matcher<View> hasNoTooltipMatcher =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
? withTooltipText(null)
: not(isLongClickable());
// The icon should not be focusable and have no tooltip by default.
onView(startIconMatcher).check(matches(allOf(not(isFocusable()), hasNoTooltipMatcher)));
// When an OnClickListener is set, the icon should become focusable and show a tooltip.
onView(withId(textInputLayoutId)).perform(setStartIconOnClickListener(v -> {}));
onView(startIconMatcher).check(matches(allOf(isFocusable(), hasTooltipMatcher)));
// When the OnClickListener is removed, the icon should no longer be focusable or have a
// tooltip.
onView(withId(textInputLayoutId)).perform(setStartIconOnClickListener(null));
onView(startIconMatcher).check(matches(allOf(not(isFocusable()), hasNoTooltipMatcher)));
}
@Test
public void testStartIconTooltip_withLongClickListener() {
final AtomicBoolean longClicked = new AtomicBoolean(false);
final int textInputLayoutId = R.id.textinput_no_icon;
setUpStartIconWithTooltip(textInputLayoutId, "tooltip");
onView(withId(textInputLayoutId))
.perform(
setStartIconOnLongClickListener(
v -> {
longClicked.set(true);
return true;
}));
onView(withId(textInputLayoutId)).perform(longClickIcon(false));
assertTrue(longClicked.get());
}
private void setUpCustomEndIconWithTooltip(int textInputLayoutId, String tooltip) {
onView(withId(textInputLayoutId))
.perform(
setEndIconMode(TextInputLayout.END_ICON_CUSTOM),
setCustomEndIconContent(),
setEndIconContentDescription(tooltip));
}
private void setUpStartIconWithTooltip(int textInputLayoutId, String tooltip) {
onView(withId(textInputLayoutId))
.perform(
setStartIcon(new ColorDrawable(Color.BLACK)), setStartIconContentDescription(tooltip));
}
private static ViewAssertion isPasswordToggledVisible(final boolean isToggledVisible) {
return (view, noViewFoundException) -> {
assertTrue(view instanceof TextInputLayout);
EditText editText = ((TextInputLayout) view).getEditText();
TransformationMethod transformationMethod = editText.getTransformationMethod();
if (isToggledVisible) {
assertNull(transformationMethod);
} else {
assertEquals(PasswordTransformationMethod.getInstance(), transformationMethod);
}
};
}
private static class TintCapturedDrawable extends ColorDrawable implements TintAwareDrawable {
ColorStateList capturedTint;
PorterDuff.Mode capturedTintMode;
TintCapturedDrawable() {
super(Color.WHITE);
}
@Override
public void setTintList(@Nullable ColorStateList tint) {
capturedTint = tint;
}
@Override
public void setTintMode(@Nullable PorterDuff.Mode tintMode) {
capturedTintMode = tintMode;
}
}
}