chromium/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/sheet_tabs/PasswordAccessorySheetViewTest.java

// Copyright 2018 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.keyboard_accessory.sheet_tabs;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import android.text.method.PasswordTransformationMethod;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.StringRes;
import androidx.appcompat.widget.SwitchCompat;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.filters.MediumTest;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.chrome.browser.autofill.helpers.FaviconHelper;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.keyboard_accessory.AccessoryAction;
import org.chromium.chrome.browser.keyboard_accessory.AccessoryTabType;
import org.chromium.chrome.browser.keyboard_accessory.R;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData.OptionToggle;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData.PasskeySection;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData.PlusAddressInfo;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData.UserInfo;
import org.chromium.chrome.browser.keyboard_accessory.data.UserInfoField;
import org.chromium.chrome.browser.keyboard_accessory.sheet_component.AccessorySheetCoordinator;
import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.AccessorySheetTabItemsModel.AccessorySheetDataPiece;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.browser_ui.widget.chips.ChipView;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;

/** View tests for the password accessory sheet. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class PasswordAccessorySheetViewTest {
    private AccessorySheetTabItemsModel mModel;
    private AtomicReference<RecyclerView> mView = new AtomicReference<>();

    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    @Before
    public void setUp() throws InterruptedException {
        mActivityTestRule.startMainActivityOnBlankPage();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel = new AccessorySheetTabItemsModel();

                    AccessorySheetCoordinator accessorySheet =
                            new AccessorySheetCoordinator(
                                    mActivityTestRule
                                            .getActivity()
                                            .findViewById(R.id.keyboard_accessory_sheet_stub),
                                    null);
                    accessorySheet.setTabs(
                            new KeyboardAccessoryData.Tab[] {
                                new KeyboardAccessoryData.Tab(
                                        "Passwords",
                                        null,
                                        null,
                                        R.layout.password_accessory_sheet,
                                        AccessoryTabType.ALL,
                                        new KeyboardAccessoryData.Tab.Listener() {
                                            @Override
                                            public void onTabCreated(ViewGroup view) {
                                                mView.set((RecyclerView) view);
                                                AccessorySheetTabViewBinder.initializeView(
                                                        mView.get(), null);
                                                PasswordAccessorySheetViewBinder.UiConfiguration
                                                        uiConfiguration =
                                                                new PasswordAccessorySheetViewBinder
                                                                        .UiConfiguration();
                                                uiConfiguration.faviconHelper =
                                                        FaviconHelper.create(
                                                                view.getContext(),
                                                                mActivityTestRule.getProfile(
                                                                        false));
                                                PasswordAccessorySheetViewBinder.initializeView(
                                                        mView.get(),
                                                        PasswordAccessorySheetCoordinator
                                                                .createAdapter(
                                                                        uiConfiguration, mModel));
                                            }

                                            @Override
                                            public void onTabShown() {}
                                        })
                            });
                    accessorySheet.setHeight(
                            mActivityTestRule
                                    .getActivity()
                                    .getResources()
                                    .getDimensionPixelSize(
                                            R.dimen.keyboard_accessory_sheet_height));
                    accessorySheet.show();
                });
        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mView.get(), notNullValue()));
    }

    @After
    public void tearDown() {
        mView.set(null);
    }

    @Test
    @MediumTest
    public void testAddingCaptionsToTheModelRendersThem() {
        assertThat(mView.get().getChildCount(), is(0));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.add(
                            new AccessorySheetDataPiece(
                                    "Passwords", AccessorySheetDataPiece.Type.TITLE));
                });

        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mView.get().getChildCount(), is(1)));
        View title = mView.get().findViewById(R.id.tab_title);
        assertThat(title, is(not(nullValue())));
        assertThat(title, instanceOf(TextView.class));
        assertThat(((TextView) title).getText(), is("Passwords"));
    }

    @Test
    @MediumTest
    public void testAddingUserInfoToTheModelRendersClickableActions() throws ExecutionException {
        final AtomicReference<Boolean> clicked = new AtomicReference<>(false);
        assertThat(mView.get().getChildCount(), is(0));

        UserInfo testInfo = new UserInfo("", false);
        testInfo.addField(
                new UserInfoField(
                        "Name Suggestion",
                        "Name Suggestion",
                        "",
                        false,
                        item -> clicked.set(true)));
        testInfo.addField(
                new UserInfoField(
                        "Password Suggestion",
                        "Password Suggestion",
                        "",
                        true,
                        item -> clicked.set(true)));
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.add(
                            new AccessorySheetDataPiece(
                                    testInfo, AccessorySheetDataPiece.Type.PASSWORD_INFO));
                });

        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mView.get().getChildCount(), is(1)));

        assertThat(getNameSuggestion().getPrimaryTextView().getText(), is("Name Suggestion"));
        assertThat(
                getPasswordSuggestion().getPrimaryTextView().getText(), is("Password Suggestion"));
        assertThat(
                getPasswordSuggestion().getPrimaryTextView().getTransformationMethod(),
                instanceOf(PasswordTransformationMethod.class));

        ThreadUtils.runOnUiThreadBlocking(getNameSuggestion()::performClick);
        assertThat(clicked.get(), is(true));
        clicked.set(false);
        ThreadUtils.runOnUiThreadBlocking(getPasswordSuggestion()::performClick);
        assertThat(clicked.get(), is(true));
    }

    @Test
    @MediumTest
    public void testAddingPasskeySectionToTheModelRendersClickableActions()
            throws ExecutionException {
        final AtomicReference<Boolean> clicked = new AtomicReference<>(false);
        assertThat(mView.get().getChildCount(), is(0));

        final PasskeySection kTestPasskey =
                new PasskeySection("Passkey User", () -> clicked.set(true));
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.add(
                            new AccessorySheetDataPiece(
                                    kTestPasskey, AccessorySheetDataPiece.Type.PASSKEY_SECTION));
                });

        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mView.get().getChildCount(), is(1)));

        assertThat(
                getPasskeyChipAt(0).getPrimaryTextView().getText(),
                is(kTestPasskey.getDisplayName()));
        assertThat(
                getPasskeyChipAt(0).getSecondaryTextView().getText(),
                is(getString(R.string.password_accessory_passkey_label)));

        ThreadUtils.runOnUiThreadBlocking(getPasskeyChipAt(0)::performClick);
        assertThat(clicked.get(), is(true));
    }

    @Test
    @MediumTest
    public void testAddingPlusAddressInfoToTheModelRendersClickableActions()
            throws ExecutionException {
        final AtomicReference<Boolean> clicked = new AtomicReference<>(false);
        assertThat(mView.get().getChildCount(), is(0));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.add(
                            new AccessorySheetDataPiece(
                                    new PlusAddressInfo(
                                            /* origin= */ "google.com",
                                            new UserInfoField.Builder()
                                                    .setDisplayText("[email protected]")
                                                    .setTextToFill("[email protected]")
                                                    .setIsObfuscated(false)
                                                    .setCallback(unused -> clicked.set(true))
                                                    .build()),
                                    AccessorySheetDataPiece.Type.PLUS_ADDRESS_SECTION));
                });

        CriteriaHelper.pollUiThread(
                () -> Criteria.checkThat(mView.get().getChildCount(), greaterThan(0)));

        assertThat(getPlusAddressChipAt(0).getPrimaryTextView().getText(), is("[email protected]"));

        // Plus address chip is clickable:
        ThreadUtils.runOnUiThreadBlocking(getPlusAddressChipAt(0)::performClick);
        assertThat(clicked.get(), is(true));
    }

    @Test
    @MediumTest
    public void testAddingUserInfoWithObfuscatedTextAndNullCallbackRendersDialog()
            throws ExecutionException {
        final AtomicReference<Boolean> clicked = new AtomicReference<>(false);
        assertThat(mView.get().getChildCount(), is(0));

        UserInfo usernameEnabled = new UserInfo("", false);
        usernameEnabled.addField(
                new UserInfoField("username1", "username1", "", false, item -> clicked.set(true)));
        usernameEnabled.addField(
                new UserInfoField("pa55w0rd", "Password for username1", "", true, null));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel.add(
                            new AccessorySheetDataPiece(
                                    usernameEnabled, AccessorySheetDataPiece.Type.PASSWORD_INFO));
                });

        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mView.get().getChildCount(), is(1)));

        assertThat(getNameSuggestion().getPrimaryTextView().getText(), is("username1"));
        assertThat(getPasswordSuggestion().getPrimaryTextView().getText(), is("pa55w0rd"));
        assertThat(
                getPasswordSuggestion().getPrimaryTextView().getTransformationMethod(),
                instanceOf(PasswordTransformationMethod.class));

        ThreadUtils.runOnUiThreadBlocking(getNameSuggestion()::performClick);
        assertThat(clicked.get(), is(true));
        ThreadUtils.runOnUiThreadBlocking(getPasswordSuggestion()::performClick);
        assertInsecureFillingDialog();
    }

    @Test
    @MediumTest
    public void testAddingUserInfoTitlesAreRenderedIfNotEmpty() {
        assertThat(mView.get().getChildCount(), is(0));
        final UserInfoField kUnusedInfoField =
                new UserInfoField("Unused Name", "Unused Password", "", false, cb -> {});

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    UserInfo sameOriginInfo = new UserInfo("", true);
                    sameOriginInfo.addField(kUnusedInfoField);
                    sameOriginInfo.addField(kUnusedInfoField);
                    mModel.add(
                            new AccessorySheetDataPiece(
                                    sameOriginInfo, AccessorySheetDataPiece.Type.PASSWORD_INFO));

                    UserInfo pslOriginInfo = new UserInfo("other.origin.eg", false);
                    pslOriginInfo.addField(kUnusedInfoField);
                    pslOriginInfo.addField(kUnusedInfoField);
                    mModel.add(
                            new AccessorySheetDataPiece(
                                    pslOriginInfo, AccessorySheetDataPiece.Type.PASSWORD_INFO));
                });

        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mView.get().getChildCount(), is(2)));

        assertThat(getUserInfoAt(0).getTitle().isShown(), is(false));
        assertThat(getUserInfoAt(1).getTitle().isShown(), is(true));
        assertThat(getUserInfoAt(1).getTitle().getText(), is("other.origin.eg"));
    }

    @Test
    @MediumTest
    public void testOptionToggleRenderedIfNotEmpty() throws ExecutionException {
        assertThat(mView.get().getChildCount(), is(0));
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    OptionToggle toggle =
                            new OptionToggle(
                                    "Save passwords for this site",
                                    false,
                                    AccessoryAction.TOGGLE_SAVE_PASSWORDS,
                                    result -> {});
                    mModel.add(
                            new AccessorySheetDataPiece(
                                    toggle, AccessorySheetDataPiece.Type.OPTION_TOGGLE));
                });

        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mView.get().getChildCount(), is(1)));
        View title = mView.get().findViewById(R.id.option_toggle_title);
        assertThat(title, is(not(nullValue())));
        assertThat(title, instanceOf(TextView.class));
        assertThat(((TextView) title).getText(), is("Save passwords for this site"));

        View subtitle = mView.get().findViewById(R.id.option_toggle_subtitle);
        assertThat(subtitle, is(not(nullValue())));
        assertThat(subtitle, instanceOf(TextView.class));
        assertThat(subtitle, withText(R.string.text_off));

        View switchView = mView.get().findViewById(R.id.option_toggle_switch);
        assertThat(switchView, is(not(nullValue())));
        assertThat(switchView, instanceOf(SwitchCompat.class));
        assertFalse(((SwitchCompat) switchView).isChecked());
    }

    @Test
    @MediumTest
    public void testClickingDisabledToggleInvokesCallbackToEnable() throws ExecutionException {
        AtomicReference<Boolean> toggleEnabled = new AtomicReference<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    OptionToggle toggle =
                            new OptionToggle(
                                    "Save passwords for this site",
                                    false,
                                    AccessoryAction.TOGGLE_SAVE_PASSWORDS,
                                    toggleEnabled::set);
                    mModel.add(
                            new AccessorySheetDataPiece(
                                    toggle, AccessorySheetDataPiece.Type.OPTION_TOGGLE));
                });

        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mView.get().getChildCount(), is(1)));
        ThreadUtils.runOnUiThreadBlocking(
                mView.get().findViewById(R.id.option_toggle)::performClick);
        assertTrue(toggleEnabled.get());
    }

    @Test
    @MediumTest
    public void testClickingEnabledToggleInvokesCallbackToDisable() throws ExecutionException {
        AtomicReference<Boolean> toggleEnabled = new AtomicReference<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    OptionToggle toggle =
                            new OptionToggle(
                                    "Save passwords for this site",
                                    true,
                                    AccessoryAction.TOGGLE_SAVE_PASSWORDS,
                                    toggleEnabled::set);
                    mModel.add(
                            new AccessorySheetDataPiece(
                                    toggle, AccessorySheetDataPiece.Type.OPTION_TOGGLE));
                });

        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(mView.get().getChildCount(), is(1)));
        ThreadUtils.runOnUiThreadBlocking(
                mView.get().findViewById(R.id.option_toggle)::performClick);

        assertFalse(toggleEnabled.get());
    }

    private String getString(@StringRes int strId) {
        return mView.get().getResources().getString(strId);
    }

    private ChipView getPlusAddressChipAt(int index) {
        assertThat(mView.get().getChildCount(), is(greaterThan(index)));
        assertThat(mView.get().getChildAt(index), instanceOf(ViewGroup.class));
        LinearLayout plusAddressInfo = (LinearLayout) mView.get().getChildAt(index);
        return plusAddressInfo.findViewById(R.id.plus_address);
    }

    private ChipView getPasskeyChipAt(int index) {
        assertThat(mView.get().getChildCount(), is(greaterThan(index)));
        assertThat(mView.get().getChildAt(index), instanceOf(ViewGroup.class));
        LinearLayout passkeySection = (LinearLayout) mView.get().getChildAt(index);
        return passkeySection.findViewById(R.id.keyboard_accessory_sheet_chip);
    }

    private PasswordAccessoryInfoView getUserInfoAt(int index) {
        assertThat(mView.get().getChildCount(), is(greaterThan(index)));
        assertThat(mView.get().getChildAt(index), instanceOf(PasswordAccessoryInfoView.class));
        return (PasswordAccessoryInfoView) mView.get().getChildAt(index);
    }

    private ChipView getNameSuggestion() {
        View view = getUserInfoAt(0).findViewById(R.id.suggestion_text);
        assertThat(view, is(not(nullValue())));
        assertThat(view, instanceOf(ChipView.class));
        return (ChipView) view;
    }

    private ChipView getPasswordSuggestion() {
        View view = getUserInfoAt(0).findViewById(R.id.password_text);
        assertThat(view, is(not(nullValue())));
        assertThat(view, instanceOf(ChipView.class));
        return (ChipView) view;
    }

    private void assertInsecureFillingDialog() {
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    onView(withText(R.string.passwords_not_secure_filling))
                            .inRoot(isDialog())
                            .check(matches(isDisplayed()));
                    onView(withText(R.string.passwords_not_secure_filling_details))
                            .inRoot(isDialog())
                            .check(matches(isDisplayed()));
                });
    }
}