chromium/components/android_autofill/browser/junit/src/org/chromium/components/autofill/AutofillRequestTest.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.components.autofill;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import android.graphics.Rect;
import android.graphics.RectF;
import android.util.SparseArray;
import android.view.View;
import android.view.autofill.AutofillValue;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;

import org.chromium.base.test.BaseRobolectricTestRunner;

import java.util.Arrays;

/** Unit test for {@link AutofillRequest}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class AutofillRequestTest {
    private static final int FORM_SESSION_ID = 123;
    private static final String FORM_DOMAIN = "https://example.com";
    private static final String FORM_NAME = "sample-form-name";

    private static FormFieldDataBuilder createTextFieldBuilder() {
        FormFieldDataBuilder builder = new FormFieldDataBuilder();
        builder.mValue = "current value";
        builder.mMaxLength = 15;
        return builder;
    }

    private static FormFieldDataBuilder createDatalistFieldBuilder() {
        FormFieldDataBuilder builder = new FormFieldDataBuilder();
        builder.mValue = "current value";
        builder.mDatalistValues = new String[] {"entry1", "entry2"};
        return builder;
    }

    private static FormFieldDataBuilder createCheckboxFieldBuilder() {
        FormFieldDataBuilder builder = new FormFieldDataBuilder();
        builder.mIsCheckField = true;
        builder.mIsChecked = true;
        return builder;
    }

    private static FormFieldDataBuilder createListFieldBuilder() {
        FormFieldDataBuilder builder = new FormFieldDataBuilder();
        builder.mOptionValues = new String[] {"value1", "value2"};
        builder.mOptionContents = new String[] {"content1", "content2"};
        builder.mValue = "value2";
        return builder;
    }

    private static AutofillRequest createRequest(int sessionId, FormFieldData... fields) {
        FormData formData = new FormData(sessionId, FORM_NAME, FORM_DOMAIN, Arrays.asList(fields));

        return new AutofillRequest(formData, null, /* hasServerPrediction= */ false);
    }

    private static AutofillRequest createSampleRequest() {
        FormFieldDataBuilder fieldBuilder1 = new FormFieldDataBuilder();
        FormFieldDataBuilder fieldBuilder2 = new FormFieldDataBuilder();
        return createRequest(FORM_SESSION_ID, fieldBuilder1.build(), fieldBuilder2.build());
    }

    private static TestViewStructure fillStructureForRequest(AutofillRequest request) {
        TestViewStructure structure = new TestViewStructure();
        request.getForm().fillViewStructure(structure, (short) -1);
        return structure;
    }

    @Test
    // Tests that the information that deals with form level data (host, form name) is set
    // correctly.
    public void testFormInformationIsSet() {
        TestViewStructure structure = fillStructureForRequest(createSampleRequest());

        assertEquals(FORM_DOMAIN, structure.getWebDomain());
        TestViewStructure.TestHtmlInfo htmlInfoForm = structure.getHtmlInfo();
        assertEquals("form", htmlInfoForm.getTag());
        assertEquals(FORM_NAME, htmlInfoForm.getAttribute("name"));
    }

    @Test
    // Tests that forms with multiple children result in ViewStructures with multiple leaf nodes.
    public void testMultipleChildrenAreAddedToViewStructure() {
        AutofillRequest request = createSampleRequest();
        assertEquals(2, request.getFieldCount());
        TestViewStructure structure = fillStructureForRequest(request);
        assertEquals(2, structure.getChildCount());
    }

    @Test
    // Tests that the form field level data (bounds, visibility, labels, etc.) apart from the
    // control type is set correctly
    public void testFormFieldInformationIsSet() {
        FormFieldDataBuilder fieldBuilder = new FormFieldDataBuilder();
        fieldBuilder.mAutocompleteAttr = "username";
        fieldBuilder.mPlaceholder = "placeholder";
        fieldBuilder.mBoundsInContainerViewCoordinates = new RectF(0, 0, 40, 60);
        fieldBuilder.mName = "username-field";
        fieldBuilder.mType = "username";
        fieldBuilder.mLabel = "Username";
        fieldBuilder.mHeuristicType = "PASSWORD";
        fieldBuilder.mId = "username-id";
        fieldBuilder.mServerType = "USERNAME";
        fieldBuilder.mComputedType = "USERNAME";

        TestViewStructure structure =
                fillStructureForRequest(createRequest(FORM_SESSION_ID, fieldBuilder.build()));

        assertEquals(1, structure.getChildCount());
        TestViewStructure child = structure.getChild(0);

        assertArrayEquals(new String[] {"username"}, child.getAutofillHints());
        assertEquals("placeholder", child.getHint());
        assertEquals(new Rect(0, 0, 40, 60), child.getDimensRect());
        TestViewStructure.TestHtmlInfo htmlInfoField = child.getHtmlInfo();
        assertEquals("input", htmlInfoField.getTag());
        assertEquals("username-field", htmlInfoField.getAttribute("name"));
        assertEquals("username", htmlInfoField.getAttribute("type"));
        assertEquals("Username", htmlInfoField.getAttribute("label"));
        assertEquals("PASSWORD", htmlInfoField.getAttribute("ua-autofill-hints"));
        assertEquals("username-id", htmlInfoField.getAttribute("id"));
        assertEquals("USERNAME", htmlInfoField.getAttribute("crowdsourcing-autofill-hints"));
        assertEquals("USERNAME", htmlInfoField.getAttribute("computed-autofill-hints"));
    }

    @Test
    // Tests that the control-type specific data of a text field is set correctly.
    public void testControlTypeSpecificInformationIsSetForTextFields() {
        FormFieldDataBuilder fieldBuilder = createTextFieldBuilder();
        TestViewStructure structure =
                fillStructureForRequest(createRequest(FORM_SESSION_ID, fieldBuilder.build()));

        assertEquals(1, structure.getChildCount());
        TestViewStructure child = structure.getChild(0);

        // The default type is a text field.
        assertEquals(View.AUTOFILL_TYPE_TEXT, child.getAutofillType());
        assertEquals(AutofillValue.forText(fieldBuilder.mValue), child.getAutofillValue());
        assertEquals(
                Integer.toString(fieldBuilder.mMaxLength),
                child.getHtmlInfo().getAttribute("maxlength"));
    }

    @Test
    // Tests that the control-type specific data of a data list field is set correctly.
    public void testControlTypeSpecificInformationIsSetForDatalistFields() {
        FormFieldDataBuilder fieldBuilder = createDatalistFieldBuilder();
        TestViewStructure structure =
                fillStructureForRequest(createRequest(FORM_SESSION_ID, fieldBuilder.build()));

        assertEquals(1, structure.getChildCount());
        TestViewStructure child = structure.getChild(0);

        // Datalists also have text type.
        assertEquals(View.AUTOFILL_TYPE_TEXT, child.getAutofillType());
        assertEquals(AutofillValue.forText(fieldBuilder.mValue), child.getAutofillValue());
        assertEquals(fieldBuilder.mDatalistValues.length, child.getAutofillOptions().length);
        assertEquals(fieldBuilder.mDatalistValues[0], child.getAutofillOptions()[0]);
        assertEquals(fieldBuilder.mDatalistValues[1], child.getAutofillOptions()[1]);
    }

    @Test
    // Tests that the control-type specific data of a checkbox field is set correctly.
    public void testControlTypeSpecificInformationIsSetForCheckboxFields() {
        FormFieldDataBuilder fieldBuilder = createCheckboxFieldBuilder();
        TestViewStructure structure =
                fillStructureForRequest(createRequest(FORM_SESSION_ID, fieldBuilder.build()));

        assertEquals(1, structure.getChildCount());
        TestViewStructure child = structure.getChild(0);

        assertEquals(View.AUTOFILL_TYPE_TOGGLE, child.getAutofillType());
        assertEquals(AutofillValue.forToggle(fieldBuilder.mIsChecked), child.getAutofillValue());
    }

    @Test
    // Tests that the control-type specific data of a list field is set correctly.
    public void testControlTypeSpecificInformationIsSetForListFields() {
        FormFieldDataBuilder fieldBuilder = createListFieldBuilder();
        TestViewStructure structure =
                fillStructureForRequest(createRequest(FORM_SESSION_ID, fieldBuilder.build()));

        assertEquals(1, structure.getChildCount());
        TestViewStructure child = structure.getChild(0);

        assertEquals(View.AUTOFILL_TYPE_LIST, child.getAutofillType());
        // The field's value matches the option entry with index 1.
        assertEquals(AutofillValue.forList(1), child.getAutofillValue());

        fieldBuilder.mValue = "value3";
        structure = fillStructureForRequest(createRequest(FORM_SESSION_ID, fieldBuilder.build()));
        assertEquals(1, structure.getChildCount());
        child = structure.getChild(0);
        assertEquals(View.AUTOFILL_TYPE_LIST, child.getAutofillType());
        // If there is no matching entry, the AutofillValue is not set.
        assertEquals(null, child.getAutofillValue());
    }

    @Test
    // Tests that autofill() updates the underlying FormFieldData for a text field.
    public void testAutofillUpdatesTextField() {
        FormFieldDataBuilder fieldBuilder = createTextFieldBuilder();
        AutofillRequest request = createRequest(FORM_SESSION_ID, fieldBuilder.build());
        TestViewStructure structure = fillStructureForRequest(request);
        assertEquals(1, structure.getChildCount());
        TestViewStructure child = structure.getChild(0);

        SparseArray<AutofillValue> valuesToFill = new SparseArray<AutofillValue>();
        valuesToFill.append(child.getId(), AutofillValue.forText("new value"));

        // The autofill requests succeeds.
        assertTrue(request.autofill(valuesToFill));
        // The underlying FormFieldData object is updated.
        assertEquals("new value", request.getField((short) 0).getValue());
    }

    @Test
    // Tests that autofill() updates the underlying FormFieldData for a datalist field.
    public void testAutofillUpdatesDatalistField() {
        FormFieldDataBuilder fieldBuilder = createDatalistFieldBuilder();
        AutofillRequest request = createRequest(FORM_SESSION_ID, fieldBuilder.build());
        TestViewStructure structure = fillStructureForRequest(request);
        assertEquals(1, structure.getChildCount());
        TestViewStructure child = structure.getChild(0);

        SparseArray<AutofillValue> valuesToFill = new SparseArray<AutofillValue>();
        valuesToFill.append(child.getId(), AutofillValue.forText("entry2"));

        // The autofill requests succeeds.
        assertTrue(request.autofill(valuesToFill));
        // The underlying FormFieldData object is updated.
        assertEquals("entry2", request.getField((short) 0).getValue());
    }

    @Test
    // Tests that autofill() updates the underlying FormFieldData for a checkbox field.
    public void testAutofillUpdatesCheckboxField() {
        FormFieldDataBuilder fieldBuilder = createCheckboxFieldBuilder();
        AutofillRequest request = createRequest(FORM_SESSION_ID, fieldBuilder.build());
        TestViewStructure structure = fillStructureForRequest(request);
        assertEquals(1, structure.getChildCount());
        TestViewStructure child = structure.getChild(0);

        SparseArray<AutofillValue> valuesToFill = new SparseArray<AutofillValue>();
        valuesToFill.append(child.getId(), AutofillValue.forToggle(false));

        // The autofill requests succeeds.
        assertTrue(request.autofill(valuesToFill));
        // The underlying FormFieldData object is updated.
        assertEquals(false, request.getField((short) 0).isChecked());
    }

    @Test
    // Tests that autofill() updates the underlying FormFieldData for a list field.
    public void testAutofillUpdatesListField() {
        FormFieldDataBuilder fieldBuilder = createListFieldBuilder();
        AutofillRequest request = createRequest(FORM_SESSION_ID, fieldBuilder.build());
        TestViewStructure structure = fillStructureForRequest(request);
        assertEquals(1, structure.getChildCount());
        TestViewStructure child = structure.getChild(0);

        SparseArray<AutofillValue> valuesToFill = new SparseArray<AutofillValue>();
        valuesToFill.append(child.getId(), AutofillValue.forList(0));

        // The autofill requests succeeds.
        assertTrue(request.autofill(valuesToFill));
        // The underlying FormFieldData object is updated.
        assertEquals("value1", request.getField((short) 0).getValue());

        // Invalid list indices are ignored.
        valuesToFill.put(child.getId(), AutofillValue.forList(3));
        assertFalse(request.autofill(valuesToFill));
        assertEquals("value1", request.getField((short) 0).getValue());
        valuesToFill.put(child.getId(), AutofillValue.forList(-1));
        assertFalse(request.autofill(valuesToFill));
        assertEquals("value1", request.getField((short) 0).getValue());
    }

    @Test
    // Tests that autofill() returns false if the session id does not match that of the
    // AutofillRequest.
    public void testAutofillDoesNotFillDifferentForm() {
        AutofillRequest request1 = createRequest(FORM_SESSION_ID, createTextFieldBuilder().build());
        TestViewStructure structure1 = fillStructureForRequest(request1);
        assertEquals(1, structure1.getChildCount());

        // Create a separate request with a different session id.
        AutofillRequest request2 =
                createRequest(FORM_SESSION_ID + 1, createTextFieldBuilder().build());

        // Use the id from the old request for the autofill call.
        SparseArray<AutofillValue> valuesToFill = new SparseArray<AutofillValue>();
        valuesToFill.append(structure1.getChild(0).getId(), AutofillValue.forText("new text"));

        assertFalse(request2.autofill(valuesToFill));
    }

    @Test
    // Tests that autofill() returns true if at least one session id matches that of the
    // AutofillRequest.
    public void testAutofillWithMixOfSessionIds() {
        AutofillRequest request1 = createRequest(FORM_SESSION_ID, createTextFieldBuilder().build());
        AutofillRequest request2 =
                createRequest(FORM_SESSION_ID + 1, createTextFieldBuilder().build());
        TestViewStructure structure1 = fillStructureForRequest(request1);
        request2.getForm().fillViewStructure(structure1, (short) -1);
        assertEquals(2, structure1.getChildCount());

        // Request to autofill a field from the previous request and one from the current.
        SparseArray<AutofillValue> valuesToFill = new SparseArray<AutofillValue>();
        valuesToFill.append(structure1.getChild(0).getId(), AutofillValue.forText("skipped"));
        valuesToFill.append(structure1.getChild(1).getId(), AutofillValue.forText("as expected"));

        assertTrue(request2.autofill(valuesToFill));
        assertEquals("as expected", request2.getField((short) 0).getValue());
    }

    @Test
    // Tests that autofill() returns false if the session id does not match that of the
    // AutofillRequest.
    public void testAutofillDoesNotFillUnknownField() {
        AutofillRequest request = createRequest(FORM_SESSION_ID, createTextFieldBuilder().build());
        TestViewStructure structure = fillStructureForRequest(request);
        assertEquals(1, structure.getChildCount());

        // Increment the id by 1 to generate an invalid id for the autofill call.
        SparseArray<AutofillValue> valuesToFill = new SparseArray<AutofillValue>();
        valuesToFill.append(structure.getChild(0).getId() + 1, AutofillValue.forText("new text"));

        assertFalse(request.autofill(valuesToFill));
    }

    @Test
    public void testFocusedField() {
        AutofillRequest request = createSampleRequest();
        TestViewStructure structure = new TestViewStructure();
        request.getForm().fillViewStructure(structure, /* focusedIndex= */ (short) 1);

        assertFalse(structure.getChild(0).getFocused());
        assertTrue(structure.getChild(1).getFocused());
    }
}