chromium/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/all_passwords_bottom_sheet/AllPasswordsBottomSheetViewTest.java

// Copyright 2020 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.all_passwords_bottom_sheet;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition;
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.verify;

import static org.chromium.base.test.util.CriteriaHelper.pollInstrumentationThread;
import static org.chromium.base.test.util.CriteriaHelper.pollUiThread;
import static org.chromium.chrome.browser.keyboard_accessory.all_passwords_bottom_sheet.AllPasswordsBottomSheetProperties.VISIBLE;

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

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

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.autofill.helpers.FaviconHelper;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.keyboard_accessory.R;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.SheetState;
import org.chromium.components.browser_ui.widget.chips.ChipView;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.concurrent.ExecutionException;

/**
 * View tests for the AllPasswordsBottomSheet ensure that model changes are reflected in the sheet.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@EnableFeatures(ChromeFeatureList.FILLING_PASSWORDS_FROM_ANY_ORIGIN)
public class AllPasswordsBottomSheetViewTest {
    private static final Credential ANA =
            new Credential(
                    /* username= */ "[email protected]",
                    /* password= */ "S3cr3t",
                    /* formattedUsername= */ "[email protected]",
                    /* originUrl= */ "https://example.com",
                    /* isAndroidCredential= */ false,
                    /* appDisplayName= */ "",
                    /* isPlusAddressUsername= */ true);
    private static final Credential NO_ONE =
            new Credential(
                    /* username= */ "",
                    /* password= */ "***",
                    /* formattedUsername= */ "No Username",
                    /* originUrl= */ "https://m.example.xyz",
                    /* isAndroidCredential= */ false,
                    /* appDisplayName= */ "",
                    /* isPlusAddressUsername= */ false);
    private static final Credential BOB =
            new Credential(
                    /* username= */ "Bob",
                    /* password= */ "***",
                    /* formattedUsername= */ "Bob",
                    /* originUrl= */ "android://com.facebook.org",
                    /* isAndroidCredential= */ true,
                    /* appDisplayName= */ "facebook",
                    /* isPlusAddressUsername= */ false);
    private static final boolean IS_PASSWORD_FIELD = true;
    private static final String EXAMPLE_ORIGIN = "https://m.example.com/";

    @Mock private Callback<Integer> mDismissHandler;
    @Mock private Callback<CredentialFillRequest> mCredentialFillRequestCallback;
    @Mock private Callback<String> mSearchQueryCallback;

    private PropertyModel mModel;
    private ListModel<ListItem> mListModel;
    private AllPasswordsBottomSheetView mAllPasswordsBottomSheetView;
    private BottomSheetController mBottomSheetController;

    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    @Before
    public void setUp() throws InterruptedException {
        MockitoAnnotations.initMocks(this);
        mActivityTestRule.startMainActivityOnBlankPage();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModel =
                            AllPasswordsBottomSheetProperties.createDefaultModel(
                                    EXAMPLE_ORIGIN, mDismissHandler, mSearchQueryCallback);
                    mListModel = new ListModel<>();
                    mBottomSheetController =
                            mActivityTestRule
                                    .getActivity()
                                    .getRootUiCoordinatorForTesting()
                                    .getBottomSheetController();
                    mAllPasswordsBottomSheetView =
                            new AllPasswordsBottomSheetView(getActivity(), mBottomSheetController);
                    AllPasswordsBottomSheetViewBinder.UiConfiguration uiConfiguration =
                            new AllPasswordsBottomSheetViewBinder.UiConfiguration();
                    uiConfiguration.faviconHelper =
                            FaviconHelper.create(
                                    getActivity(), mActivityTestRule.getProfile(false));
                    AllPasswordsBottomSheetCoordinator.setUpView(
                            mModel, mListModel, mAllPasswordsBottomSheetView, uiConfiguration);
                });
    }

    @Test
    @MediumTest
    public void testVisibilityChangedByModel() {
        // After setting the visibility to true, the view should exist and be visible.
        ThreadUtils.runOnUiThreadBlocking(() -> mModel.set(VISIBLE, true));
        pollUiThread(() -> getBottomSheetState() == SheetState.FULL);
        assertThat(mAllPasswordsBottomSheetView.getContentView().isShown(), is(true));

        // After hiding the view, the view should still exist but be invisible.
        ThreadUtils.runOnUiThreadBlocking(() -> mModel.set(VISIBLE, false));
        pollUiThread(() -> getBottomSheetState() == BottomSheetController.SheetState.HIDDEN);
        assertThat(mAllPasswordsBottomSheetView.getContentView().isShown(), is(false));
    }

    @Test
    @MediumTest
    public void testShowsWarningWithOriginByDefaultWithUpmEnabled() {
        ThreadUtils.runOnUiThreadBlocking(() -> mModel.set(VISIBLE, true));
        pollUiThread(() -> getBottomSheetState() == SheetState.FULL);
        assertThat(mAllPasswordsBottomSheetView.getContentView().isShown(), is(true));
        assertEquals(
                mAllPasswordsBottomSheetView.getWarningText().toString(),
                String.format(
                        getString(R.string.all_passwords_bottom_sheet_subtitle), "m.example.com"));
    }

    @Test
    @MediumTest
    public void testCredentialsChangedByModel() {
        addDefaultCredentialsToTheModel();

        pollUiThread(() -> getBottomSheetState() == SheetState.FULL);
        onView(withId(R.id.sheet_item_list))
                .perform(scrollToPosition(0))
                .check(
                        (view, e) -> {
                            View child =
                                    ((RecyclerView) view)
                                            .findViewHolderForAdapterPosition(0)
                                            .itemView;
                            assertThat(getCredentialOrigin(child).getText(), is("example.com"));
                            assertThat(
                                    getCredentialName(child).getPrimaryTextView().getText(),
                                    is(ANA.getFormattedUsername()));
                            assertThat(
                                    getCredentialPassword(child).getPrimaryTextView().getText(),
                                    is(ANA.getPassword()));
                            assertThat(
                                    getCredentialPassword(child)
                                            .getPrimaryTextView()
                                            .getTransformationMethod(),
                                    instanceOf(PasswordTransformationMethod.class));
                            assertThat(getCredentialName(child).isEnabled(), is(true));
                            assertThat(getCredentialName(child).isClickable(), is(true));
                            assertThat(getCredentialPassword(child).isEnabled(), is(true));
                            assertThat(getCredentialPassword(child).isClickable(), is(true));
                        });

        onView(withId(R.id.sheet_item_list))
                .perform(scrollToPosition(1))
                .check(
                        (view, e) -> {
                            View child =
                                    ((RecyclerView) view)
                                            .findViewHolderForAdapterPosition(1)
                                            .itemView;
                            assertThat(getCredentialOrigin(child).getText(), is("m.example.xyz"));
                            assertThat(
                                    getCredentialName(child).getPrimaryTextView().getText(),
                                    is(NO_ONE.getFormattedUsername()));
                            assertThat(
                                    getCredentialPassword(child).getPrimaryTextView().getText(),
                                    is(NO_ONE.getPassword()));
                            assertThat(
                                    getCredentialPassword(child)
                                            .getPrimaryTextView()
                                            .getTransformationMethod(),
                                    instanceOf(PasswordTransformationMethod.class));
                            assertThat(getCredentialName(child).isEnabled(), is(false));
                            assertThat(getCredentialName(child).isClickable(), is(false));
                            assertThat(getCredentialPassword(child).isEnabled(), is(true));
                            assertThat(getCredentialPassword(child).isClickable(), is(true));
                        });

        onView(withId(R.id.sheet_item_list))
                .perform(scrollToPosition(2))
                .check(
                        (view, e) -> {
                            View child =
                                    ((RecyclerView) view)
                                            .findViewHolderForAdapterPosition(2)
                                            .itemView;
                            assertThat(getCredentialOrigin(child).getText(), is("facebook"));
                            assertThat(
                                    getCredentialName(child).getPrimaryTextView().getText(),
                                    is(BOB.getFormattedUsername()));
                            assertThat(
                                    getCredentialPassword(child).getPrimaryTextView().getText(),
                                    is(BOB.getPassword()));
                            assertThat(
                                    getCredentialPassword(child)
                                            .getPrimaryTextView()
                                            .getTransformationMethod(),
                                    instanceOf(PasswordTransformationMethod.class));
                            assertThat(getCredentialName(child).isEnabled(), is(true));
                            assertThat(getCredentialName(child).isClickable(), is(true));
                            assertThat(getCredentialPassword(child).isEnabled(), is(true));
                            assertThat(getCredentialPassword(child).isClickable(), is(true));
                        });
    }

    @Test
    @MediumTest
    public void testFillingPasswordInNonPasswordFieldShowsWarningDialog()
            throws ExecutionException {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mAllPasswordsBottomSheetView.setVisible(true);
                    mListModel.add(
                            new ListItem(
                                    AllPasswordsBottomSheetProperties.ItemType.CREDENTIAL,
                                    AllPasswordsBottomSheetProperties.CredentialProperties
                                            .createCredentialModel(
                                                    ANA, mCredentialFillRequestCallback, false)));
                });

        pollUiThread(() -> getBottomSheetState() == SheetState.FULL);

        onView(allOf(withId(R.id.password_text), isDescendantOfA(withId(R.id.sheet_item_list))))
                .perform(click());

        pollInstrumentationThread(
                () -> {
                    onView(withText(R.string.passwords_not_secure_filling))
                            .inRoot(isDialog())
                            .check(matches(isDisplayed()));
                });
    }

    @Test
    @MediumTest
    public void testDismissesWhenHidden() {
        addDefaultCredentialsToTheModel();

        ThreadUtils.runOnUiThreadBlocking(() -> mModel.set(VISIBLE, true));
        pollUiThread(() -> getBottomSheetState() == SheetState.FULL);
        ThreadUtils.runOnUiThreadBlocking(() -> mModel.set(VISIBLE, false));
        pollUiThread(() -> getBottomSheetState() == BottomSheetController.SheetState.HIDDEN);
        verify(mDismissHandler).onResult(BottomSheetController.StateChangeReason.NONE);
    }

    @Test
    @MediumTest
    public void testSearchIsCalledOnSearchQueryChange() {
        addDefaultCredentialsToTheModel();
        pollUiThread(() -> mAllPasswordsBottomSheetView.getSearchView().setQuery("a", false));
        verify(mSearchQueryCallback).onResult("a");
    }

    // Adds three credential items to the model.
    private void addDefaultCredentialsToTheModel() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mAllPasswordsBottomSheetView.setVisible(true);
                    mListModel.add(
                            new ListItem(
                                    AllPasswordsBottomSheetProperties.ItemType.CREDENTIAL,
                                    AllPasswordsBottomSheetProperties.CredentialProperties
                                            .createCredentialModel(
                                                    ANA,
                                                    mCredentialFillRequestCallback,
                                                    IS_PASSWORD_FIELD)));
                    mListModel.add(
                            new ListItem(
                                    AllPasswordsBottomSheetProperties.ItemType.CREDENTIAL,
                                    AllPasswordsBottomSheetProperties.CredentialProperties
                                            .createCredentialModel(
                                                    NO_ONE,
                                                    mCredentialFillRequestCallback,
                                                    IS_PASSWORD_FIELD)));
                    mListModel.add(
                            new ListItem(
                                    AllPasswordsBottomSheetProperties.ItemType.CREDENTIAL,
                                    AllPasswordsBottomSheetProperties.CredentialProperties
                                            .createCredentialModel(
                                                    BOB,
                                                    mCredentialFillRequestCallback,
                                                    IS_PASSWORD_FIELD)));
                });
    }

    private ChromeActivity getActivity() {
        return mActivityTestRule.getActivity();
    }

    private String getString(@StringRes int stringRes) {
        return mAllPasswordsBottomSheetView.getContentView().getResources().getString(stringRes);
    }

    private @SheetState int getBottomSheetState() {
        return mBottomSheetController.getSheetState();
    }

    private RecyclerView getCredentials() {
        return (RecyclerView)
                mAllPasswordsBottomSheetView.getContentView().findViewById(R.id.sheet_item_list);
    }

    private TextView getCredentialOrigin(View parent) {
        return parent.findViewById(R.id.password_info_title);
    }

    private ChipView getCredentialName(View parent) {
        return parent.findViewById(R.id.suggestion_text);
    }

    private ChipView getCredentialPassword(View parent) {
        return parent.findViewById(R.id.password_text);
    }
}