// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.ui.autofill;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.text.SpannableString;
import android.text.style.ClickableSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.content.res.ResourcesCompat;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.ui.autofill.internal.R;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogType;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.modaldialog.FakeModalDialogManager;
import java.util.Optional;
/** Unit tests for {@link OtpVerificationDialogView}. */
@RunWith(BaseRobolectricTestRunner.class)
@EnableFeatures({ChromeFeatureList.AUTOFILL_ENABLE_MOVING_GPAY_LOGO_TO_THE_RIGHT_ON_CLANK})
public class OtpVerificationDialogTest {
private static final String ERROR_MESSAGE = "Error message";
private static final String VALID_OTP = "123456";
private OtpVerificationDialogView mOtpVerificationDialogView;
private FakeModalDialogManager mModalDialogManager;
private OtpVerificationDialogCoordinator mOtpVerificationDialogCoordinator;
private Resources mResources;
@Mock private OtpVerificationDialogCoordinator.Delegate mDelegate;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mModalDialogManager = new FakeModalDialogManager(ModalDialogType.TAB);
mResources = ApplicationProvider.getApplicationContext().getResources();
mOtpVerificationDialogView =
(OtpVerificationDialogView)
LayoutInflater.from(ApplicationProvider.getApplicationContext())
.inflate(R.layout.otp_verification_dialog, null);
mOtpVerificationDialogCoordinator =
new OtpVerificationDialogCoordinator(
ApplicationProvider.getApplicationContext(),
mModalDialogManager,
mOtpVerificationDialogView,
mDelegate);
}
@Test
public void testDefaultState() {
int otpLength = 6;
mOtpVerificationDialogCoordinator.show(otpLength);
PropertyModel model = mModalDialogManager.getShownDialogModel();
View view = model.get(ModalDialogProperties.CUSTOM_VIEW);
// Verify that the error message is not shown.
assertThat(view.findViewById(R.id.otp_error_message).getVisibility()).isEqualTo(View.GONE);
// Verify that the hint shown in the OTP input field contains the value of the otpLength set
// above.
assertThat(((EditText) view.findViewById(R.id.otp_input)).getHint())
.isEqualTo(
ApplicationProvider.getApplicationContext()
.getString(
R.string
.autofill_payments_otp_verification_dialog_otp_input_hint,
otpLength));
// Verify that the positive button is disabled.
assertThat(model.get(ModalDialogProperties.POSITIVE_BUTTON_DISABLED)).isTrue();
}
@Test
public void testShowHideErrorMessage() {
mOtpVerificationDialogCoordinator.show(/* otpLength= */ 6);
mOtpVerificationDialogView.showOtpErrorMessage(Optional.of(ERROR_MESSAGE));
PropertyModel model = mModalDialogManager.getShownDialogModel();
View view = model.get(ModalDialogProperties.CUSTOM_VIEW);
// Verify that the error message is shown.
TextView errorMessageTextView = (TextView) view.findViewById(R.id.otp_error_message);
assertThat(errorMessageTextView.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(errorMessageTextView.getText()).isEqualTo(ERROR_MESSAGE);
// Verify that editing the error test, hides the error message.
EditText otpInputEditText = (EditText) view.findViewById(R.id.otp_input);
otpInputEditText.setText("123");
assertThat(errorMessageTextView.getVisibility()).isEqualTo(View.GONE);
}
@Test
public void testPositiveButtonDisabledState() {
mOtpVerificationDialogCoordinator.show(/* otpLength= */ 6);
PropertyModel model = mModalDialogManager.getShownDialogModel();
View view = model.get(ModalDialogProperties.CUSTOM_VIEW);
EditText otpInputEditText = (EditText) view.findViewById(R.id.otp_input);
// Verify that the positive button is disabled for input length < otpLength.
otpInputEditText.setText("123");
assertThat(model.get(ModalDialogProperties.POSITIVE_BUTTON_DISABLED)).isTrue();
// Verify that the positive button is enabled for input length == otpLength.
otpInputEditText.setText("123456");
assertThat(model.get(ModalDialogProperties.POSITIVE_BUTTON_DISABLED)).isFalse();
// Verify that the positive button is disabled for input length > otpLength.
otpInputEditText.setText("1234567");
assertThat(model.get(ModalDialogProperties.POSITIVE_BUTTON_DISABLED)).isTrue();
}
@Test
public void testOtpSubmission() {
mOtpVerificationDialogCoordinator.show(/* otpLength= */ 6);
PropertyModel model = mModalDialogManager.getShownDialogModel();
View view = model.get(ModalDialogProperties.CUSTOM_VIEW);
EditText otpInputEditText = (EditText) view.findViewById(R.id.otp_input);
otpInputEditText.setText(VALID_OTP);
mModalDialogManager.clickPositiveButton();
// Verify that the listener is called with the text entered in the OTP input field.
verify(mDelegate, times(1)).onConfirm(VALID_OTP);
// Verify that the progress bar is shown.
assertThat(view.findViewById(R.id.progress_bar_overlay).getVisibility())
.isEqualTo(View.VISIBLE);
}
@Test
public void testGetNewCode() {
mOtpVerificationDialogCoordinator.show(/* otpLength= */ 6);
PropertyModel model = mModalDialogManager.getShownDialogModel();
View view = model.get(ModalDialogProperties.CUSTOM_VIEW);
TextView otpResendMessageTextView = (TextView) view.findViewById(R.id.otp_resend_message);
SpannableString otpResendMessage = (SpannableString) otpResendMessageTextView.getText();
ClickableSpan getNewCodeSpan =
otpResendMessage.getSpans(0, otpResendMessage.length(), ClickableSpan.class)[0];
getNewCodeSpan.onClick(otpResendMessageTextView);
verify(mDelegate, times(1)).onNewOtpRequested();
}
@Test
public void testDialogDismissal() {
mOtpVerificationDialogCoordinator.show(/* otpLength= */ 6);
mModalDialogManager.dismissDialog(
mModalDialogManager.getShownDialogModel(),
DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
verify(mDelegate, times(1)).onDialogDismissed();
}
@Test
@DisableFeatures({ChromeFeatureList.AUTOFILL_ENABLE_MOVING_GPAY_LOGO_TO_THE_RIGHT_ON_CLANK})
public void testDefaultTitleView() throws Exception {
mOtpVerificationDialogCoordinator.show(/* otpLength= */ 6);
PropertyModel model = mModalDialogManager.getShownDialogModel();
assertThat(model).isNotNull();
// Verify that the title set by modal dialog is correct.
assertThat(model.get(ModalDialogProperties.TITLE))
.isEqualTo(
mResources.getString(R.string.autofill_card_unmask_otp_input_dialog_title));
// Verify that the title icon set by modal dialog is correct.
Drawable expectedDrawable =
ResourcesCompat.getDrawable(
mResources,
R.drawable.google_pay_with_divider,
ApplicationProvider.getApplicationContext().getTheme());
assertTrue(
getBitmap(expectedDrawable)
.sameAs(getBitmap(model.get(ModalDialogProperties.TITLE_ICON))));
// Verify that title and title icon is not set by custom view.
View customView = model.get(ModalDialogProperties.CUSTOM_VIEW);
assertThat((TextView) customView.findViewById(R.id.title)).isNull();
assertThat((ImageView) customView.findViewById(R.id.title_icon)).isNull();
}
@Test
public void testCustomTitleView() throws Exception {
mOtpVerificationDialogCoordinator.show(/* otpLength= */ 6);
PropertyModel model = mModalDialogManager.getShownDialogModel();
assertThat(model).isNotNull();
View customView = model.get(ModalDialogProperties.CUSTOM_VIEW);
// Verify that the title set by custom view is correct.
TextView title = (TextView) customView.findViewById(R.id.title);
assertThat(title.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(title.getText())
.isEqualTo(
mResources.getString(R.string.autofill_card_unmask_otp_input_dialog_title));
// Verify that the title icon set by custom view is correct.
ImageView title_icon = (ImageView) customView.findViewById(R.id.title_icon);
Drawable expectedDrawable =
ResourcesCompat.getDrawable(
mResources,
R.drawable.google_pay,
ApplicationProvider.getApplicationContext().getTheme());
assertThat(title_icon.getVisibility()).isEqualTo(View.VISIBLE);
assertTrue(getBitmap(expectedDrawable).sameAs(getBitmap(title_icon.getDrawable())));
// Verify that title and title icon is not set by modal dialog.
assertThat(model.get(ModalDialogProperties.TITLE)).isNull();
assertThat(model.get(ModalDialogProperties.TITLE_ICON)).isNull();
}
// Convert a drawable to a Bitmap for comparison.
private static Bitmap getBitmap(Drawable drawable) {
Bitmap bitmap =
Bitmap.createBitmap(
drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}