chromium/chrome/browser/touch_to_fill/password_manager/android/junit/src/org/chromium/chrome/browser/touch_to_fill/TouchToFillControllerTest.java

// Copyright 2019 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.touch_to_fill;

import static androidx.test.espresso.matcher.ViewMatchers.assertThat;

import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasItem;
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.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.CREDENTIAL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.FAVICON_OR_FALLBACK;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.FORMATTED_ORIGIN;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.ON_CLICK_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.SHOW_SUBMIT_BUTTON;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.DISMISS_HANDLER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FooterProperties.MANAGE_BUTTON_TEXT;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FooterProperties.ON_CLICK_HYBRID;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FooterProperties.ON_CLICK_MANAGE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FooterProperties.SHOW_HYBRID;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.AVATAR;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.IMAGE_DRAWABLE_ID;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.SUBTITLE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.TITLE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.SHEET_ITEMS;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.VISIBLE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.WebAuthnCredentialProperties.ON_WEBAUTHN_CLICK_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.WebAuthnCredentialProperties.SHOW_WEBAUTHN_SUBMIT_BUTTON;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.WebAuthnCredentialProperties.WEBAUTHN_CREDENTIAL;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;

import androidx.annotation.Px;

import jp.tomorrowkey.android.gifplayer.BaseGifImage;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;

import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.task.test.ShadowPostTask;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.password_manager.GetLoginMatchType;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FaviconOrFallback;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.ItemType;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.MorePasskeysProperties;
import org.chromium.chrome.browser.touch_to_fill.common.BottomSheetFocusHelper;
import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.chrome.browser.touch_to_fill.data.WebauthnCredential;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.util.AvatarGenerator;
import org.chromium.components.favicon.IconType;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.components.image_fetcher.ImageFetcher;
import org.chromium.components.image_fetcher.ImageFetcherConfig;
import org.chromium.components.url_formatter.SchemeDisplay;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
 * Controller tests verify that the Touch To Fill controller modifies the model if the API is used
 * properly.
 */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {ShadowPostTask.class})
public class TouchToFillControllerTest {
    private static final GURL TEST_URL = JUnitTestGURLs.EXAMPLE_URL;
    private static final String TEST_URL_FORMATTED =
            UrlFormatter.formatUrlForSecurityDisplay(TEST_URL, SchemeDisplay.OMIT_HTTP_AND_HTTPS);
    private static final String TEST_SUBDOMAIN_URL = "https://subdomain.example.xyz";
    private static final Credential ANA =
            new Credential(
                    "Ana",
                    "S3cr3t",
                    "Ana",
                    "https://m.a.xyz/",
                    "m.a.xyz",
                    GetLoginMatchType.PSL,
                    0);
    private static final Credential BOB =
            new Credential(
                    "Bob",
                    "*****",
                    "Bob",
                    TEST_SUBDOMAIN_URL,
                    "subdomain.example.xyz",
                    GetLoginMatchType.PSL,
                    0);
    private static final Credential CARL =
            new Credential(
                    "Carl",
                    "G3h3!m",
                    "Carl",
                    TEST_URL.getSpec(),
                    "example.xyz",
                    GetLoginMatchType.EXACT,
                    0);
    private static final WebauthnCredential DINO =
            new WebauthnCredential("dinos.com", new byte[] {1}, new byte[] {2}, "[email protected]");
    private static final @Px int DESIRED_FAVICON_SIZE = 64;
    private Bitmap mBitmapFromImageFetcher =
            Bitmap.createBitmap(/* width= */ 1, /* height= */ 1, Bitmap.Config.ARGB_8888);

    @Rule public JniMocker mJniMocker = new JniMocker();
    @Mock private TouchToFillComponent.Delegate mMockDelegate;
    @Mock private LargeIconBridge mMockIconBridge;
    @Mock private BottomSheetFocusHelper mMockFocusHelper;

    // Can't be local, as it has to be initialized by initMocks.
    @Captor private ArgumentCaptor<LargeIconBridge.LargeIconCallback> mCallbackArgumentCaptor;

