Skip to content

Commit

Permalink
Persist password visibility state
Browse files Browse the repository at this point in the history
Also refactor one of the existing tests to use activity
recreation instead of orientation change (which is not as
reliable)

Bug: 37930078
Bug: 35368213
Test: ./gradlew support-design:connectedCheck --info --daemon
-Pandroid.testInstrumentationRunnerArguments.package=android.support.design.widget
Change-Id: I1b9165fb11e03b9c855e118f31dee27e7dac8149
PiperOrigin-RevId: 177198734
  • Loading branch information
kirill-grouchnikov authored and dsn5ft committed Jan 11, 2018
1 parent e7f01c6 commit 68c77f0
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 43 deletions.
14 changes: 12 additions & 2 deletions lib/src/android/support/design/widget/TextInputLayout.java
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,7 @@ private void ensureBackgroundDrawableStateWorkaround() {

static class SavedState extends AbsSavedState {
CharSequence error;
boolean isPasswordToggledVisible;

SavedState(Parcelable superState) {
super(superState);
Expand All @@ -1451,12 +1452,14 @@ static class SavedState extends AbsSavedState {
SavedState(Parcel source, ClassLoader loader) {
super(source, loader);
error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
isPasswordToggledVisible = (source.readInt() == 1);
}

@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
TextUtils.writeToParcel(error, dest, flags);
dest.writeInt(isPasswordToggledVisible ? 1 : 0);
}

@Override
Expand Down Expand Up @@ -1494,6 +1497,7 @@ public Parcelable onSaveInstanceState() {
if (indicatorViewController.errorShouldBeShown()) {
ss.error = getError();
}
ss.isPasswordToggledVisible = mPasswordToggledVisible;
return ss;
}

Expand All @@ -1506,6 +1510,9 @@ protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
setError(ss.error);
if (ss.isPasswordToggledVisible) {
passwordVisibilityToggleRequested(true /* shouldSkipAnimations */);
}
requestLayout();
}

Expand Down Expand Up @@ -1600,7 +1607,7 @@ private void updatePasswordToggleView() {
new View.OnClickListener() {
@Override
public void onClick(View view) {
passwordVisibilityToggleRequested();
passwordVisibilityToggleRequested(false /* shouldSkipAnimations */);
}
});
}
Expand Down Expand Up @@ -1802,7 +1809,7 @@ public void setPasswordVisibilityToggleTintMode(@Nullable PorterDuff.Mode mode)
applyPasswordToggleTint();
}

