chromium/chrome/browser/autofill/android/junit/src/org/chromium/chrome/browser/autofill/editors/TextFieldViewUnitTest.java

// Copyright 2023 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.autofill.editors;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.IS_REQUIRED;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.LABEL;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.VALIDATOR;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.VALUE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.TextFieldProperties.TEXT_ALL_KEYS;

import android.app.Activity;
import android.text.Editable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AutoCompleteTextView;
import android.widget.LinearLayout;

import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.text.EmptyTextWatcher;

/** Unit test for {@link TextFieldView}. */
@RunWith(BaseRobolectricTestRunner.class)
public final class TextFieldViewUnitTest {
    private Activity mActivity;
    private ViewGroup mContentView;
    private View mOtherFocusableField;

    @Before
    public void setUp() {
        mActivity = Robolectric.setupActivity(Activity.class);
        mContentView = new LinearLayout(mActivity);
        mOtherFocusableField = new TextInputEditText(mActivity);
        mContentView.addView(mOtherFocusableField);
    }

    private PropertyModel buildDefaultPropertyModel() {
        return new PropertyModel.Builder(TEXT_ALL_KEYS)
                .with(LABEL, "label")
                .with(VALUE, "value")
                .build();
    }

    private TextFieldView attachTextFieldView(PropertyModel model) {
        TextFieldView field = new TextFieldView(mActivity, model);
        mContentView.addView(field);
        PropertyModelChangeProcessor.create(
                model, field, EditorDialogViewBinder::bindTextFieldView);
        return field;
    }

    /**
     * Test that the text field does not show an error message if there are no validation issues.
     */
    @Test
    public void testNoError() {
        PropertyModel model = buildDefaultPropertyModel();
        model.set(IS_REQUIRED, true);
        model.set(
                VALIDATOR,
                EditorFieldValidator.builder()
                        .withRequiredErrorMessage("required error message")
                        .build());
        TextFieldView field = attachTextFieldView(model);

        assertTrue(field.validate());
        assertNull(field.getInputLayoutForTesting().getError());
    }

    /** Test the error message is hidden when the user edits the field. */
    @Test
    public void testEditHideErrorMessage() {
        PropertyModel model = buildDefaultPropertyModel();
        model.set(
                VALIDATOR,
                EditorFieldValidator.builder()
                        .withValidationPredicate(
                                (value) -> {
                                    return false;
                                },
                                "Error Message")
                        .build());
        TextFieldView field = attachTextFieldView(model);

        AutoCompleteTextView fieldEditText = field.getEditText();
        TextInputLayout inputLayout = field.getInputLayoutForTesting();

        // Set and check initial state
        assertFalse(field.validate());
        fieldEditText.requestFocus();
        assertTrue(fieldEditText.hasFocus());
        assertTrue(inputLayout.getError() != null);

        // Editing field should clear error message.
        fieldEditText.setText("edited");
        assertNull(inputLayout.getError());
    }

    /**
     * Test that the error message is shown again when the user focuses a different field after
     * editing it if the field is still invalid.
     */
    @Test
    public void testUnfocusFieldStillInvalid() {
        PropertyModel model = buildDefaultPropertyModel();
        model.set(
                VALIDATOR,
                EditorFieldValidator.builder()
                        .withValidationPredicate(
                                (value) -> {
                                    return false;
                                },
                                "Error Message")
                        .build());
        TextFieldView field = attachTextFieldView(model);

        AutoCompleteTextView fieldEditText = field.getEditText();
        TextInputLayout inputLayout = field.getInputLayoutForTesting();

        // Set and check initial state
        assertFalse(field.validate());
        fieldEditText.requestFocus();
        fieldEditText.setText("edited");
        assertTrue(fieldEditText.hasFocus());
        assertNull(inputLayout.getError());

        // Error should be shown again when field loses focus.
        mOtherFocusableField.requestFocus();
        assertFalse(fieldEditText.hasFocus());
        assertTrue(inputLayout.getError() != null);
    }

    /**
     * Test that the error message is hidden when a field loses focuses when a user corrects the
     * field's invalid value.
     */
    @Test
    public void testUnfocusFieldNowValid() {
        PropertyModel model = buildDefaultPropertyModel();
        model.set(
                VALIDATOR,
                EditorFieldValidator.builder()
                        .withValidationPredicate(
                                (value) -> {
                                    return "valid".equals(value);
                                },
                                "Error Message")
                        .build());
        TextFieldView field = attachTextFieldView(model);

        AutoCompleteTextView fieldEditText = field.getEditText();
        TextInputLayout inputLayout = field.getInputLayoutForTesting();

        // Set and check initial state
        assertFalse(field.validate());
        fieldEditText.requestFocus();
        assertTrue(fieldEditText.hasFocus());
        assertTrue(inputLayout.getError() != null);

        fieldEditText.setText("valid");
        mOtherFocusableField.requestFocus();
        assertFalse(fieldEditText.hasFocus());
        assertNull(inputLayout.getError());
    }

