1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.design.widget;
18
19import static android.support.design.testutils.TestUtilsActions.setCompoundDrawablesRelative;
20import static android.support.design.testutils.TestUtilsActions.setEnabled;
21import static android.support.design.testutils.TestUtilsMatchers.withCompoundDrawable;
22import static android.support.design.testutils.TestUtilsMatchers.withTextColor;
23import static android.support.design.testutils.TestUtilsMatchers.withTypeface;
24import static android.support.design.testutils.TextInputLayoutActions.clickPasswordToggle;
25import static android.support.design.testutils.TextInputLayoutActions.setCounterEnabled;
26import static android.support.design.testutils.TextInputLayoutActions.setCounterMaxLength;
27import static android.support.design.testutils.TextInputLayoutActions.setError;
28import static android.support.design.testutils.TextInputLayoutActions.setErrorEnabled;
29import static android.support.design.testutils.TextInputLayoutActions.setErrorTextAppearance;
30import static android.support.design.testutils.TextInputLayoutActions
31        .setPasswordVisibilityToggleEnabled;
32import static android.support.design.testutils.TextInputLayoutActions.setTypeface;
33import static android.support.design.testutils.TextInputLayoutMatchers.doesNotShowPasswordToggle;
34import static android.support.design.testutils.TextInputLayoutMatchers
35        .passwordToggleHasContentDescription;
36import static android.support.design.testutils.TextInputLayoutMatchers.passwordToggleIsNotChecked;
37import static android.support.test.InstrumentationRegistry.getInstrumentation;
38import static android.support.test.espresso.Espresso.onView;
39import static android.support.test.espresso.action.ViewActions.click;
40import static android.support.test.espresso.action.ViewActions.typeText;
41import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
42import static android.support.test.espresso.assertion.ViewAssertions.matches;
43import static android.support.test.espresso.contrib.AccessibilityChecks.accessibilityAssertion;
44import static android.support.test.espresso.matcher.ViewMatchers.hasFocus;
45import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
46import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
47import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
48import static android.support.test.espresso.matcher.ViewMatchers.withId;
49import static android.support.test.espresso.matcher.ViewMatchers.withText;
50
51import static org.hamcrest.Matchers.not;
52import static org.hamcrest.core.AllOf.allOf;
53import static org.junit.Assert.assertEquals;
54import static org.junit.Assert.assertNotEquals;
55import static org.junit.Assert.assertNull;
56import static org.junit.Assert.assertSame;
57import static org.junit.Assert.assertTrue;
58
59import android.app.Activity;
60import android.content.Context;
61import android.graphics.Color;
62import android.graphics.Typeface;
63import android.graphics.drawable.ColorDrawable;
64import android.graphics.drawable.Drawable;
65import android.os.Build;
66import android.os.Parcelable;
67import android.support.design.test.R;
68import android.support.design.testutils.ActivityUtils;
69import android.support.design.testutils.RecreatedAppCompatActivity;
70import android.support.design.testutils.TestUtils;
71import android.support.design.testutils.ViewStructureImpl;
72import android.support.test.annotation.UiThreadTest;
73import android.support.test.espresso.NoMatchingViewException;
74import android.support.test.espresso.ViewAssertion;
75import android.support.test.filters.LargeTest;
76import android.support.test.filters.MediumTest;
77import android.support.test.filters.SdkSuppress;
78import android.support.v4.widget.TextViewCompat;
79import android.text.method.PasswordTransformationMethod;
80import android.text.method.TransformationMethod;
81import android.util.AttributeSet;
82import android.util.SparseArray;
83import android.view.KeyEvent;
84import android.view.View;
85import android.view.inputmethod.EditorInfo;
86import android.widget.EditText;
87
88import org.junit.Test;
89
90@MediumTest
91public class TextInputLayoutTest extends BaseInstrumentationTestCase<TextInputLayoutActivity> {
92
93    private static final String ERROR_MESSAGE_1 = "An error has occured";
94    private static final String ERROR_MESSAGE_2 = "Some other error has occured";
95
96    private static final String INPUT_TEXT = "Random input text";
97
98    private static final Typeface CUSTOM_TYPEFACE = Typeface.SANS_SERIF;
99
100    public class TestTextInputLayout extends TextInputLayout {
101        public int animateToExpansionFractionCount = 0;
102        public float animateToExpansionFractionRecentValue = -1;
103
104        public TestTextInputLayout(Context context) {
105            super(context);
106        }
107
108        public TestTextInputLayout(Context context, AttributeSet attrs) {
109            super(context, attrs);
110        }
111
112        public TestTextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
113            super(context, attrs, defStyleAttr);
114        }
115
116        @Override
117        protected void animateToExpansionFraction(float target) {
118            super.animateToExpansionFraction(target);
119            animateToExpansionFractionRecentValue = target;
120            animateToExpansionFractionCount++;
121        }
122    }
123
124    public TextInputLayoutTest() {
125        super(TextInputLayoutActivity.class);
126    }
127
128    @Test
129    public void testTypingTextCollapsesHint() {
130        // Type some text
131        onView(withId(R.id.textinput_edittext)).perform(typeText(INPUT_TEXT));
132        // ...and check that the hint has collapsed
133        onView(withId(R.id.textinput)).check(isHintExpanded(false));
134    }
135
136    @Test
137    public void testSetErrorEnablesErrorIsDisplayed() {
138        onView(withId(R.id.textinput)).perform(setError(ERROR_MESSAGE_1));
139        onView(withText(ERROR_MESSAGE_1)).check(matches(isDisplayed()));
140    }
141
142    @Test
143    public void testDisabledErrorIsNotDisplayed() {
144        // First show an error, and then disable error functionality
145        onView(withId(R.id.textinput))
146                .perform(setError(ERROR_MESSAGE_1))
147                .perform(setErrorEnabled(false));
148
149        // Check that the error is no longer there
150        onView(withText(ERROR_MESSAGE_1)).check(doesNotExist());
151    }
152
153    @Test
154    public void testSetErrorOnDisabledSetErrorIsDisplayed() {
155        // First show an error, and then disable error functionality
156        onView(withId(R.id.textinput))
157                .perform(setError(ERROR_MESSAGE_1))
158                .perform(setErrorEnabled(false));
159
160        // Now show a different error message
161        onView(withId(R.id.textinput)).perform(setError(ERROR_MESSAGE_2));
162        // And check that it is displayed
163        onView(withText(ERROR_MESSAGE_2)).check(matches(isDisplayed()));
164    }
165
166    @Test
167    public void testPasswordToggleClick() {
168        // Type some text on the EditText
169        onView(withId(R.id.textinput_edittext_pwd)).perform(typeText(INPUT_TEXT));
170
171        final Activity activity = mActivityTestRule.getActivity();
172        final EditText textInput = (EditText) activity.findViewById(R.id.textinput_edittext_pwd);
173
174        // Assert that the password is disguised
175        assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
176
177        // Now click the toggle button
178        onView(withId(R.id.textinput_password)).perform(clickPasswordToggle());
179
180        // And assert that the password is not disguised
181        assertEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
182    }
183
184    @Test
185    public void testPasswordToggleDisable() {
186        final Activity activity = mActivityTestRule.getActivity();
187        final EditText textInput = (EditText) activity.findViewById(R.id.textinput_edittext_pwd);
188
189        // Set some text on the EditText
190        onView(withId(R.id.textinput_edittext_pwd))
191                .perform(typeText(INPUT_TEXT));
192        // Assert that the password is disguised
193        assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
194
195        // Disable the password toggle
196        onView(withId(R.id.textinput_password))
197                .perform(setPasswordVisibilityToggleEnabled(false));
198
199        // Check that the password toggle view is not visible
200        onView(withId(R.id.textinput_password)).check(matches(doesNotShowPasswordToggle()));
201        // ...and that the password is disguised still
202        assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
203    }
204
205    @Test
206    public void testPasswordToggleDisableWhenVisible() {
207        final Activity activity = mActivityTestRule.getActivity();
208        final EditText textInput = (EditText) activity.findViewById(R.id.textinput_edittext_pwd);
209
210        // Type some text on the EditText
211        onView(withId(R.id.textinput_edittext_pwd)).perform(typeText(INPUT_TEXT));
212        // Assert that the password is disguised
213        assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
214
215        // Now click the toggle button
216        onView(withId(R.id.textinput_password)).perform(clickPasswordToggle());
217        // Disable the password toggle
218        onView(withId(R.id.textinput_password))
219                .perform(setPasswordVisibilityToggleEnabled(false));
220
221        // Check that the password is disguised again
222        assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
223    }
224
225    @Test
226    public void testPasswordToggleMaintainsCompoundDrawables() {
227        // Set a known set of test compound drawables on the EditText
228        final Drawable start = new ColorDrawable(Color.RED);
229        final Drawable top = new ColorDrawable(Color.GREEN);
230        final Drawable end = new ColorDrawable(Color.BLUE);
231        final Drawable bottom = new ColorDrawable(Color.BLACK);
232        onView(withId(R.id.textinput_edittext_pwd))
233                .perform(setCompoundDrawablesRelative(start, top, end, bottom));
234
235        // Enable the password toggle and check that the start, top and bottom drawables are
236        // maintained
237        onView(withId(R.id.textinput_password))
238                .perform(setPasswordVisibilityToggleEnabled(true));
239        onView(withId(R.id.textinput_edittext_pwd))
240                .check(matches(withCompoundDrawable(0, start)))
241                .check(matches(withCompoundDrawable(1, top)))
242                .check(matches(not(withCompoundDrawable(2, end))))
243                .check(matches(withCompoundDrawable(3, bottom)));
244
245        // Now disable the password toggle and check that all of the original compound drawables
246        // are set
247        onView(withId(R.id.textinput_password))
248                .perform(setPasswordVisibilityToggleEnabled(false));
249        onView(withId(R.id.textinput_edittext_pwd))
250                .check(matches(withCompoundDrawable(0, start)))
251                .check(matches(withCompoundDrawable(1, top)))
252                .check(matches(withCompoundDrawable(2, end)))
253                .check(matches(withCompoundDrawable(3, bottom)));
254    }
255
256    @Test
257    public void testPasswordToggleIsHiddenAfterReenable() {
258        final Activity activity = mActivityTestRule.getActivity();
259        final EditText textInput = (EditText) activity.findViewById(R.id.textinput_edittext_pwd);
260
261        // Type some text on the EditText and then click the toggle button
262        onView(withId(R.id.textinput_edittext_pwd)).perform(typeText(INPUT_TEXT));
263        onView(withId(R.id.textinput_password)).perform(clickPasswordToggle());
264
265        // Disable the password toggle, and then re-enable it
266        onView(withId(R.id.textinput_password))
267                .perform(setPasswordVisibilityToggleEnabled(false))
268                .perform(setPasswordVisibilityToggleEnabled(true));
269
270        // Check that the password is disguised and the toggle button reflects the same state
271        assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
272        onView(withId(R.id.textinput_password)).check(matches(passwordToggleIsNotChecked()));
273    }
274
275    @Test
276    public void testSetEnabledFalse() {
277        // First click on the EditText, so that it is focused and the hint collapses...
278        onView(withId(R.id.textinput_edittext)).perform(click());
279
280        // Now disable the TextInputLayout and check that the hint expands
281        onView(withId(R.id.textinput))
282                .perform(setEnabled(false))
283                .check(isHintExpanded(true));
284
285        // Finally check that the EditText is no longer enabled
286        onView(withId(R.id.textinput_edittext)).check(matches(not(isEnabled())));
287    }
288
289    @Test
290    public void testSetEnabledFalseWithText() {
291        // First set some text, then disable the TextInputLayout
292        onView(withId(R.id.textinput_edittext))
293                .perform(typeText(INPUT_TEXT));
294        onView(withId(R.id.textinput)).perform(setEnabled(false));
295
296        // Now check that the EditText is no longer enabled
297        onView(withId(R.id.textinput_edittext)).check(matches(not(isEnabled())));
298    }
299
300    @UiThreadTest
301    @Test
302    public void testExtractUiHintSet() {
303        final Activity activity = mActivityTestRule.getActivity();
304
305        // Set a hint on the TextInputLayout
306        final TextInputLayout layout = (TextInputLayout) activity.findViewById(R.id.textinput);
307        layout.setHint(INPUT_TEXT);
308
309        final EditText editText = (EditText) activity.findViewById(R.id.textinput_edittext);
310
311        // Now manually pass in a EditorInfo to the EditText and make sure it updates the
312        // hintText to our known value
313        final EditorInfo info = new EditorInfo();
314        editText.onCreateInputConnection(info);
315
316        assertEquals(INPUT_TEXT, info.hintText);
317    }
318
319    @UiThreadTest
320    @Test
321    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
322    public void testDispatchProvideAutofillStructure() {
323        final Activity activity = mActivityTestRule.getActivity();
324
325        final TextInputLayout layout = activity.findViewById(R.id.textinput);
326
327        final ViewStructureImpl structure = new ViewStructureImpl();
328        layout.dispatchProvideAutofillStructure(structure, 0);
329
330        assertEquals(2, structure.getChildCount()); // EditText and TextView
331
332        // Asserts the structure.
333        final ViewStructureImpl childStructure = structure.getChildAt(0);
334        assertEquals(EditText.class.getName(), childStructure.getClassName());
335        assertEquals("Hint to the user", childStructure.getHint());
336
337        // Make sure the widget's hint was restored.
338        assertEquals("Hint to the user", layout.getHint());
339        final EditText editText = activity.findViewById(R.id.textinput_edittext);
340        assertNull(editText.getHint());
341    }
342
343    /**
344     * Regression test for b/31663756.
345     */
346    @UiThreadTest
347    @Test
348    public void testDrawableStateChanged() {
349        final Activity activity = mActivityTestRule.getActivity();
350        final TextInputLayout layout = (TextInputLayout) activity.findViewById(R.id.textinput);
351
352        // Force a drawable state change.
353        layout.drawableStateChanged();
354    }
355
356    @UiThreadTest
357    @Test
358    public void testSaveRestoreStateAnimation() {
359        final Activity activity = mActivityTestRule.getActivity();
360        final TestTextInputLayout layout = new TestTextInputLayout(activity);
361        layout.setId(R.id.textinputlayout);
362        final TextInputEditText editText = new TextInputEditText(activity);
363        editText.setText(INPUT_TEXT);
364        editText.setId(R.id.textinputedittext);
365        layout.addView(editText);
366
367        SparseArray<Parcelable> container = new SparseArray<>();
368        layout.saveHierarchyState(container);
369        layout.restoreHierarchyState(container);
370        assertEquals("Expected no animations since we simply saved/restored state",
371                0, layout.animateToExpansionFractionCount);
372
373        editText.setText("");
374        assertEquals("Expected one call to animate because we cleared text in editText",
375                1, layout.animateToExpansionFractionCount);
376        assertEquals(0f, layout.animateToExpansionFractionRecentValue, 0f);
377
378        container = new SparseArray<>();
379        layout.saveHierarchyState(container);
380        layout.restoreHierarchyState(container);
381        assertEquals("Expected no additional animations since we simply saved/restored state",
382                1, layout.animateToExpansionFractionCount);
383    }
384
385    @UiThreadTest
386    @Test
387    public void testMaintainsLeftRightCompoundDrawables() throws Throwable {
388        final Activity activity = mActivityTestRule.getActivity();
389
390        // Set a known set of test compound drawables on the EditText
391        final Drawable left = new ColorDrawable(Color.RED);
392        final Drawable top = new ColorDrawable(Color.GREEN);
393        final Drawable right = new ColorDrawable(Color.BLUE);
394        final Drawable bottom = new ColorDrawable(Color.BLACK);
395
396        final TextInputEditText editText = new TextInputEditText(activity);
397        editText.setCompoundDrawables(left, top, right, bottom);
398
399        // Now add the EditText to a TextInputLayout
400        TextInputLayout til = (TextInputLayout)
401                activity.findViewById(R.id.textinput_noedittext);
402        til.addView(editText);
403
404        // Finally assert that all of the drawables are untouched
405        final Drawable[] compoundDrawables = editText.getCompoundDrawables();
406        assertSame(left, compoundDrawables[0]);
407        assertSame(top, compoundDrawables[1]);
408        assertSame(right, compoundDrawables[2]);
409        assertSame(bottom, compoundDrawables[3]);
410    }
411
412    @UiThreadTest
413    @Test
414    public void testMaintainsStartEndCompoundDrawables() throws Throwable {
415        final Activity activity = mActivityTestRule.getActivity();
416
417        // Set a known set of test compound drawables on the EditText
418        final Drawable start = new ColorDrawable(Color.RED);
419        final Drawable top = new ColorDrawable(Color.GREEN);
420        final Drawable end = new ColorDrawable(Color.BLUE);
421        final Drawable bottom = new ColorDrawable(Color.BLACK);
422
423        final TextInputEditText editText = new TextInputEditText(activity);
424        TextViewCompat.setCompoundDrawablesRelative(editText, start, top, end, bottom);
425
426        // Now add the EditText to a TextInputLayout
427        TextInputLayout til = (TextInputLayout)
428                activity.findViewById(R.id.textinput_noedittext);
429        til.addView(editText);
430
431        // Finally assert that all of the drawables are untouched
432        final Drawable[] compoundDrawables = TextViewCompat.getCompoundDrawablesRelative(editText);
433        assertSame(start, compoundDrawables[0]);
434        assertSame(top, compoundDrawables[1]);
435        assertSame(end, compoundDrawables[2]);
436        assertSame(bottom, compoundDrawables[3]);
437    }
438
439    @Test
440    public void testPasswordToggleHasDefaultContentDescription() {
441        // Check that the TextInputLayout says that it has a content description and that the
442        // underlying toggle has content description as well
443        onView(withId(R.id.textinput_password))
444                .check(matches(passwordToggleHasContentDescription()));
445    }
446
447    /**
448     * Simple test that uses AccessibilityChecks to check that the password toggle icon is
449     * 'accessible'.
450     */
451    @Test
452    public void testPasswordToggleIsAccessible() {
453        onView(allOf(withId(R.id.text_input_password_toggle),
454                isDescendantOfA(withId(R.id.textinput_password)))).check(accessibilityAssertion());
455    }
456
457    @Test
458    public void testSetTypefaceUpdatesErrorView() {
459        onView(withId(R.id.textinput))
460                .perform(setErrorEnabled(true))
461                .perform(setError(ERROR_MESSAGE_1))
462                .perform(setTypeface(CUSTOM_TYPEFACE));
463
464        // Check that the error message is updated
465        onView(withText(ERROR_MESSAGE_1))
466                .check(matches(withTypeface(CUSTOM_TYPEFACE)));
467    }
468
469    @Test
470    public void testSetTypefaceUpdatesCharacterCountView() {
471        // Turn on character counting
472        onView(withId(R.id.textinput))
473                .perform(setCounterEnabled(true), setCounterMaxLength(10))
474                .perform(setTypeface(CUSTOM_TYPEFACE));
475
476        // Check that the counter message is updated
477        onView(withId(R.id.textinput_counter))
478                .check(matches(withTypeface(CUSTOM_TYPEFACE)));
479    }
480
481    @Test
482    public void testThemedColorStateListForErrorTextColor() {
483        final Activity activity = mActivityTestRule.getActivity();
484        final int textColor = TestUtils.getThemeAttrColor(activity, R.attr.colorAccent);
485
486        onView(withId(R.id.textinput))
487                .perform(setErrorEnabled(true))
488                .perform(setError(ERROR_MESSAGE_1))
489                .perform(setErrorTextAppearance(R.style.TextAppearanceWithThemedCslTextColor));
490
491        onView(withText(ERROR_MESSAGE_1))
492                .check(matches(withTextColor(textColor)));
493    }
494
495    @Test
496    public void testTextSetViaAttributeCollapsedHint() {
497        onView(withId(R.id.textinput_with_text)).check(isHintExpanded(false));
498    }
499
500    @Test
501    public void testFocusMovesToEditTextWithPasswordEnabled() {
502        // Focus the preceding EditText
503        onView(withId(R.id.textinput_edittext))
504                .perform(click())
505                .check(matches(hasFocus()));
506
507        // Then send a TAB to focus the next view
508        getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
509
510        // And check that the EditText is focused
511        onView(withId(R.id.textinput_edittext_pwd))
512                .check(matches(hasFocus()));
513    }
514
515    @Test
516    @LargeTest
517    public void testSaveAndRestorePasswordVisibility() throws Throwable {
518        // Type some text on the EditText
519        onView(withId(R.id.textinput_edittext_pwd)).perform(typeText(INPUT_TEXT));
520        onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(false));
521
522        // Toggle password to be shown as plain text
523        onView(withId(R.id.textinput_password)).perform(clickPasswordToggle());
524        onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(true));
525
526        RecreatedAppCompatActivity activity = mActivityTestRule.getActivity();
527        activity = ActivityUtils.recreateActivity(mActivityTestRule, activity);
528        ActivityUtils.waitForExecution(mActivityTestRule);
529
530        // Check that the password is still toggled to be shown as plain text
531        onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(true));
532    }
533
534    static ViewAssertion isHintExpanded(final boolean expanded) {
535        return new ViewAssertion() {
536            @Override
537            public void check(View view, NoMatchingViewException noViewFoundException) {
538                assertTrue(view instanceof TextInputLayout);
539                assertEquals(expanded, ((TextInputLayout) view).isHintExpanded());
540            }
541        };
542    }
543
544    static ViewAssertion isPasswordToggledVisible(final boolean isToggledVisible) {
545        return new ViewAssertion() {
546            @Override
547            public void check(View view, NoMatchingViewException noViewFoundException) {
548                assertTrue(view instanceof TextInputLayout);
549                EditText editText = ((TextInputLayout) view).getEditText();
550                TransformationMethod transformationMethod = editText.getTransformationMethod();
551                if (isToggledVisible) {
552                    assertNull(transformationMethod);
553                } else {
554                    assertEquals(PasswordTransformationMethod.getInstance(), transformationMethod);
555                }
556            }
557        };
558    }
559}
560