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