void passwordVisibilityToggleRequested() {
private void passwordVisibilityToggleRequested(boolean shouldSkipAnimations) {
if (mPasswordToggleEnabled) {
// Store the current cursor position
final int selection = mEditText.getSelectionEnd();
Expand All @@ -1816,6 +1823,9 @@ void passwordVisibilityToggleRequested() {
}

mPasswordToggleView.setChecked(mPasswordToggledVisible);
if (shouldSkipAnimations) {
mPasswordToggleView.jumpDrawablesToCurrentState();
}

// And restore the cursor position
mEditText.setSelection(selection);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@

import android.os.Bundle;
import android.support.annotation.LayoutRes;
import android.support.v7.app.AppCompatActivity;
import android.view.WindowManager;

public abstract class BaseTestActivity extends AppCompatActivity {
/** Base activity type for all Material Components test fixtures. */
public abstract class BaseTestActivity extends RecreatableAppCompatActivity {

private boolean mDestroyed;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2017 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.testapp.base;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import java.util.concurrent.CountDownLatch;

/**
* Activity that keeps track of resume / destroy lifecycle events, as well as of the last instance
* of itself.
*/
public class RecreatableAppCompatActivity extends AppCompatActivity {
// These must be cleared after each test using clearState()
@SuppressLint("StaticFieldLeak") // Not an issue because this is test-only and gets cleared
public static RecreatableAppCompatActivity activity;

public static CountDownLatch resumedLatch;
public static CountDownLatch destroyedLatch;

public static void clearState() {
activity = null;
resumedLatch = null;
destroyedLatch = null;
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activity = this;
}

@Override
protected void onResume() {
super.onResume();
if (resumedLatch != null) {
resumedLatch.countDown();
}
}

@Override
protected void onDestroy() {
super.onDestroy();
if (destroyedLatch != null) {
destroyedLatch.countDown();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2017 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.testutils;

import static org.junit.Assert.assertTrue;

import android.os.Looper;
import android.support.design.testapp.base.RecreatableAppCompatActivity;
import android.support.test.rule.ActivityTestRule;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/** Utility methods for testing activities. */
public class ActivityUtils {
private static final Runnable DO_NOTHING =
new Runnable() {
@Override
public void run() {}
};

public static void waitForExecution(
final ActivityTestRule<? extends RecreatableAppCompatActivity> rule) {
// Wait for two cycles. When starting a postponed transition, it will post to
// the UI thread and then the execution will be added onto the queue after that.
// The two-cycle wait makes sure fragments have the opportunity to complete both
// before returning.
try {
rule.runOnUiThread(DO_NOTHING);
rule.runOnUiThread(DO_NOTHING);
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
}

private static void runOnUiThreadRethrow(
ActivityTestRule<? extends RecreatableAppCompatActivity> rule, Runnable r) {
if (Looper.getMainLooper() == Looper.myLooper()) {
r.run();
} else {
try {
rule.runOnUiThread(r);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
}
/**
* Restarts the RecreatedAppCompatActivity and waits for the new activity to be resumed.
*
* @return The newly-restarted RecreatedAppCompatActivity
*/
@SuppressWarnings("unchecked") // The type of the recreated activity is guaranteed to be T
public static <T extends RecreatableAppCompatActivity> T recreateActivity(
ActivityTestRule<? extends RecreatableAppCompatActivity> rule, final T activity)
throws InterruptedException {
// Now switch the orientation
RecreatableAppCompatActivity.resumedLatch = new CountDownLatch(1);
RecreatableAppCompatActivity.destroyedLatch = new CountDownLatch(1);
runOnUiThreadRethrow(
rule,
new Runnable() {
@Override
public void run() {
activity.recreate();
}
});
assertTrue(RecreatableAppCompatActivity.resumedLatch.await(1, TimeUnit.SECONDS));
assertTrue(RecreatableAppCompatActivity.destroyedLatch.await(1, TimeUnit.SECONDS));
T newActivity = (T) RecreatableAppCompatActivity.activity;
waitForExecution(rule);
RecreatableAppCompatActivity.clearState();
return newActivity;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
import android.graphics.Typeface;
import android.support.annotation.ColorInt;
import android.support.annotation.DimenRes;
import android.support.design.R;
import android.support.design.widget.TextInputLayout;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.test.espresso.matcher.ViewMatchers;
import android.view.View;
import org.hamcrest.Matcher;

Expand Down Expand Up @@ -364,4 +366,29 @@ public void perform(UiController uiController, View view) {
}
};
}

/** Toggles password. */
public static ViewAction clickPasswordToggle() {
return new ViewAction() {

@Override
public Matcher<View> getConstraints() {
return ViewMatchers.isAssignableFrom(TextInputLayout.class);
}

@Override
public String getDescription() {
return "Clicks the password toggle";
}

@Override
public void perform(UiController uiController, View view) {
TextInputLayout textInputLayout = (TextInputLayout) view;
// Reach in and find the password toggle since we don't have a public API
// to get a reference to it
View passwordToggle = textInputLayout.findViewById(R.id.text_input_password_toggle);
passwordToggle.performClick();
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@

package android.support.design.testutils;

import android.support.design.R;
import android.support.design.widget.CheckableImageButton;
import android.support.design.widget.TextInputLayout;
import android.text.TextUtils;
import android.view.View;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
Expand All @@ -28,17 +31,58 @@ public class TextInputLayoutMatchers {
* Returns a matcher that matches TextInputLayouts with non-empty content descriptions for the
* password toggle.
*/
public static Matcher hasPasswordToggleContentDescription() {
return new TypeSafeMatcher<TextInputLayout>() {
public static Matcher<View> passwordToggleHasContentDescription() {
return new TypeSafeMatcher<View>(TextInputLayout.class) {
@Override
public void describeTo(Description description) {
description.appendText(
"TextInputLayout has non-empty content description" + "for password toggle.");
}

@Override
protected boolean matchesSafely(TextInputLayout item) {
return !TextUtils.isEmpty(item.getPasswordVisibilityToggleContentDescription());
protected boolean matchesSafely(View view) {
TextInputLayout item = (TextInputLayout) view;
// Reach in and find the password toggle since we don't have a public API
// to get a reference to it
View passwordToggle = item.findViewById(R.id.text_input_password_toggle);
return !TextUtils.isEmpty(item.getPasswordVisibilityToggleContentDescription())
&& !TextUtils.isEmpty(passwordToggle.getContentDescription());
}
};
}

/** Returns a matcher that matches TextInputLayouts with non-displayed password toggles */
public static Matcher<View> doesNotShowPasswordToggle() {
return new TypeSafeMatcher<View>(TextInputLayout.class) {
@Override
public void describeTo(Description description) {
description.appendText("TextInputLayout shows password toggle.");
}

@Override
protected boolean matchesSafely(View item) {
// Reach in and find the password toggle since we don't have a public API
// to get a reference to it
View passwordToggle = item.findViewById(R.id.text_input_password_toggle);
return passwordToggle.getVisibility() != View.VISIBLE;
}
};
}

/** Returns a matcher that matches TextInputLayouts with non-displayed password toggles */
public static Matcher<View> passwordToggleIsNotChecked() {
return new TypeSafeMatcher<View>(TextInputLayout.class) {
@Override
public void describeTo(Description description) {
description.appendText("TextInputLayout has checked password toggle.");
}

@Override
protected boolean matchesSafely(View item) {
// Reach in and find the password toggle since we don't have a public API
// to get a reference to it
CheckableImageButton passwordToggle = item.findViewById(R.id.text_input_password_toggle);
return !passwordToggle.isChecked();
}
};
}
Expand Down
Loading

0 comments on commit 68c77f0

Please sign in to comment.