    /**
     * Test that the initial error message is not hidden if the user focuses the field but does not
     * edit it.
     */
    @Test
    public void testInitialErrorMessageValueNotEdited() {
        PropertyModel model = buildDefaultPropertyModel();
        model.set(
                VALIDATOR,
                EditorFieldValidator.builder()
                        .withInitialErrorMessage("Initial error message")
                        .build());
        TextFieldView field = attachTextFieldView(model);

        AutoCompleteTextView fieldEditText = field.getEditText();
        TextInputLayout inputLayout = field.getInputLayoutForTesting();

        // Set and check initial state
        assertFalse(field.validate());
        fieldEditText.requestFocus();
        assertTrue(fieldEditText.hasFocus());
        assertTrue(inputLayout.getError() != null);

        mOtherFocusableField.requestFocus();
        assertFalse(fieldEditText.hasFocus());
        assertTrue(inputLayout.getError() != null);
    }

    /**
     * Test that the initial error message is hidden when a field loses focus if the user edits the
     * value.
     */
    @Test
    public void testInitialErrorMessageValueEdited() {
        PropertyModel model = buildDefaultPropertyModel();
        model.set(
                VALIDATOR,
                EditorFieldValidator.builder()
                        .withInitialErrorMessage("Initial error message")
                        .build());
        TextFieldView field = attachTextFieldView(model);

        AutoCompleteTextView fieldEditText = field.getEditText();
        TextInputLayout inputLayout = field.getInputLayoutForTesting();

        // Set and check initial state
        assertFalse(field.validate());
        fieldEditText.requestFocus();
        assertTrue(fieldEditText.hasFocus());
        assertTrue(inputLayout.getError() != null);

        fieldEditText.setText("other");
        mOtherFocusableField.requestFocus();
        assertFalse(fieldEditText.hasFocus());
        assertNull(inputLayout.getError());
    }

    /**
     * Test that the initial error message is not hidden if a field's value is programmatically
     * changed via {@link TextFieldView#setValue()} while it has focus.
     */
    @Test
    public void testInitialErrorMessageSetValue() {
        PropertyModel model = buildDefaultPropertyModel();
        model.set(
                VALIDATOR,
                EditorFieldValidator.builder()
                        .withInitialErrorMessage("Initial error message")
                        .build());
        final TextFieldView field = attachTextFieldView(model);

        // Set text formatter in order to test nested calls of {@link TextFieldView#setValue(()}.
        field.setTextFormatter(
                new EmptyTextWatcher() {
                    @Override
                    public void afterTextChanged(Editable s) {
                        if (!s.toString().startsWith("formatted")) {
                            s.insert(0, "formatted");
                        }
                    }
                });

        AutoCompleteTextView fieldEditText = field.getEditText();
        TextInputLayout inputLayout = field.getInputLayoutForTesting();

        // Set and check initial state
        assertFalse(field.validate());
        fieldEditText.requestFocus();
        assertTrue(fieldEditText.hasFocus());
        assertTrue(inputLayout.getError() != null);

        field.setValue("other");
        mOtherFocusableField.requestFocus();
        assertFalse(fieldEditText.hasFocus());
        assertTrue(inputLayout.getError() != null);
    }

    /**
     * test that calling {@link #validate()} multiple times does not clear the initial error
     * message.
     */
    @Test
    public void testValidatingMultipleTimesInitialMessage() {
        PropertyModel model = buildDefaultPropertyModel();
        model.set(
                VALIDATOR,
                EditorFieldValidator.builder()
                        .withInitialErrorMessage("Initial error message")
                        .build());
        TextFieldView field = attachTextFieldView(model);

        TextInputLayout inputLayout = field.getInputLayoutForTesting();

        // Set and check initial state
        assertFalse(field.validate());
        assertTrue(inputLayout.getError() != null);

        // Calling {@link #validate()} multiple times should have no effect.
        assertFalse(field.validate());
        assertTrue(inputLayout.getError() != null);
    }

    /**
     * Test that calling {@link #validate()} shows the error message in the case that the error
     * message is hidden by the user editing the field.
     *
     * <p>This behavior is needed when validation is triggered by submitting the form via the
     * virtual keyboard.
     */
    @Test
    public void testValidateShowsErrorMessage() {
        PropertyModel model = buildDefaultPropertyModel();
        model.set(
                VALIDATOR,
                EditorFieldValidator.builder()
                        .withValidationPredicate(
                                (value) -> {
                                    return false;
                                },
                                "Error Message")
                        .build());
        TextFieldView field = attachTextFieldView(model);

        AutoCompleteTextView fieldEditText = field.getEditText();
        TextInputLayout inputLayout = field.getInputLayoutForTesting();

        // Set and check initial state
        assertFalse(field.validate());
        fieldEditText.requestFocus();
        assertTrue(fieldEditText.hasFocus());
        fieldEditText.setText("edited");
        assertNull(inputLayout.getError());

        assertFalse(field.validate());
        assertTrue(inputLayout.getError() != null);
    }

    /** Test that {@link #validate()} returns true when there is no validator. */
    @Test
    public void testNullValidator() {
        PropertyModel model = buildDefaultPropertyModel();
        TextFieldView field = attachTextFieldView(model);

        assertTrue(field.validate());
    }
}