afohrman 127dfe1255 Add graceful degradation for character counter and label text overflow color.
Previously, the character counter and label overflow color ended up as Color.MAGENTA, which actually is a sickly pink color. This appears to have been happening when the theme doesn't extend from Theme.Design, and therefore doesn't have textErrorColor. On API 23 and above, it gets set to MAGENTA instead of throwing an exception. The only reason this pink color was being used for the character counter and not the error text is because there already a fix for the error text - it just wasn't applied to the character counter.

I pulled the logic for the fix out to make this graceful degradation code reusable for both error and character text, and for future situations like this.

PiperOrigin-RevId: 159229718
2017-09-21 11:23:47 -04:00

476 lines
19 KiB
Java

/*
* Copyright (C) 2016 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 android.support.design.widget;
import static android.support.design.testutils.TestUtilsActions.setCompoundDrawablesRelative;
import static android.support.design.testutils.TestUtilsActions.setEnabled;
import static android.support.design.testutils.TestUtilsMatchers.withCompoundDrawable;
import static android.support.design.testutils.TestUtilsMatchers.withTextColor;
import static android.support.design.testutils.TestUtilsMatchers.withTypeface;
import static android.support.design.testutils.TextInputLayoutActions.setCounterEnabled;
import static android.support.design.testutils.TextInputLayoutActions.setCounterMaxLength;
import static android.support.design.testutils.TextInputLayoutActions.setError;
import static android.support.design.testutils.TextInputLayoutActions.setErrorEnabled;
import static android.support.design.testutils.TextInputLayoutActions.setErrorTextAppearance;
import static android.support.design.testutils.TextInputLayoutActions.setPasswordVisibilityToggleEnabled;
import static android.support.design.testutils.TextInputLayoutActions.setTypeface;
import static android.support.design.testutils.TextInputLayoutMatchers.hasPasswordToggleContentDescription;
import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.contrib.AccessibilityChecks.accessibilityAssertion;
import static android.support.test.espresso.matcher.ViewMatchers.hasContentDescription;
import static android.support.test.espresso.matcher.ViewMatchers.hasFocus;
import static android.support.test.espresso.matcher.ViewMatchers.isChecked;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Parcelable;
import android.support.design.testapp.R;
import android.support.design.testapp.TextInputLayoutActivity;
import android.support.design.testutils.TestUtils;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.MediumTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.widget.TextViewCompat;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.ViewAssertion;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class TextInputLayoutTest {
@Rule
public final ActivityTestRule<TextInputLayoutActivity> activityTestRule =
new ActivityTestRule<>(TextInputLayoutActivity.class);
private static final String ERROR_MESSAGE_1 = "An error has occured";
private static final String ERROR_MESSAGE_2 = "Some other error has occured";
private static final String INPUT_TEXT = "Random input text";
private static final Typeface CUSTOM_TYPEFACE = Typeface.SANS_SERIF;
private static class TestTextInputLayout extends TextInputLayout {
public int animateToExpansionFractionCount = 0;
public float animateToExpansionFractionRecentValue = -1;
public TestTextInputLayout(Context context) {
super(context);
}
public TestTextInputLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TestTextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void animateToExpansionFraction(float target) {
super.animateToExpansionFraction(target);
animateToExpansionFractionRecentValue = target;
animateToExpansionFractionCount++;
}
}
@Test
public void testTypingTextCollapsesHint() {
// Type some text
onView(withId(R.id.textinput_edittext)).perform(typeText(INPUT_TEXT));
// ...and check that the hint has collapsed
onView(withId(R.id.textinput)).check(isHintExpanded(false));
}
@Test
public void testSetErrorEnablesErrorIsDisplayed() {
onView(withId(R.id.textinput)).perform(setError(ERROR_MESSAGE_1));
onView(withText(ERROR_MESSAGE_1)).check(matches(isDisplayed()));
}
@Test
public void testDisabledErrorIsNotDisplayed() {
// First show an error, and then disable error functionality
onView(withId(R.id.textinput))
.perform(setError(ERROR_MESSAGE_1))
.perform(setErrorEnabled(false));
// Check that the error is no longer there
onView(withText(ERROR_MESSAGE_1)).check(doesNotExist());
}
@Test
public void testSetErrorOnDisabledSetErrorIsDisplayed() {
// First show an error, and then disable error functionality
onView(withId(R.id.textinput))
.perform(setError(ERROR_MESSAGE_1))
.perform(setErrorEnabled(false));
// Now show a different error message
onView(withId(R.id.textinput)).perform(setError(ERROR_MESSAGE_2));
// And check that it is displayed
onView(withText(ERROR_MESSAGE_2)).check(matches(isDisplayed()));
}
@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 = (EditText) 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.text_input_password_toggle)).perform(click());
// 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 = (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, textInput.getLayout().getText().toString());
// Disable the password toggle
onView(withId(R.id.textinput_password)).perform(setPasswordVisibilityToggleEnabled(false));
// Check that the password toggle view is not visible
onView(withId(R.id.text_input_password_toggle)).check(matches(not(isDisplayed())));
// ...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 = (EditText) 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.text_input_password_toggle)).perform(click());
// Disable the password toggle
onView(withId(R.id.textinput_password)).perform(setPasswordVisibilityToggleEnabled(false));
// Check that the password is disguised again
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
}
@Test
public void testPasswordToggleMaintainsCompoundDrawables() {
// 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(setPasswordVisibilityToggleEnabled(true));
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 disable the password toggle and check that all of the original compound drawables
// are set
onView(withId(R.id.textinput_password)).perform(setPasswordVisibilityToggleEnabled(false));
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 testPasswordToggleIsHiddenAfterReenable() {
final Activity activity = activityTestRule.getActivity();
final EditText textInput = (EditText) 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.text_input_password_toggle)).perform(click());
// Disable the password toggle, and then re-enable it
onView(withId(R.id.textinput_password))
.perform(setPasswordVisibilityToggleEnabled(false))
.perform(setPasswordVisibilityToggleEnabled(true));
// 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.text_input_password_toggle)).check(matches(not(isChecked())));
}
@Test
public void testSetEnabledFalse() {
// First click on the EditText, so that it is focused and the hint collapses...
onView(withId(R.id.textinput_edittext)).perform(click());
// Now disable the TextInputLayout and check that the hint expands
onView(withId(R.id.textinput)).perform(setEnabled(false)).check(isHintExpanded(true));
// Finally check that the EditText is no longer enabled
onView(withId(R.id.textinput_edittext)).check(matches(not(isEnabled())));
}
@Test
public void testSetEnabledFalseWithText() {
// First set some text, then disable the TextInputLayout
onView(withId(R.id.textinput_edittext)).perform(typeText(INPUT_TEXT));
onView(withId(R.id.textinput)).perform(setEnabled(false));
// Now check that the EditText is no longer enabled
onView(withId(R.id.textinput_edittext)).check(matches(not(isEnabled())));
}
@UiThreadTest
@Test
public void testExtractUiHintSet() {
final Activity activity = activityTestRule.getActivity();
// Set a hint on the TextInputLayout
final TextInputLayout layout = (TextInputLayout) activity.findViewById(R.id.textinput);
layout.setHint(INPUT_TEXT);
final EditText editText = (EditText) activity.findViewById(R.id.textinput_edittext);
// Now manually pass in a EditorInfo to the EditText and make sure it updates the
// hintText to our known value
final EditorInfo info = new EditorInfo();
editText.onCreateInputConnection(info);
assertEquals(INPUT_TEXT, info.hintText);
}
@UiThreadTest
@Test
public void testDrawableStateChanged() {
final Activity activity = activityTestRule.getActivity();
final TextInputLayout layout = (TextInputLayout) activity.findViewById(R.id.textinput);
// Force a drawable state change.
layout.drawableStateChanged();
}
@UiThreadTest
@Test
public void testSaveRestoreStateAnimation() {
final Activity activity = activityTestRule.getActivity();
final TestTextInputLayout layout = new TestTextInputLayout(activity);
layout.setId(R.id.textinputlayout);
final TextInputEditText editText = new TextInputEditText(activity);
editText.setText(INPUT_TEXT);
editText.setId(R.id.textinputedittext);
layout.addView(editText);
SparseArray<Parcelable> container = new SparseArray<>();
layout.saveHierarchyState(container);
layout.restoreHierarchyState(container);
assertEquals(
"Expected no animations since we simply saved/restored state",
0,
layout.animateToExpansionFractionCount);
editText.setText("");
assertEquals(
"Expected one call to animate because we cleared text in editText",
1,
layout.animateToExpansionFractionCount);
assertEquals(0f, layout.animateToExpansionFractionRecentValue, 0f);
container = new SparseArray<>();
layout.saveHierarchyState(container);
layout.restoreHierarchyState(container);
assertEquals(
"Expected no additional animations since we simply saved/restored state",
1,
layout.animateToExpansionFractionCount);
}
@UiThreadTest
@Test
public void testMaintainsLeftRightCompoundDrawables() throws Throwable {
final Activity activity = activityTestRule.getActivity();
// Set a known set of test compound drawables on the EditText
final Drawable left = new ColorDrawable(Color.RED);
final Drawable top = new ColorDrawable(Color.GREEN);
final Drawable right = new ColorDrawable(Color.BLUE);
final Drawable bottom = new ColorDrawable(Color.BLACK);
final TextInputEditText editText = new TextInputEditText(activity);
editText.setCompoundDrawables(left, top, right, bottom);
// Now add the EditText to a TextInputLayout
TextInputLayout til = (TextInputLayout) activity.findViewById(R.id.textinput_noedittext);
til.addView(editText);
// Finally assert that all of the drawables are untouched
final Drawable[] compoundDrawables = editText.getCompoundDrawables();
assertSame(left, compoundDrawables[0]);
assertSame(top, compoundDrawables[1]);
assertSame(right, compoundDrawables[2]);
assertSame(bottom, compoundDrawables[3]);
}
@UiThreadTest
@Test
public void testMaintainsStartEndCompoundDrawables() throws Throwable {
final Activity activity = activityTestRule.getActivity();
// 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);
final TextInputEditText editText = new TextInputEditText(activity);
TextViewCompat.setCompoundDrawablesRelative(editText, start, top, end, bottom);
// Now add the EditText to a TextInputLayout
TextInputLayout til = (TextInputLayout) activity.findViewById(R.id.textinput_noedittext);
til.addView(editText);
// Finally assert that all of the drawables are untouched
final Drawable[] compoundDrawables = TextViewCompat.getCompoundDrawablesRelative(editText);
assertSame(start, compoundDrawables[0]);
assertSame(top, compoundDrawables[1]);
assertSame(end, compoundDrawables[2]);
assertSame(bottom, compoundDrawables[3]);
}
@Test
public void testPasswordToggleHasDefaultContentDescription() {
// Check that the TextInputLayout says that it has a content description
onView(withId(R.id.textinput_password)).check(matches(hasPasswordToggleContentDescription()));
// Check that the underlying toggle view says that it also has a content description
onView(withId(R.id.text_input_password_toggle)).check(matches(hasContentDescription()));
}
/**
* Simple test that uses AccessibilityChecks to check that the password toggle icon is
* 'accessible'.
*/
@Test
public void testPasswordToggleIsAccessible() {
onView(withId(R.id.text_input_password_toggle)).check(accessibilityAssertion());
}
@Test
public void testSetTypefaceUpdatesErrorView() {
onView(withId(R.id.textinput))
.perform(setErrorEnabled(true))
.perform(setError(ERROR_MESSAGE_1))
.perform(setTypeface(CUSTOM_TYPEFACE));
// Check that the error message is updated
onView(withText(ERROR_MESSAGE_1)).check(matches(withTypeface(CUSTOM_TYPEFACE)));
}
@Test
public void testSetTypefaceUpdatesCharacterCountView() {
// Turn on character counting
onView(withId(R.id.textinput))
.perform(setCounterEnabled(true), setCounterMaxLength(10))
.perform(setTypeface(CUSTOM_TYPEFACE));
// Check that the counter message is updated
onView(withId(R.id.textinput_counter)).check(matches(withTypeface(CUSTOM_TYPEFACE)));
}
@Test
public void testThemedColorStateListForErrorTextColor() {
final Activity activity = activityTestRule.getActivity();
final int textColor = TestUtils.getThemeAttrColor(activity, R.attr.colorAccent);
onView(withId(R.id.textinput))
.perform(setErrorEnabled(true))
.perform(setError(ERROR_MESSAGE_1))
.perform(setErrorTextAppearance(R.style.TextAppearanceWithThemedCslTextColor));
onView(withText(ERROR_MESSAGE_1)).check(matches(withTextColor(textColor)));
}
@Test
public void testFocusMovesToEditTextWithPasswordEnabled() {
// Focus the preceding EditText
onView(withId(R.id.textinput_edittext)).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
public void testTextSetViaAttributeCollapsedHint() {
onView(withId(R.id.textinput_with_text))
.check(isHintExpanded(false));
}
private static ViewAssertion isHintExpanded(final boolean expanded) {
return new ViewAssertion() {
@Override
public void check(View view, NoMatchingViewException noViewFoundException) {
assertTrue(view instanceof TextInputLayout);
assertEquals(expanded, ((TextInputLayout) view).isHintExpanded());
}
};
}
}