    private TestImageFetcher mImageFetcher = spy(new TestImageFetcher());
    private final Context mContext = ContextUtils.getApplicationContext();
    private final TouchToFillMediator mMediator = new TouchToFillMediator();
    private final PropertyModel mModel =
            TouchToFillProperties.createDefaultModel(mMediator::onDismissed);

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        mMediator.initialize(
                mContext,
                mMockDelegate,
                mModel,
                mImageFetcher,
                mMockIconBridge,
                DESIRED_FAVICON_SIZE,
                mMockFocusHelper);
    }

    @After
    public void tearDown() {
        ShadowPostTask.reset();
    }

    @Test
    public void testCreatesValidDefaultModel() {
        assertNotNull(mModel.get(SHEET_ITEMS));
        assertNotNull(mModel.get(DISMISS_HANDLER));
        assertThat(mModel.get(VISIBLE), is(false));
    }

    @Test
    public void testShowCredentialsWithMultipleEntries() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA, CARL),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ true,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(itemList.size(), is(4)); // Header + 2 credentials + footer.
        assertThat(
                itemList.get(itemList.size() - 1).model.get(MANAGE_BUTTON_TEXT),
                is(mContext.getString(R.string.manage_passwords)));

        assertThat(itemList.get(0).type, is(ItemType.HEADER));
        assertThat(
                itemList.get(0).model.get(TITLE),
                is(mContext.getString(R.string.touch_to_fill_sheet_uniform_title)));
        assertThat(
                itemList.get(0).model.get(SUBTITLE),
                is(
                        mContext.getString(
                                R.string.touch_to_fill_sheet_subtitle_submission,
                                TEST_URL_FORMATTED)));
        assertThat(itemList.get(1).type, is(ItemType.CREDENTIAL));
        assertThat(itemList.get(1).model.get(CREDENTIAL), is(ANA));
        assertNotNull(itemList.get(1).model.get(ON_CLICK_LISTENER));
        assertThat(itemList.get(1).model.get(FORMATTED_ORIGIN), is(ANA.getDisplayName()));

        assertThat(itemList.get(2).type, is(ItemType.CREDENTIAL));
        assertThat(itemList.get(2).model.get(CREDENTIAL), is(CARL));
        assertNotNull(itemList.get(2).model.get(ON_CLICK_LISTENER));
        assertThat(itemList.get(2).model.get(FORMATTED_ORIGIN), is(CARL.getDisplayName()));
        assertThat(itemList.get(0).model.get(IMAGE_DRAWABLE_ID), is(R.drawable.ic_vpn_key_blue));
    }

    @Test
    public void testShowCredentialsWithSingleEntry() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(itemList.size(), is(4)); // Header + 1 credential + Button + Footer.
        assertThat(
                itemList.get(itemList.size() - 1).model.get(MANAGE_BUTTON_TEXT),
                is(mContext.getString(R.string.manage_passwords)));

        assertThat(itemList.get(0).type, is(ItemType.HEADER));
        assertThat(
                itemList.get(0).model.get(TITLE),
                is(mContext.getString(R.string.touch_to_fill_sheet_uniform_title)));
        assertThat(itemList.get(0).model.get(SUBTITLE), is(TEST_URL_FORMATTED));
        assertThat(itemList.get(1).type, is(ItemType.CREDENTIAL));
        assertThat(itemList.get(1).model.get(CREDENTIAL), is(ANA));
        assertNotNull(itemList.get(1).model.get(ON_CLICK_LISTENER));
        assertThat(itemList.get(1).model.get(FORMATTED_ORIGIN), is(ANA.getDisplayName()));

        assertThat(itemList.get(2).type, is(ItemType.FILL_BUTTON));
        assertThat(itemList.get(2).model.get(SHOW_SUBMIT_BUTTON), is(false));
    }

    @Test
    public void testShowCredentialsWithSingleWebAuthnEntry() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Arrays.asList(DINO),
                Collections.emptyList(),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(itemList.size(), is(4)); // Header + 1 credential + Button + Footer.
        assertThat(
                itemList.get(itemList.size() - 1).model.get(MANAGE_BUTTON_TEXT),
                is(mContext.getString(R.string.manage_passkeys)));

        assertThat(itemList.get(0).type, is(ItemType.HEADER));
        assertThat(
                itemList.get(0).model.get(TITLE),
                is(mContext.getString(R.string.touch_to_fill_sheet_title_passkey)));

        assertThat(itemList.get(1).type, is(ItemType.WEBAUTHN_CREDENTIAL));
        assertThat(itemList.get(1).model.get(WEBAUTHN_CREDENTIAL), is(DINO));
        assertNotNull(itemList.get(1).model.get(ON_WEBAUTHN_CLICK_LISTENER));

        assertThat(itemList.get(2).type, is(ItemType.FILL_BUTTON));
        assertThat(itemList.get(2).model.get(SHOW_WEBAUTHN_SUBMIT_BUTTON), is(false));
    }

    @Test
    public void testShowCredentialsWithWebAuthnAndPasswordEntries() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Arrays.asList(DINO),
                Arrays.asList(ANA),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        // Header + 1 webauthn credential + 1 password credential + Footer.
        assertThat(itemList.size(), is(4));
        assertThat(
                itemList.get(itemList.size() - 1).model.get(MANAGE_BUTTON_TEXT),
                is(mContext.getString(R.string.manage_passwords_and_passkeys)));

        assertThat(itemList.get(0).type, is(ItemType.HEADER));
        assertThat(
                itemList.get(0).model.get(TITLE),
                is(mContext.getString(R.string.touch_to_fill_sheet_title_password_or_passkey)));

        assertThat(itemList.get(1).type, is(ItemType.WEBAUTHN_CREDENTIAL));
        assertThat(itemList.get(1).model.get(WEBAUTHN_CREDENTIAL), is(DINO));
        assertNotNull(itemList.get(1).model.get(ON_WEBAUTHN_CLICK_LISTENER));

        assertThat(itemList.get(2).type, is(ItemType.CREDENTIAL));
        assertThat(itemList.get(2).model.get(CREDENTIAL), is(ANA));
        assertNotNull(itemList.get(2).model.get(ON_CLICK_LISTENER));
        assertThat(itemList.get(2).model.get(FORMATTED_ORIGIN), is(ANA.getDisplayName()));
    }

    @Test
    public void testShowCredentialsToSubmit() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ true,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(itemList.size(), is(4)); // Header + 1 credential + Button + Footer.
        assertThat(
                itemList.get(itemList.size() - 1).model.get(MANAGE_BUTTON_TEXT),
                is(mContext.getString(R.string.manage_passwords)));

        assertThat(itemList.get(0).type, is(ItemType.HEADER));
        assertThat(
                itemList.get(0).model.get(TITLE),
                is(mContext.getString(R.string.touch_to_fill_sheet_uniform_title)));
        assertThat(
                itemList.get(0).model.get(SUBTITLE),
                is(
                        mContext.getString(
                                R.string.touch_to_fill_sheet_subtitle_submission,
                                TEST_URL_FORMATTED)));

        assertThat(itemList.get(2).type, is(ItemType.FILL_BUTTON));
        assertThat(itemList.get(2).model.get(SHOW_SUBMIT_BUTTON), is(true));
    }

    @Test
    public void testShowCredentialsSetsCredentialListAndRequestsFavicons() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA, CARL, BOB),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(itemList.size(), is(5)); // Header + 3 Credentials + Footer.
        assertThat(itemList.get(1).type, is(ItemType.CREDENTIAL));
        assertThat(itemList.get(1).model.get(CREDENTIAL), is(ANA));
        assertThat(itemList.get(1).model.get(FAVICON_OR_FALLBACK), is(nullValue()));
        assertThat(itemList.get(2).type, is(ItemType.CREDENTIAL));
        assertThat(itemList.get(2).model.get(CREDENTIAL), is(CARL));
        assertThat(itemList.get(2).model.get(FAVICON_OR_FALLBACK), is(nullValue()));
        assertThat(itemList.get(3).type, is(ItemType.CREDENTIAL));
        assertThat(itemList.get(3).model.get(CREDENTIAL), is(BOB));
        assertThat(itemList.get(3).model.get(FAVICON_OR_FALLBACK), is(nullValue()));

        verify(mMockIconBridge)
                .getLargeIconForStringUrl(eq(CARL.getOriginUrl()), eq(DESIRED_FAVICON_SIZE), any());
        verify(mMockIconBridge)
                .getLargeIconForStringUrl(eq(ANA.getOriginUrl()), eq(DESIRED_FAVICON_SIZE), any());
        verify(mMockIconBridge)
                .getLargeIconForStringUrl(eq(BOB.getOriginUrl()), eq(DESIRED_FAVICON_SIZE), any());
    }

    @Test
    public void testShowSheetForOneSharedCredential() {
        List<Integer> postedTasksTraits = new ArrayList();
        ShadowPostTask.setTestImpl(
                (taskTraits, task, delay) -> {
                    assert delay == 0;
                    // Store the traits to test that tasks are posted to the proper traits.
                    postedTasksTraits.add(taskTraits);
                    task.run();
                });
        Credential sharedCredentials =
                new Credential(
                        "Ana",
                        "S3cr3t",
                        "Ana",
                        "https://m.a.xyz/",
                        "m.a.xyz",
                        GetLoginMatchType.PSL,
                        0,
                        true,
                        "Sender Name",
                        new GURL("https://sender-profile-image.xyz/"),
                        false);
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(sharedCredentials),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ true,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        // Header + 1 credential + Button + Footer.
        assertThat(itemList.size(), is(4));
        assertThat(itemList.get(0).type, is(ItemType.HEADER));
        assertThat(
                itemList.get(0).model.get(TITLE),
                is(
                        mContext.getResources()
                                .getQuantityString(
                                        R.plurals.touch_to_fill_sheet_shared_passwords_title, 1)));
        assertThat(
                itemList.get(0).model.get(SUBTITLE),
                is(
                        mContext.getString(
                                R.string.touch_to_fill_sheet_shared_passwords_one_password_subtitle,
                                "<b>Sender Name</b>",
                                TEST_URL_FORMATTED)));
        mImageFetcher.answerWithBitmap();

        // This value is chosen randomly for the test
        final int avatarImgeSizePx = 80;
        Bitmap expectedBitmap =
                ((BitmapDrawable)
                                AvatarGenerator.makeRoundAvatar(
                                        mContext.getResources(),
                                        mBitmapFromImageFetcher,
                                        avatarImgeSizePx))
                        .getBitmap();

        assertTrue(
                expectedBitmap.sameAs(
                        ((BitmapDrawable) itemList.get(0).model.get(AVATAR)).getBitmap()));
        // Two tasks have been posted, the first is to fetch the avatar imges on a non-UI task
        // runner, and the second to update the model on a UI task runner.
        assertThat(
                postedTasksTraits, contains(TaskTraits.USER_VISIBLE, TaskTraits.UI_USER_VISIBLE));
    }

    @Test
    public void testShowSheetForMultipleSharedCredentials() {
        Credential sharedCredential1 =
                new Credential(
                        "Ana",
                        "S3cr3t",
                        "Ana",
                        "https://m.a.xyz/",
                        "m.a.xyz",
                        GetLoginMatchType.PSL,
                        0,
                        true,
                        "Sender Name",
                        new GURL("https://sender-profile-image.xyz/"),
                        false);
        Credential sharedCredential2 =
                new Credential(
                        "Bob",
                        "S3cr3t",
                        "Ana",
                        "https://m.a.xyz/",
                        "m.a.xyz",
                        GetLoginMatchType.PSL,
                        0,
                        true,
                        "Sender Name",
                        new GURL("https://sender-profile-image.xyz/"),
                        false);
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(sharedCredential1, sharedCredential2),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ true,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        // Header + 2 credentials + Footer.
        assertThat(itemList.size(), is(4));
        assertThat(itemList.get(0).type, is(ItemType.HEADER));
        assertThat(
                itemList.get(0).model.get(TITLE),
                is(
                        mContext.getResources()
                                .getQuantityString(
                                        R.plurals.touch_to_fill_sheet_shared_passwords_title, 2)));
        assertThat(
                itemList.get(0).model.get(SUBTITLE),
                is(
                        mContext.getString(
                                R.string
                                        .touch_to_fill_sheet_shared_passwords_multiple_passwords_subtitle,
                                TEST_URL_FORMATTED)));
    }

    @Test
    public void testFetchFaviconUpdatesModel() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Collections.singletonList(CARL),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(itemList.size(), is(4)); // Header + Credential + Continue Button + Footer.
        assertThat(itemList.get(1).type, is(ItemType.CREDENTIAL));
        assertThat(itemList.get(1).model.get(CREDENTIAL), is(CARL));
        assertThat(itemList.get(1).model.get(FAVICON_OR_FALLBACK), is(nullValue()));

        // ANA and CARL both have TEST_URL as their origin URL
        verify(mMockIconBridge)
                .getLargeIconForStringUrl(
                        eq(TEST_URL.getSpec()),
                        eq(DESIRED_FAVICON_SIZE),
                        mCallbackArgumentCaptor.capture());
        LargeIconBridge.LargeIconCallback callback = mCallbackArgumentCaptor.getValue();
        Bitmap bitmap =
                Bitmap.createBitmap(
                        DESIRED_FAVICON_SIZE, DESIRED_FAVICON_SIZE, Bitmap.Config.ARGB_8888);
        callback.onLargeIconAvailable(bitmap, 333, true, IconType.FAVICON);
        FaviconOrFallback iconData = itemList.get(1).model.get(FAVICON_OR_FALLBACK);
        assertThat(iconData.mIcon, is(bitmap));
        assertThat(iconData.mUrl, is(TEST_URL.getSpec()));
        assertThat(iconData.mIconSize, is(DESIRED_FAVICON_SIZE));
        assertThat(iconData.mFallbackColor, is(333));
        assertThat(iconData.mIsFallbackColorDefault, is(true));
        assertThat(iconData.mIconType, is(IconType.FAVICON));
    }

    @Test
    public void testShowCredentialsFormatPslOrigins() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA, BOB),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        assertThat(mModel.get(SHEET_ITEMS).size(), is(4)); // Header + 2 Credentials + Footer.
        assertThat(mModel.get(SHEET_ITEMS).get(1).type, is(ItemType.CREDENTIAL));
        assertThat(
                mModel.get(SHEET_ITEMS).get(1).model.get(FORMATTED_ORIGIN),
                is(ANA.getDisplayName()));
        assertThat(mModel.get(SHEET_ITEMS).get(2).type, is(ItemType.CREDENTIAL));
        assertThat(
                mModel.get(SHEET_ITEMS).get(2).model.get(FORMATTED_ORIGIN),
                is(BOB.getDisplayName()));
    }

    @Test
    public void testClearsCredentialListWhenShowingAgain() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Collections.singletonList(ANA),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(itemList.size(), is(4)); // Header + Credential + Continue Button + Footer.
        assertThat(itemList.get(1).type, is(ItemType.CREDENTIAL));
        assertThat(itemList.get(1).model.get(CREDENTIAL), is(ANA));
        assertThat(itemList.get(1).model.get(FAVICON_OR_FALLBACK), is(nullValue()));

        // Showing the sheet a second time should replace all changed credentials.
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Collections.singletonList(BOB),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        itemList = mModel.get(SHEET_ITEMS);
        assertThat(itemList.size(), is(4)); // Header + Credential + Continue Button + Footer.
        assertThat(itemList.get(1).type, is(ItemType.CREDENTIAL));
        assertThat(itemList.get(1).model.get(CREDENTIAL), is(BOB));
        assertThat(itemList.get(1).model.get(FAVICON_OR_FALLBACK), is(nullValue()));
    }

    @Test
    public void testShowCredentialsSetsVisibile() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA, CARL, BOB),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        assertThat(mModel.get(VISIBLE), is(true));
    }

    @Test
    public void testCallsCallbackAndHidesOnSelectingItemDoesNotRecordIndexForSingleCredential() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        assertThat(mModel.get(VISIBLE), is(true));
        assertNotNull(mModel.get(SHEET_ITEMS).get(1).model.get(ON_CLICK_LISTENER));

        mModel.get(SHEET_ITEMS).get(1).model.get(ON_CLICK_LISTENER).onResult(ANA);
        verify(mMockDelegate).onCredentialSelected(ANA);
        assertThat(mModel.get(VISIBLE), is(false));
        assertThat(
                RecordHistogram.getHistogramTotalCountForTesting(
                        TouchToFillMediator.UMA_TOUCH_TO_FILL_CREDENTIAL_INDEX),
                is(0));
    }

    @Test
    public void testCallsCallbackAndHidesOnSelectingItem() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA, CARL),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        assertThat(mModel.get(VISIBLE), is(true));
        assertNotNull(mModel.get(SHEET_ITEMS).get(1).model.get(ON_CLICK_LISTENER));

        mModel.get(SHEET_ITEMS).get(1).model.get(ON_CLICK_LISTENER).onResult(CARL);
        verify(mMockDelegate).onCredentialSelected(CARL);
        assertThat(mModel.get(VISIBLE), is(false));
        assertThat(
                RecordHistogram.getHistogramValueCountForTesting(
                        TouchToFillMediator.UMA_TOUCH_TO_FILL_CREDENTIAL_INDEX, 1),
                is(1));
    }

    @Test
    public void testCallsDelegateAndHidesOnDismiss() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA, CARL),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        mMediator.onDismissed(BottomSheetController.StateChangeReason.BACK_PRESS);
        verify(mMockDelegate).onDismissed();
        assertThat(mModel.get(VISIBLE), is(false));
        assertThat(
                RecordHistogram.getHistogramValueCountForTesting(
                        TouchToFillMediator.UMA_TOUCH_TO_FILL_DISMISSAL_REASON,
                        BottomSheetController.StateChangeReason.BACK_PRESS),
                is(1));
    }

    @Test
    public void testHidesWhenSelectingManagePasswords() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA, CARL, BOB),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(
                itemList.get(itemList.size() - 1).model.get(ON_CLICK_MANAGE), is(notNullValue()));
        itemList.get(itemList.size() - 1).model.get(ON_CLICK_MANAGE).run();
        verify(mMockDelegate).onManagePasswordsSelected(/* passkeysShown= */ false);
        assertThat(mModel.get(VISIBLE), is(false));
    }

    @Test
    public void testManagePasswordsWithPasskeysShown() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Arrays.asList(DINO),
                Collections.emptyList(),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ true,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(
                itemList.get(itemList.size() - 1).model.get(ON_CLICK_MANAGE), is(notNullValue()));
        itemList.get(itemList.size() - 1).model.get(ON_CLICK_MANAGE).run();
        verify(mMockDelegate).onManagePasswordsSelected(/* passkeysShown= */ true);
    }

    @Test
    public void testManagePasskeysButtonTitleWhenPasswordsHidden() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Arrays.asList(DINO),
                Arrays.asList(ANA),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ true,
                /* showHybridPasskeyOption= */ false);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        // Header + 1 webauthn credential + 1 password credential + Footer.
        assertThat(itemList.size(), is(4));
        assertThat(
                itemList.get(itemList.size() - 1).model.get(MANAGE_BUTTON_TEXT),
                is(mContext.getString(R.string.manage_passkeys)));
    }

    @Test
    public void testAddsTheBottomSheetHelperToObserveTheSheet() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Arrays.asList(DINO),
                Arrays.asList(ANA),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ true,
                /* showHybridPasskeyOption= */ false);

        verify(mMockFocusHelper).registerForOneTimeUse();
    }

    @Test
    public void testHybridPasskeysShown() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA, CARL, BOB),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ true);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(itemList.get(itemList.size() - 1).model.get(SHOW_HYBRID), is(true));
    }

    @Test
    public void testHidesWhenSelectingHybridSignin() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA, CARL, BOB),
                /* showMorePasskeys= */ false,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ true);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(
                itemList.get(itemList.size() - 1).model.get(ON_CLICK_HYBRID), is(notNullValue()));
        itemList.get(itemList.size() - 1).model.get(ON_CLICK_HYBRID).run();
        verify(mMockDelegate).onHybridSignInSelected();
        assertThat(mModel.get(VISIBLE), is(false));
    }

    @Test
    public void testSelectPasskeyShownAndRan() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA),
                /* showMorePasskeys= */ true,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ true);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(itemList.get(2).type, is(ItemType.MORE_PASSKEYS));
        PropertyModel model = itemList.get(2).model;
        assertThat(
                model.get(MorePasskeysProperties.TITLE),
                is(mContext.getString(R.string.touch_to_fill_select_passkey)));
        Runnable onMorePasskeysItemRunnable = model.get(MorePasskeysProperties.ON_CLICK);
        assertThat(onMorePasskeysItemRunnable, is(notNullValue()));

        onMorePasskeysItemRunnable.run();
        verify(mMockDelegate).onShowMorePasskeysSelected();
        assertThat(mModel.get(VISIBLE), is(false));
    }

    @Test
    public void testMorePasskeysShownAndRan() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Arrays.asList(DINO),
                Arrays.asList(ANA),
                /* showMorePasskeys= */ true,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ true);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(itemList.get(3).type, is(ItemType.MORE_PASSKEYS));
        PropertyModel model = itemList.get(3).model;
        assertThat(
                model.get(MorePasskeysProperties.TITLE),
                is(mContext.getString(R.string.touch_to_fill_more_passkeys)));

        Runnable onMorePasskeysItemRunnable = model.get(MorePasskeysProperties.ON_CLICK);
        assertThat(onMorePasskeysItemRunnable, is(notNullValue()));

        onMorePasskeysItemRunnable.run();
        verify(mMockDelegate).onShowMorePasskeysSelected();
        assertThat(mModel.get(VISIBLE), is(false));
    }

    @Test
    public void testHidesConfirmationWhenMorePasskeysShown() {
        mMediator.showCredentials(
                TEST_URL,
                true,
                Collections.emptyList(),
                Arrays.asList(ANA),
                /* showMorePasskeys= */ true,
                /* submitCredential= */ false,
                /* managePasskeysHidesPasswords= */ false,
                /* showHybridPasskeyOption= */ true);
        ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
        assertThat(
                StreamSupport.stream(itemList.spliterator(), false)
                        .map((item) -> item.type)
                        .collect(Collectors.toList()),
                not(hasItem(ItemType.FILL_BUTTON)));
    }

    class TestImageFetcher extends ImageFetcher.ImageFetcherForTesting {
        private Callback<Bitmap> mCallback;

        private void answerWithBitmap() {
            mCallback.onResult(mBitmapFromImageFetcher);
            mCallback = null;
        }

        private void answerWithNull() {
            mCallback.onResult(null);
            mCallback = null;
        }

        @Override
        public void fetchImage(final ImageFetcher.Params params, Callback<Bitmap> callback) {
            mCallback = callback;
        }

        @Override
        public void fetchGif(final ImageFetcher.Params params, Callback<BaseGifImage> callback) {}

        @Override
        public void clear() {}

        @Override
        public @ImageFetcherConfig int getConfig() {
            return ImageFetcherConfig.IN_MEMORY_ONLY;
        }

        @Override
        public void destroy() {}
    }
}