chromium/chrome/android/features/keyboard_accessory/junit/src/org/chromium/chrome/browser/keyboard_accessory/ManualFillingControllerTest.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;

import static android.content.res.Configuration.HARDKEYBOARDHIDDEN_UNDEFINED;

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

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static org.chromium.chrome.browser.keyboard_accessory.AccessoryAction.GENERATE_PASSWORD_AUTOMATIC;
import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KEYBOARD_EXTENSION_STATE;
import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.EXTENDING_KEYBOARD;
import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.FLOATING_BAR;
import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.FLOATING_SHEET;
import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.HIDDEN;
import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.REPLACING_KEYBOARD;
import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.WAITING_TO_REPLACE;
import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.SHOULD_EXTEND_KEYBOARD;
import static org.chromium.chrome.browser.keyboard_accessory.ManualFillingProperties.SHOW_WHEN_VISIBLE;
import static org.chromium.chrome.browser.tab.Tab.INVALID_TAB_ID;
import static org.chromium.chrome.browser.tab.TabLaunchType.FROM_BROWSER_ACTIONS;
import static org.chromium.chrome.browser.tab.TabSelectionType.FROM_NEW;
import static org.chromium.chrome.browser.tab.TabSelectionType.FROM_USER;

import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.view.Surface;
import android.view.View;

import androidx.annotation.Nullable;

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.RuntimeEnvironment;
import org.robolectric.annotation.Config;

import org.chromium.base.UserDataHost;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.ChromeWindow;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.back_press.BackPressManager;
import org.chromium.chrome.browser.compositor.CompositorViewHolder;
import org.chromium.chrome.browser.fullscreen.BrowserControlsManager;
import org.chromium.chrome.browser.fullscreen.FullscreenManager;
import org.chromium.chrome.browser.fullscreen.FullscreenOptions;
import org.chromium.chrome.browser.keyboard_accessory.ManualFillingComponent.UpdateAccessorySheetDelegate;
import org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryCoordinator;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData.AccessorySheetData;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData.Action;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData.UserInfo;
import org.chromium.chrome.browser.keyboard_accessory.data.PropertyProvider;
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.AccessorySheetTabCoordinator;
import org.chromium.chrome.browser.password_manager.ConfirmationDialogHelper;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileJni;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.components.embedder_support.view.ContentView;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.InsetObserver;
import org.chromium.ui.base.ApplicationViewportInsetSupplier;
import org.chromium.ui.display.DisplayAndroid;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.mojom.VirtualKeyboardMode;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;

/** Controller tests for the root controller for interactions with the manual filling UI. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ManualFillingControllerTest {
    private static final int sKeyboardHeightDp = 100;
    private static final int sAccessoryHeightDp = 48;

    @Mock private ChromeWindow mMockWindow;
    @Mock private ChromeActivity mMockActivity;
    private WebContents mLastMockWebContents;
    @Mock private Profile mMockProfile;
    @Mock private Profile.Natives mProfileJniMock;
    @Mock private ContentView mMockContentView;
    @Mock private TabModelSelector mMockTabModelSelector;
    @Mock private android.content.res.Resources mMockResources;
    @Mock private KeyboardAccessoryCoordinator mMockKeyboardAccessory;
    @Mock private AccessorySheetCoordinator mMockAccessorySheet;
    @Mock private CompositorViewHolder mMockCompositorViewHolder;
    @Mock private BottomSheetController mMockBottomSheetController;
    @Mock private ManualFillingComponent.SoftKeyboardDelegate mMockSoftKeyboardDelegate;
    @Mock private ConfirmationDialogHelper mMockConfirmationHelper;
    @Mock private FullscreenManager mMockFullscreenManager;
    @Mock private InsetObserver mInsetObserver;
    @Mock private BackPressManager mMockBackPressManager;
    @Mock private EdgeToEdgeController mMockEdgeToEdgeController;

    @Rule public JniMocker mJniMocker = new JniMocker();

    @Captor ArgumentCaptor<FullscreenManager.Observer> mFullscreenObserverCaptor;

    private final ManualFillingCoordinator mController = new ManualFillingCoordinator();
    private final ManualFillingMediator mMediator = mController.getMediatorForTesting();
    private final ManualFillingStateCache mCache = mMediator.getStateCacheForTesting();
    private final PropertyModel mModel = mMediator.getModelForTesting();
    private final UserDataHost mUserDataHost = new UserDataHost();
    private final ApplicationViewportInsetSupplier mInsetSupplier =
            ApplicationViewportInsetSupplier.createForTests();
    private final ObservableSupplierImpl<Integer> mKeyboardInsetSupplier =
            new ObservableSupplierImpl<>();
    private final ObservableSupplierImpl<EdgeToEdgeController> mMockEdgeToEdgeControllerSupplier =
            new ObservableSupplierImpl<EdgeToEdgeController>();

    private static class MockActivityTabProvider extends ActivityTabProvider {
        public Tab mTab;

        @Override
        public void set(Tab tab) {
            mTab = tab;
        }

        @Override
        public Tab get() {
            return mTab;
        }
    }

    private final MockActivityTabProvider mActivityTabProvider = new MockActivityTabProvider();

    /**
     * Helper class that provides shortcuts to providing and observing AccessorySheetData and
     * Actions.
     */
    private static class SheetProviderHelper {
        private final PropertyProvider<Action[]> mActionListProvider =
                new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC);
        private final PropertyProvider<AccessorySheetData> mAccessorySheetDataProvider =
                new PropertyProvider<>();

        private final ArrayList<Action> mRecordedActions = new ArrayList<>();
        private int mRecordedActionNotifications;
        private final AtomicReference<AccessorySheetData> mRecordedSheetData =
                new AtomicReference<>();

        /**
         * Can be used to capture data from an observer. Retrieve the last captured data with {@link
         * #getRecordedActions()} and {@link #getFirstRecordedAction()}.
         *
         * @param unusedTypeId Unused but necessary to enable use as method reference.
         * @param item The {@link Action[]} provided by a {@link PropertyProvider<Action[]>}.
         */
        void record(int unusedTypeId, Action[] item) {
            mRecordedActionNotifications++;
            mRecordedActions.clear();
            mRecordedActions.addAll(Arrays.asList(item));
        }

        /**
         * Can be used to capture data from an observer. Retrieve the last captured data with {@link
         * #getRecordedSheetData()} and {@link #getFirstRecordedPassword()}.
         *
         * @param unusedTypeId Unused but necessary to enable use as method reference.
         * @param data The {@link AccessorySheetData} provided by a {@link PropertyProvider}.
         */
        void record(int unusedTypeId, AccessorySheetData data) {
            mRecordedSheetData.set(data);
        }

        /**
         * Uses the provider as returned by {@link #getActionListProvider()} to provide an Action.
         *
         * @param actionType The type for the provided generation action.
         */
        void provideAction(@AccessoryAction int actionType) {
            provideActions(new Action[] {new Action(actionType, action -> {})});
        }

        /**
         * Uses the provider as returned by {@link #getActionListProvider()} to provide Actions.
         *
         * @param actions The {@link Action}s to provide.
         */
        void provideActions(Action[] actions) {
            mActionListProvider.notifyObservers(actions);
        }

        /**
         * Uses the provider as returned by {@link #getSheetDataProvider()} to provide an simple
         * password sheet with one credential pair.
         *
         * @param passwordString The only provided password in the new sheet.
         */
        void providePasswordSheet(String passwordString) {
            AccessorySheetData sheetData =
                    new AccessorySheetData(
                            AccessoryTabType.PASSWORDS,
                            /* userInfoTitle= */ "Passwords",
                            /* plusAddressTitle= */ "",
                            /* warning= */ "");
            UserInfo userInfo = new UserInfo("", false);
            userInfo.addField(
                    new UserInfoField("(No username)", "No username", /* id= */ "", false, null));
            userInfo.addField(
                    new UserInfoField(passwordString, "Password", /* id= */ "", true, null));
            sheetData.getUserInfoList().add(userInfo);
            mAccessorySheetDataProvider.notifyObservers(sheetData);
        }

        /**
         * @return The {@link Action} last captured with {@link #record(int, Action[])}.
         */
        Action getFirstRecordedAction() {
            int firstNonTabLayoutAction = 0;
            assert mRecordedActions.size() >= firstNonTabLayoutAction;
            return mRecordedActions.get(firstNonTabLayoutAction);
        }

        /**
         * @return First password in a sheet captured by {@link #record(int, AccessorySheetData)}.
         */
        String getFirstRecordedPassword() {
            assert getRecordedSheetData() != null;
            assert getRecordedSheetData().getUserInfoList() != null;
            UserInfo info = getRecordedSheetData().getUserInfoList().get(0);
            assert info != null;
            assert info.getFields() != null;
            assert info.getFields().size() > 1;
            return info.getFields().get(1).getDisplayText();
        }

        /**
         * @return True if {@link #record(int, Action[])} was notified.
         */
        boolean hasRecordedActions() {
            return mRecordedActionNotifications > 0;
        }

        /**
         * @return The {@link Action}s last captured with {@link #record(int, Action[])}.
         */
        ArrayList<Action> getRecordedActions() {
            return mRecordedActions;
        }

        /**
         * @return {@link AccessorySheetData} captured by {@link #record(int, AccessorySheetData)}.
         */
        AccessorySheetData getRecordedSheetData() {
            return mRecordedSheetData.get();
        }

        /**
         * The returned provider is the same used by {@link #provideActions(Action[])}.
         *
         * @return A {@link PropertyProvider}.
         */
        PropertyProvider<Action[]> getActionListProvider() {
            return mActionListProvider;
        }

        /**
         * The returned provider is the same used by {@link #providePasswordSheet(String)}.
         *
         * @return A {@link PropertyProvider}.
         */
        PropertyProvider<AccessorySheetData> getSheetDataProvider() {
            return mAccessorySheetDataProvider;
        }
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        when(mMockWindow.getActivity()).thenReturn(new WeakReference<>(mMockActivity));
        mInsetSupplier.setKeyboardInsetSupplier(mKeyboardInsetSupplier);
        mInsetSupplier.setKeyboardAccessoryInsetSupplier(mController.getBottomInsetSupplier());
        when(mMockWindow.getApplicationBottomInsetSupplier()).thenReturn(mInsetSupplier);
        when(mMockSoftKeyboardDelegate.calculateSoftKeyboardHeight(any())).thenReturn(0);
        when(mMockActivity.getTabModelSelector()).thenReturn(mMockTabModelSelector);
        when(mMockActivity.getActivityTabProvider()).thenReturn(mActivityTabProvider);
        BrowserControlsManager browserControlsManager =
                new BrowserControlsManager(mMockActivity, 0);
        when(mMockActivity.getBrowserControlsManager()).thenReturn(browserControlsManager);
        when(mMockActivity.getFullscreenManager()).thenReturn(mMockFullscreenManager);
        doNothing().when(mMockFullscreenManager).addObserver(mFullscreenObserverCaptor.capture());
        ObservableSupplierImpl<CompositorViewHolder> compositorViewHolderSupplier =
                new ObservableSupplierImpl<>();
        compositorViewHolderSupplier.set(mMockCompositorViewHolder);
        when(mMockActivity.getCompositorViewHolderSupplier())
                .thenReturn(compositorViewHolderSupplier);
        when(mMockActivity.getResources()).thenReturn(mMockResources);
        when(mMockActivity.getPackageManager())
                .thenReturn(RuntimeEnvironment.application.getPackageManager());
        when(mMockActivity.findViewById(android.R.id.content)).thenReturn(mMockContentView);
        when(mMockContentView.getRootView()).thenReturn(mock(View.class));
        mLastMockWebContents = mock(WebContents.class);
        when(mMockActivity.getCurrentWebContents()).then(i -> mLastMockWebContents);

        mJniMocker.mock(ProfileJni.TEST_HOOKS, mProfileJniMock);
        when(mProfileJniMock.fromWebContents(any())).thenReturn(mMockProfile);

        when(mMockWindow.getInsetObserver()).thenReturn(mInsetObserver);
        simulateLayoutSizeChange(
                2.f, 80, 128, /* keyboardShown= */ false, VirtualKeyboardMode.RESIZES_VISUAL);
        Configuration config = new Configuration();
        config.hardKeyboardHidden = HARDKEYBOARDHIDDEN_UNDEFINED;
        when(mMockResources.getConfiguration()).thenReturn(config);
        AccessorySheetTabCoordinator.IconProvider.setIconForTesting(mock(Drawable.class));
        doNothing()
                .when(mMockBackPressManager)
                .addHandler(any(), eq(BackPressHandler.Type.MANUAL_FILLING));
        when(mMockEdgeToEdgeController.getBottomInset()).thenReturn(0);
        mMockEdgeToEdgeControllerSupplier.set(mMockEdgeToEdgeController);
        mController.initialize(
                mMockWindow,
                mMockKeyboardAccessory,
                mMockAccessorySheet,
                mMockBottomSheetController,
                /* isContextualSearchOpened= */ () -> false,
                mMockBackPressManager,
                mMockEdgeToEdgeControllerSupplier,
                mMockSoftKeyboardDelegate,
                mMockConfirmationHelper);
    }

    @Test
    public void testCreatesValidSubComponents() {
        assertThat(mController, is(notNullValue()));
        assertThat(mMediator, is(notNullValue()));
        assertThat(mCache, is(notNullValue()));
    }

    @Test
    public void testAddingNewTabIsAddedToAccessoryAndSheet() {
        // Clear any calls that happened during initialization:
        reset(mMockKeyboardAccessory);
        reset(mMockAccessorySheet);

        // Create a new tab with a passwords tab:
        addBrowserTab(mMediator, 1111, null);

        // Registering a provider creates a new passwords tab:
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());

        // Now check the how many tabs were sent to the sub components:
        ArgumentCaptor<KeyboardAccessoryData.Tab[]> barTabCaptor =
                ArgumentCaptor.forClass(KeyboardAccessoryData.Tab[].class);
        ArgumentCaptor<KeyboardAccessoryData.Tab[]> sheetTabCaptor =
                ArgumentCaptor.forClass(KeyboardAccessoryData.Tab[].class);
        verify(mMockKeyboardAccessory, times(2)).setTabs(barTabCaptor.capture());
        verify(mMockAccessorySheet, times(2)).setTabs(sheetTabCaptor.capture());

        // Initial empty state:
        assertThat(barTabCaptor.getAllValues().get(0).length, is(0));
        assertThat(sheetTabCaptor.getAllValues().get(0).length, is(0));

        // When creating the password sheet:
        assertThat(barTabCaptor.getAllValues().get(1).length, is(1));
        assertThat(sheetTabCaptor.getAllValues().get(1).length, is(1));
    }

    @Test
    public void testAddingBrowserTabsCreatesValidAccessoryState() {
        // Emulate adding a browser tab. Expect the model to have another entry.
        Tab firstTab = addBrowserTab(mMediator, 1111, null);
        ManualFillingState firstState = mCache.getStateFor(firstTab);
        assertThat(firstState, notNullValue());

        // Emulate adding a second browser tab. Expect the model to have another entry.
        Tab secondTab = addBrowserTab(mMediator, 2222, firstTab);
        ManualFillingState secondState = mCache.getStateFor(secondTab);
        assertThat(secondState, notNullValue());

        assertThat(firstState, not(equalTo(secondState)));
    }

    @Test
    public void testPasswordItemsPersistAfterSwitchingBrowserTabs() {
        SheetProviderHelper firstTabHelper = new SheetProviderHelper();
        SheetProviderHelper secondTabHelper = new SheetProviderHelper();
        UpdateAccessorySheetDelegate firstSheetUpdater = mock(UpdateAccessorySheetDelegate.class);
        UpdateAccessorySheetDelegate secondSheetUpdater = mock(UpdateAccessorySheetDelegate.class);

        // Simulate opening a new tab which automatically triggers the registration:
        Tab firstTab = addBrowserTab(mMediator, 1111, null);
        mController.registerSheetDataProvider(
                mLastMockWebContents,
                AccessoryTabType.PASSWORDS,
                firstTabHelper.getSheetDataProvider());
        mController.registerSheetUpdateDelegate(mLastMockWebContents, firstSheetUpdater);
        getStateForBrowserTab()
                .getSheetDataProvider(AccessoryTabType.PASSWORDS)
                .addObserver(firstTabHelper::record);
        firstTabHelper.providePasswordSheet("FirstPassword");
        assertThat(firstTabHelper.getFirstRecordedPassword(), is("FirstPassword"));

        // Simulate creating a second tab:
        Tab secondTab = addBrowserTab(mMediator, 2222, firstTab);
        mController.registerSheetDataProvider(
                mLastMockWebContents,
                AccessoryTabType.PASSWORDS,
                secondTabHelper.getSheetDataProvider());
        mController.registerSheetUpdateDelegate(mLastMockWebContents, secondSheetUpdater);
        getStateForBrowserTab()
                .getSheetDataProvider(AccessoryTabType.PASSWORDS)
                .addObserver(secondTabHelper::record);
        secondTabHelper.providePasswordSheet("SecondPassword");
        assertThat(secondTabHelper.getFirstRecordedPassword(), is("SecondPassword"));

        // Simulate switching back to the first tab:
        switchBrowserTab(mMediator, /* from= */ secondTab, /* to= */ firstTab);
        // Wiring affects the same sheet only and is triggered after switching
        verify(firstSheetUpdater).requestSheet(AccessoryTabType.PASSWORDS);
        firstTabHelper.providePasswordSheet("FirstPassword");
        assertThat(firstTabHelper.getFirstRecordedPassword(), is("FirstPassword"));

        // And back to the second:
        switchBrowserTab(mMediator, /* from= */ firstTab, /* to= */ secondTab);
        // Wiring affects the same sheet only and is triggered after switching
        verify(secondSheetUpdater).requestSheet(AccessoryTabType.PASSWORDS);
        secondTabHelper.providePasswordSheet("SecondPassword");
        assertThat(secondTabHelper.getFirstRecordedPassword(), is("SecondPassword"));
    }

    @Test
    public void testKeyboardAccessoryActionsPersistAfterSwitchingBrowserTabs() {
        SheetProviderHelper firstTabHelper = new SheetProviderHelper();
        SheetProviderHelper secondTabHelper = new SheetProviderHelper();

        // Simulate opening a new tab which automatically triggers the registration:
        Tab firstTab = addBrowserTab(mMediator, 1111, null);
        mController.registerActionProvider(
                mLastMockWebContents, firstTabHelper.getActionListProvider());
        getStateForBrowserTab().getActionsProvider().addObserver(firstTabHelper::record);
        firstTabHelper.provideAction(AccessoryAction.GENERATE_PASSWORD_AUTOMATIC);
        assertThat(
                firstTabHelper.getFirstRecordedAction().getActionType(),
                is(AccessoryAction.GENERATE_PASSWORD_AUTOMATIC));

        // Simulate creating a second tab:
        Tab secondTab = addBrowserTab(mMediator, 2222, firstTab);
        mController.registerActionProvider(
                mLastMockWebContents, secondTabHelper.getActionListProvider());
        getStateForBrowserTab().getActionsProvider().addObserver(secondTabHelper::record);
        secondTabHelper.provideActions(new Action[0]);
        assertThat(secondTabHelper.getRecordedActions().size(), is(0));

        // Simulate switching back to the first tab:
        switchBrowserTab(mMediator, /* from= */ secondTab, /* to= */ firstTab);
        assertThat(
                firstTabHelper.getFirstRecordedAction().getActionType(),
                is(AccessoryAction.GENERATE_PASSWORD_AUTOMATIC));

        // And back to the second:
        switchBrowserTab(mMediator, /* from= */ firstTab, /* to= */ secondTab);
        assertThat(secondTabHelper.getRecordedActions().size(), is(0));
    }

    @Test
    public void testPasswordTabRestoredWhenSwitchingBrowserTabs() {
        // Clear any calls that happened during initialization:
        reset(mMockKeyboardAccessory);
        reset(mMockAccessorySheet);

        // Create a new tab:
        Tab firstTab = addBrowserTab(mMediator, 1111, null);

        // Create a new passwords tab:
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());

        // Simulate creating a second tab without any tabs:
        Tab secondTab = addBrowserTab(mMediator, 2222, firstTab);

        // Simulate switching back to the first tab:
        switchBrowserTab(mMediator, /* from= */ secondTab, /* to= */ firstTab);

        // And back to the second:
        switchBrowserTab(mMediator, /* from= */ firstTab, /* to= */ secondTab);

        ArgumentCaptor<KeyboardAccessoryData.Tab[]> barTabCaptor =
                ArgumentCaptor.forClass(KeyboardAccessoryData.Tab[].class);
        ArgumentCaptor<KeyboardAccessoryData.Tab[]> sheetTabCaptor =
                ArgumentCaptor.forClass(KeyboardAccessoryData.Tab[].class);
        verify(mMockKeyboardAccessory, times(5)).setTabs(barTabCaptor.capture());
        verify(mMockAccessorySheet, times(5)).setTabs(sheetTabCaptor.capture());

        // Initial empty state:
        assertThat(barTabCaptor.getAllValues().get(0).length, is(0));
        assertThat(sheetTabCaptor.getAllValues().get(0).length, is(0));

        // When creating the password sheet in 1st tab:
        assertThat(barTabCaptor.getAllValues().get(1).length, is(1));
        assertThat(sheetTabCaptor.getAllValues().get(1).length, is(1));

        // When switching to empty 2nd tab:
        assertThat(barTabCaptor.getAllValues().get(2).length, is(0));
        assertThat(sheetTabCaptor.getAllValues().get(2).length, is(0));

        // When switching back to 1st tab with password sheet:
        assertThat(barTabCaptor.getAllValues().get(3).length, is(1));
        assertThat(sheetTabCaptor.getAllValues().get(3).length, is(1));

        // When switching back to empty 2nd tab:
        assertThat(barTabCaptor.getAllValues().get(4).length, is(0));
        assertThat(sheetTabCaptor.getAllValues().get(4).length, is(0));
    }

    @Test
    public void testPasswordTabRestoredWhenClosingTabIsUndone() {
        // Clear any calls that happened during initialization:
        reset(mMockKeyboardAccessory);
        reset(mMockAccessorySheet);

        // Create a new tab with a passwords tab:
        Tab tab = addBrowserTab(mMediator, 1111, null);

        // Create a new passwords tab:
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());

        // Simulate closing the tab (uncommitted):
        mMediator.getTabModelObserverForTesting().willCloseTab(tab, true);
        mMediator.getTabObserverForTesting().onHidden(tab, TabHidingType.CHANGED_TABS);
        getStateForBrowserTab().getWebContentsObserverForTesting().wasHidden();
        // The state should be kept if the closure wasn't committed.
        assertThat(getStateForBrowserTab().getTabs().length, is(1));
        mLastMockWebContents = null;

        // Simulate undo closing the tab and selecting it:
        mMediator.getTabModelObserverForTesting().tabClosureUndone(tab);
        switchBrowserTab(mMediator, null, tab);

        // Simulate closing the tab and committing to it (i.e. wait out undo message):
        WebContents oldWebContents = mLastMockWebContents;
        closeBrowserTab(mMediator, tab);
        // The state should be cleaned up, now that it was committed.
        assertThat(
                mMediator.getStateCacheForTesting().getStateFor(oldWebContents).getTabs().length,
                is(0));

        ArgumentCaptor<KeyboardAccessoryData.Tab[]> barTabCaptor =
                ArgumentCaptor.forClass(KeyboardAccessoryData.Tab[].class);
        ArgumentCaptor<KeyboardAccessoryData.Tab[]> sheetTabCaptor =
                ArgumentCaptor.forClass(KeyboardAccessoryData.Tab[].class);
        verify(mMockKeyboardAccessory, times(4)).setTabs(barTabCaptor.capture());
        verify(mMockAccessorySheet, times(4)).setTabs(sheetTabCaptor.capture());

        // Initial empty state:
        assertThat(barTabCaptor.getAllValues().get(0).length, is(0));
        assertThat(sheetTabCaptor.getAllValues().get(0).length, is(0));

        // When creating the password sheet:
        assertThat(barTabCaptor.getAllValues().get(1).length, is(1));
        assertThat(sheetTabCaptor.getAllValues().get(1).length, is(1));

        // When restoring the tab:
        assertThat(barTabCaptor.getAllValues().get(2).length, is(1));
        assertThat(sheetTabCaptor.getAllValues().get(2).length, is(1));

        // When committing to close the tab:
        assertThat(barTabCaptor.getAllValues().get(3).length, is(0));
        assertThat(sheetTabCaptor.getAllValues().get(3).length, is(0));
    }

    @Test
    public void testTreatNeverProvidedActionsAsEmptyActionList() {
        SheetProviderHelper firstTabHelper = new SheetProviderHelper();
        SheetProviderHelper secondTabHelper = new SheetProviderHelper();

        // Open a tab.
        Tab tab = addBrowserTab(mMediator, 1111, null);
        // Add an action provider that never provides any actions.
        mController.registerActionProvider(
                mLastMockWebContents, new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC));
        getStateForBrowserTab().getActionsProvider().addObserver(firstTabHelper::record);

        // Create a new tab with an action:
        Tab secondTab = addBrowserTab(mMediator, 1111, tab);
        mController.registerActionProvider(
                mLastMockWebContents, secondTabHelper.getActionListProvider());
        getStateForBrowserTab().getActionsProvider().addObserver(secondTabHelper::record);
        secondTabHelper.provideAction(AccessoryAction.CREDMAN_CONDITIONAL_UI_REENTRY);
        assertThat(
                secondTabHelper.getFirstRecordedAction().getActionType(),
                is(AccessoryAction.CREDMAN_CONDITIONAL_UI_REENTRY));

        // Switching back should notify the accessory about the still empty state of the accessory.
        switchBrowserTab(mMediator, secondTab, tab);
        assertThat(firstTabHelper.hasRecordedActions(), is(true));
        assertThat(firstTabHelper.getRecordedActions().size(), is(0));
    }

    @Test
    public void testUpdatesInactiveAccessory() {
        SheetProviderHelper delayedTabHelper = new SheetProviderHelper();
        SheetProviderHelper secondTabHelper = new SheetProviderHelper();

        // Open a tab.
        Tab delayedTab = addBrowserTab(mMediator, 1111, null);
        // Add an action provider that hasn't provided actions yet.
        mController.registerActionProvider(
                mLastMockWebContents, delayedTabHelper.getActionListProvider());
        getStateForBrowserTab().getActionsProvider().addObserver(delayedTabHelper::record);
        assertThat(delayedTabHelper.hasRecordedActions(), is(false));

        // Create and switch to a new tab:
        Tab secondTab = addBrowserTab(mMediator, 2222, delayedTab);
        mController.registerActionProvider(
                mLastMockWebContents, secondTabHelper.getActionListProvider());
        getStateForBrowserTab().getActionsProvider().addObserver(secondTabHelper::record);

        // And provide data to the active browser tab.
        secondTabHelper.provideAction(AccessoryAction.CREDMAN_CONDITIONAL_UI_REENTRY);
        // Now, have the delayed provider provide data for the backgrounded browser tab.
        delayedTabHelper.provideAction(AccessoryAction.GENERATE_PASSWORD_AUTOMATIC);

        // The current tab should not be influenced by the delayed provider.
        assertThat(secondTabHelper.getRecordedActions().size(), is(1));
        assertThat(
                secondTabHelper.getFirstRecordedAction().getActionType(),
                is(AccessoryAction.CREDMAN_CONDITIONAL_UI_REENTRY));

        // Switching tabs back should only show the action that was received in the background.
        switchBrowserTab(mMediator, secondTab, delayedTab);
        assertThat(delayedTabHelper.getRecordedActions().size(), is(1));
        assertThat(
                delayedTabHelper.getFirstRecordedAction().getActionType(),
                is(AccessoryAction.GENERATE_PASSWORD_AUTOMATIC));
    }

    @Test
    public void testDestroyingTabCleansModelForThisTab() {
        // Clear any calls that happened during initialization:
        reset(mMockKeyboardAccessory);
        reset(mMockAccessorySheet);
        SheetProviderHelper firstTabHelper = new SheetProviderHelper();
        SheetProviderHelper secondTabHelper = new SheetProviderHelper();
        UpdateAccessorySheetDelegate secondSheetUpdater = mock(UpdateAccessorySheetDelegate.class);

        // Simulate opening a new tab:
        Tab firstTab = addBrowserTab(mMediator, 1111, null);
        mController.registerSheetDataProvider(
                mLastMockWebContents,
                AccessoryTabType.PASSWORDS,
                firstTabHelper.getSheetDataProvider());
        mController.registerActionProvider(
                mLastMockWebContents, firstTabHelper.getActionListProvider());
        getStateForBrowserTab()
                .getSheetDataProvider(AccessoryTabType.PASSWORDS)
                .addObserver(firstTabHelper::record);
        getStateForBrowserTab().getActionsProvider().addObserver(firstTabHelper::record);
        firstTabHelper.providePasswordSheet("FirstPassword");
        firstTabHelper.provideAction(AccessoryAction.CREDMAN_CONDITIONAL_UI_REENTRY);

        // Create and switch to a new tab: (because destruction shouldn't rely on tab to be active)
        Tab secondTab = addBrowserTab(mMediator, 2222, firstTab);
        mController.registerSheetDataProvider(
                mLastMockWebContents,
                AccessoryTabType.PASSWORDS,
                secondTabHelper.getSheetDataProvider());
        mController.registerSheetUpdateDelegate(mLastMockWebContents, secondSheetUpdater);
        mController.registerActionProvider(
                mLastMockWebContents, secondTabHelper.getActionListProvider());
        getStateForBrowserTab()
                .getSheetDataProvider(AccessoryTabType.PASSWORDS)
                .addObserver(secondTabHelper::record);
        getStateForBrowserTab().getActionsProvider().addObserver(secondTabHelper::record);
        secondTabHelper.providePasswordSheet("SecondPassword");
        secondTabHelper.provideAction(AccessoryAction.CREDMAN_CONDITIONAL_UI_REENTRY);

        // The newly created tab should be valid.
        assertThat(secondTabHelper.getFirstRecordedPassword(), is("SecondPassword"));
        assertThat(
                secondTabHelper.getFirstRecordedAction().getActionType(),
                is(AccessoryAction.CREDMAN_CONDITIONAL_UI_REENTRY));

        // Request destruction of the first Tab:
        mMediator.getTabObserverForTesting().onDestroyed(firstTab);

        // The current tab should not be influenced by the destruction...
        // Wiring affects the same sheet only and is triggered after switching
        verify(secondSheetUpdater).requestSheet(AccessoryTabType.PASSWORDS);
        secondTabHelper.providePasswordSheet("SecondPassword");
        assertThat(secondTabHelper.getFirstRecordedPassword(), is("SecondPassword"));
        assertThat(
                secondTabHelper.getFirstRecordedAction().getActionType(),
                is(AccessoryAction.CREDMAN_CONDITIONAL_UI_REENTRY));
        assertThat(getStateForBrowserTab(), is(mCache.getStateFor(secondTab)));
        // ... but the other tab's data should be gone.
        assertThat(mCache.getStateFor(firstTab).getActionsProvider(), nullValue());
        assertThat(mCache.getStateFor(firstTab).getTabs().length, is(0));
    }

    @Test
    public void testDisplaysAccessoryOnlyWhenSpaceIsSufficient() {
        reset(mMockKeyboardAccessory);

        addBrowserTab(mMediator, 1234, null);
        SheetProviderHelper tabHelper = new SheetProviderHelper();
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, tabHelper.getSheetDataProvider());
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(any(), any())).thenReturn(true);
        mKeyboardInsetSupplier.set(sKeyboardHeightDp * /* density= */ 2);
        when(mMockSoftKeyboardDelegate.calculateSoftKeyboardHeight(any()))
                .thenReturn(sKeyboardHeightDp * /* density= */ 2);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);

        // Show the accessory bar for the default dimensions ([email protected]).
        mController.show(true);
        verify(mMockKeyboardAccessory).show();

        // The accessory is shown and the content area plus bar size don't exceed the threshold.
        simulateLayoutSizeChange(
                2.f, 180, 128, /* keyboardShown= */ true, VirtualKeyboardMode.RESIZES_VISUAL);

        verify(mMockKeyboardAccessory, never()).dismiss();
    }

    @Test
    public void testDisplaysAccessoryOnlyWhenSpaceIsSufficient_KeyboardResizesContent() {
        mInsetSupplier.setVirtualKeyboardMode(VirtualKeyboardMode.RESIZES_CONTENT);
        reset(mMockKeyboardAccessory);

        addBrowserTab(mMediator, 1234, null);
        SheetProviderHelper tabHelper = new SheetProviderHelper();
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, tabHelper.getSheetDataProvider());
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(any(), any())).thenReturn(true);
        mKeyboardInsetSupplier.set(sKeyboardHeightDp * /* density= */ 2);
        when(mMockSoftKeyboardDelegate.calculateSoftKeyboardHeight(any()))
                .thenReturn(sKeyboardHeightDp * /* density= */ 2);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);

        // Show the accessory bar for the default dimensions ([email protected]).
        mController.show(true);
        verify(mMockKeyboardAccessory).show();

        // The accessory is shown and the content area plus bar size don't exceed the threshold.
        simulateLayoutSizeChange(
                2.f, 180, 128, /* keyboardShown= */ true, VirtualKeyboardMode.RESIZES_CONTENT);

        verify(mMockKeyboardAccessory, never()).dismiss();
    }

    @Test
    public void testHidesAccessoryAfterRotation() {
        reset(mMockKeyboardAccessory);
        setContentAreaDimensions(2.f, 180, 320);
        addBrowserTab(mMediator, 1234, null);
        SheetProviderHelper tabHelper = new SheetProviderHelper();
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, tabHelper.getSheetDataProvider());
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(any(), any())).thenReturn(true);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);

        mController.show(true);
        setContentAreaDimensions(2.f, 180, 220);
        mMediator.onLayoutChange(mMockContentView, 0, 0, 540, 360, 0, 0, 640, 360);
        verify(mMockKeyboardAccessory).show();
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), not(is(HIDDEN)));

        // Rotating the screen causes a relayout:
        setContentAreaDimensions(2.f, 320, 128, Surface.ROTATION_90);
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(eq(mMockActivity), any()))
                .thenReturn(false);
        mMediator.onLayoutChange(mMockContentView, 0, 0, 160, 640, 0, 0, 540, 360);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(HIDDEN));
    }

    @Test
    public void testDisplaysAccessoryOnlyWhenVerticalSpaceIsSufficient() {
        mInsetSupplier.setVirtualKeyboardMode(VirtualKeyboardMode.RESIZES_CONTENT);
        reset(mMockKeyboardAccessory);
        addBrowserTab(mMediator, 1234, null);
        SheetProviderHelper tabHelper = new SheetProviderHelper();
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, tabHelper.getSheetDataProvider());
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(eq(mMockActivity), any()))
                .thenReturn(true);
        mKeyboardInsetSupplier.set(sKeyboardHeightDp * /* density= */ 2);
        when(mMockSoftKeyboardDelegate.calculateSoftKeyboardHeight(any()))
                .thenReturn(sKeyboardHeightDp * /* density= */ 2);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);

        // Show the accessory bar for the dimensions exactly at the threshold: [email protected].
        simulateLayoutSizeChange(
                2.0f, 300, 128, /* keyboardShown= */ true, VirtualKeyboardMode.RESIZES_CONTENT);
        mController.show(true);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), not(is(HIDDEN)));
        verify(mMockKeyboardAccessory).show();

        // The height is now reduced by the 48dp high accessory -- it should remain visible.
        simulateLayoutSizeChange(
                2.0f, 300, 128, /* keyboardShown= */ true, VirtualKeyboardMode.RESIZES_CONTENT);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), not(is(HIDDEN)));

        // Use a height that is too small but with a valid width (e.g. resized multi-window window).
        simulateLayoutSizeChange(
                2.0f, 300, 127, /* keyboardShown= */ true, VirtualKeyboardMode.RESIZES_CONTENT);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(HIDDEN));

        // Also test in RESIZES_VISUAL mode where the keyboard and accessory won't resize the
        // WebContents.
        mInsetSupplier.setVirtualKeyboardMode(VirtualKeyboardMode.RESIZES_VISUAL);
        simulateLayoutSizeChange(
                2.0f, 300, 127, /* keyboardShown= */ true, VirtualKeyboardMode.RESIZES_VISUAL);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(HIDDEN));

        simulateLayoutSizeChange(
                2.0f, 300, 128, /* keyboardShown= */ true, VirtualKeyboardMode.RESIZES_VISUAL);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), not(is(HIDDEN)));
    }

    @Test
    public void testDisplaysAccessoryOnlyWhenHorizontalSpaceIsSufficient() {
        reset(mMockKeyboardAccessory);

        addBrowserTab(mMediator, 1234, null);
        SheetProviderHelper tabHelper = new SheetProviderHelper();
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, tabHelper.getSheetDataProvider());
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(eq(mMockActivity), any()))
                .thenReturn(true);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);

        // Show the accessory bar for the dimensions exactly at the threshold: [email protected].
        simulateLayoutSizeChange(
                2.0f, 180, 128, /* keyboardShown= */ true, VirtualKeyboardMode.RESIZES_VISUAL);
        mController.show(true);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), not(is(HIDDEN)));

        // Use a width that is too small but with a valid height (e.g. resized multi-window window).
        simulateLayoutSizeChange(
                2.0f, 179, 128, /* keyboardShown= */ true, VirtualKeyboardMode.RESIZES_VISUAL);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(HIDDEN));
    }

    /**
     * This tests the case where an accessory sheet is showing instead of a keyboard. The screen is
     * rotated so that the amount of vertical space shrinks below the minimum allowed. Confirm that
     * the accessory sheet's height is shrunken.
     */
    @Test
    public void testRestrictsSheetSizeIfVerticalSpaceChanges() {
        final int density = 2;
        final int accessorySheetHeightDp = 100; // The height of a large keyboard.
        final int minimumVisibleHeightDp = 128; // This is a constant from ManualFillingMediator.
        final int initialWidthDp = 200;
        final int initialHeightDp = 300;

        addBrowserTab(mMediator, 1234, null);

        // Resize the screen to [email protected].
        simulateLayoutSizeChange(
                density,
                initialWidthDp,
                initialHeightDp,
                /* keyboardShown= */ false,
                VirtualKeyboardMode.RESIZES_VISUAL);

        // Now simulate showing the accessory sheet.
        when(mMockKeyboardAccessory.empty()).thenReturn(false);
        when(mMockKeyboardAccessory.isShown()).thenReturn(true);
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true);
        when(mMockAccessorySheet.getHeight()).thenReturn(accessorySheetHeightDp * density);
        mModel.set(SHOW_WHEN_VISIBLE, true);
        mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_SHEET);
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());
        reset(mMockKeyboardAccessory, mMockAccessorySheet);

        when(mMockKeyboardAccessory.empty()).thenReturn(false);
        when(mMockKeyboardAccessory.isShown()).thenReturn(true);
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true);
        when(mMockAccessorySheet.isShown()).thenReturn(true);
        when(mMockAccessorySheet.getHeight()).thenReturn(accessorySheetHeightDp * density);

        // Set layout as if it was rotated: 300x200@2f. The sheet does not inset WebContents since
        // we're in the default RESIZES_VISUAL VirtualKeyboardMode. Even though contentsHeightDp >
        // minimumVisibleHeightDp, test that the visible area is correctly deduced to be 200 - 100 <
        // minimumVisibleHeightDp so the sheet should be restricted in height.
        assertEquals(
                (int) mController.getBottomInsetSupplier().get(), accessorySheetHeightDp * density);
        simulateLayoutSizeChange(
                density,
                initialHeightDp,
                initialWidthDp,
                /* keyboardShown= */ false,
                VirtualKeyboardMode.RESIZES_VISUAL);
        assertEquals(mLastMockWebContents.getHeight(), initialWidthDp);

        // 200 - 128 = 72
        int expectedSheetHeightDp = initialWidthDp - minimumVisibleHeightDp;
        verify(mMockAccessorySheet).setHeight(density * expectedSheetHeightDp);
    }

    /**
     * This tests the case where an accessory sheet is showing instead of a keyboard. The screen is
     * rotated so that the amount of vertical space shrinks below the minimum allowed. Confirm that
     * the accessory sheet's height is shrunken.
     *
     * <p>This is the same test as above but with the keyboard in RESIZES_CONTENT mode, so that the
     * WebContents height is insetted by the keyboard and its accessories.
     */
    @Test
    public void testRestrictsSheetSizeIfVerticalSpaceChangesWithResizesContent() {
        final int density = 2;
        final int accessorySheetHeightDp = 100; // The height of a large keyboard.
        final int minimumVisibleHeightDp = 128; // This is a constant from ManualFillingMediator.
        final int initialWidthDp = 200;
        final int initialHeightDp = 300;

        mInsetSupplier.setVirtualKeyboardMode(VirtualKeyboardMode.RESIZES_CONTENT);
        addBrowserTab(mMediator, 1234, null);
        // Resize the screen to [email protected].
        simulateLayoutSizeChange(
                density,
                initialWidthDp,
                initialHeightDp,
                /* keyboardShown= */ false,
                VirtualKeyboardMode.RESIZES_CONTENT);

        // Now simulate showing the accessory sheet.
        when(mMockKeyboardAccessory.empty()).thenReturn(false);
        when(mMockKeyboardAccessory.isShown()).thenReturn(true);
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true);
        when(mMockAccessorySheet.getHeight()).thenReturn(accessorySheetHeightDp * density);
        mModel.set(SHOW_WHEN_VISIBLE, true);
        mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_SHEET);
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());
        reset(mMockKeyboardAccessory, mMockAccessorySheet);

        when(mMockKeyboardAccessory.empty()).thenReturn(false);
        when(mMockKeyboardAccessory.isShown()).thenReturn(true);
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true);
        when(mMockAccessorySheet.isShown()).thenReturn(true);
        when(mMockAccessorySheet.getHeight()).thenReturn(accessorySheetHeightDp * density);

        // Set layout as if it was rotated: 300x200@2f. Since we're in RESIZES_CONTENT mode, the
        // sheet will cause a resize to the web contents.  WebContents.getHeight <
        // minimumVisibleHeightDp so the sheet should be restricted in height.
        assertEquals(
                (int) mController.getBottomInsetSupplier().get(), accessorySheetHeightDp * density);
        simulateLayoutSizeChange(
                density,
                initialHeightDp,
                initialWidthDp,
                /* keyboardShown= */ false,
                VirtualKeyboardMode.RESIZES_CONTENT);
        assertEquals(mLastMockWebContents.getHeight(), initialWidthDp - accessorySheetHeightDp);

        // 200 - 128 = 72
        int expectedSheetHeightDp = initialWidthDp - minimumVisibleHeightDp;
        verify(mMockAccessorySheet).setHeight(density * expectedSheetHeightDp);
    }

    @Test
    public void testAdjustsOffsetAndHeightForFullscreen() {
        final int density = 2;
        // Turn off E2E mode
        mMockEdgeToEdgeControllerSupplier.set(null);

        mInsetSupplier.setVirtualKeyboardMode(VirtualKeyboardMode.RESIZES_CONTENT);
        Tab tab = addBrowserTab(mMediator, 1234, null);

        // Now simulate showing the accessory bar.
        when(mMockKeyboardAccessory.empty()).thenReturn(false);
        when(mMockKeyboardAccessory.isShown()).thenReturn(true);
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(false);
        mModel.set(SHOW_WHEN_VISIBLE, true);
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(eq(mMockActivity), any()))
                .thenReturn(true);
        mModel.set(KEYBOARD_EXTENSION_STATE, EXTENDING_KEYBOARD);

        // Ensure it's bottom-aligned and insetting the page with its height.
        assertEquals(
                sAccessoryHeightDp * density, (int) mController.getBottomInsetSupplier().get());
        verify(mMockKeyboardAccessory).setBottomOffset(0);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);

        // Simulate entering fullscreen mode which makes the keyboard overlaying.
        mFullscreenObserverCaptor
                .getValue()
                .onEnterFullscreen(tab, new FullscreenOptions(false, false));

        // Ensure it's not insetting the page.
        assertEquals(0, (int) mController.getBottomInsetSupplier().get());
    }

    @Test
    public void testAdjustsOffsetAndHeightForFullscreenOnE2EMode() {
        final int density = 2;

        mInsetSupplier.setVirtualKeyboardMode(VirtualKeyboardMode.RESIZES_CONTENT);
        Tab tab = addBrowserTab(mMediator, 1234, null);

        // Now simulate showing the accessory bar.
        when(mMockKeyboardAccessory.empty()).thenReturn(false);
        when(mMockKeyboardAccessory.isShown()).thenReturn(true);
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(false);

        mModel.set(SHOW_WHEN_VISIBLE, true);
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(eq(mMockActivity), any()))
                .thenReturn(true);
        mModel.set(KEYBOARD_EXTENSION_STATE, EXTENDING_KEYBOARD);

        // Ensure it's bottom-aligned and insetting the page with its height.
        assertEquals(
                sAccessoryHeightDp * density, (int) mController.getBottomInsetSupplier().get());
        verify(mMockKeyboardAccessory).setBottomOffset(0);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);

        // Simulate entering fullscreen mode which makes the keyboard overlaying.
        mFullscreenObserverCaptor
                .getValue()
                .onEnterFullscreen(tab, new FullscreenOptions(false, false));

        // Ensure it's not insetting the page.
        assertEquals(
                sAccessoryHeightDp * density, (int) mController.getBottomInsetSupplier().get());
    }

    @Test
    public void testIsFillingViewShownReturnsTargetValueAheadOfComponentUpdate() {
        // After initialization with one tab, the accessory sheet is closed.
        addBrowserTab(mMediator, 1234, null);
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(false);
        assertThat(mController.isFillingViewShown(null), is(false));

        // As soon as active tab and keyboard change, |isFillingViewShown| returns the expected
        // state - even if the sheet component wasn't updated yet.
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true);
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(eq(mMockActivity), any()))
                .thenReturn(false);
        assertThat(mController.isFillingViewShown(null), is(true));

        // The layout change impacts the component, but not the coordinator method.
        mMediator.onLayoutChange(null, 0, 0, 0, 0, 0, 0, 0, 0);
        assertThat(mController.isFillingViewShown(null), is(true));
    }

    @Test
    public void testTransitionToHiddenHidesEverything() {
        addBrowserTab(mMediator, 1111, null);
        // Make sure the model is in a non-HIDDEN state first.
        mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_SHEET);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);

        // Set the model HIDDEN. This should update keyboard and subcomponents.
        mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(HIDDEN));

        verify(mMockAccessorySheet).hide();
        verify(mMockKeyboardAccessory).closeActiveTab();
        verify(mMockKeyboardAccessory).dismiss();
        verify(mMockCompositorViewHolder).requestLayout(); // Triggered as if it was a keyboard.
    }

    @Test
    public void testTransitionToExtendingShowsBarAndHidesSheet() {
        addBrowserTab(mMediator, 1111, null);
        mModel.set(SHOW_WHEN_VISIBLE, true);
        // Make sure the model is in a non-EXTENDING_KEYBOARD state first.
        mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);

        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(eq(mMockActivity), any()))
                .thenReturn(true);
        // Set the model EXTENDING_KEYBOARD. This should update keyboard and subcomponents.
        mModel.set(KEYBOARD_EXTENSION_STATE, EXTENDING_KEYBOARD);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(EXTENDING_KEYBOARD));

        verify(mMockAccessorySheet).hide();
        verify(mMockKeyboardAccessory).closeActiveTab();
        verify(mMockKeyboardAccessory).show();
    }

    @Test
    public void testTransitionToFloatingBarShowsBarAndHidesSheet() {
        addBrowserTab(mMediator, 1111, null);
        mModel.set(SHOW_WHEN_VISIBLE, true);
        // Make sure the model is in a non-FLOATING_BAR state first.
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(eq(mMockActivity), any()))
                .thenReturn(false);
        mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);

        // Set the model FLOATING_BAR. This should update keyboard and subcomponents.
        mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_BAR);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_BAR));

        verify(mMockSoftKeyboardDelegate, atLeastOnce()).showSoftKeyboard(any());
        verify(mMockAccessorySheet).hide();
        verify(mMockKeyboardAccessory).closeActiveTab();
        verify(mMockCompositorViewHolder).requestLayout(); // Triggered as if it was a keyboard.
        verify(mMockKeyboardAccessory).show();
    }

    @Test
    public void testTransitionToFloatingBarWithShouldExtendKeyboardFalse() {
        addBrowserTab(mMediator, 1111, null);
        mModel.set(SHOW_WHEN_VISIBLE, true);
        // Make sure the model is in a non-FLOATING_BAR state first.
        mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);

        // Set the model FLOATING_BAR but not extend the keyboard with SHOULD_EXTEND_KEYBOARD
        mModel.set(SHOULD_EXTEND_KEYBOARD, false);
        mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_BAR);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_BAR));

        verify(mMockSoftKeyboardDelegate, never()).showSoftKeyboard(any());
        verify(mMockAccessorySheet).hide();
        verify(mMockKeyboardAccessory).closeActiveTab();
        verify(mMockKeyboardAccessory).show();
    }

    @Test
    public void testTransitionToFloatingSheetShowsSheet() {
        addBrowserTab(mMediator, 1111, null);
        // Make sure the model is in a non-FLOATING_SHEET state first.
        mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);

        // Set the model FLOATING_SHEET. This should update keyboard and subcomponents.
        mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_SHEET);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_SHEET));

        verify(mMockSoftKeyboardDelegate).showSoftKeyboard(any());
        verify(mMockAccessorySheet).show();
        verify(mMockKeyboardAccessory, never()).show();
    }

    @Test
    public void testTransitionToReplacingShowsSheet() {
        addBrowserTab(mMediator, 1111, null);
        // Make sure the model is in a non-REPLACING_KEYBOARD state first.
        mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);

        // Set the model REPLACING_KEYBOARD. This should update keyboard and subcomponents.
        mModel.set(KEYBOARD_EXTENSION_STATE, REPLACING_KEYBOARD);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(REPLACING_KEYBOARD));

        verify(mMockAccessorySheet).show();
        verify(mMockKeyboardAccessory, never()).show();
    }

    @Test
    public void testTransitionToWaitingHidesKeyboardAndShowsSheet() {
        addBrowserTab(mMediator, 1111, null);
        // Make sure the model is in a non-REPLACING_KEYBOARD state first.
        mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);

        // Set the model REPLACING_KEYBOARD. This should update keyboard and subcomponents.
        mModel.set(KEYBOARD_EXTENSION_STATE, WAITING_TO_REPLACE);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(WAITING_TO_REPLACE));

        verify(mMockSoftKeyboardDelegate).hideSoftKeyboardOnly(any());
        verify(mMockAccessorySheet, never()).hide();
        verify(mMockKeyboardAccessory, never()).closeActiveTab();
        verify(mMockKeyboardAccessory, never()).show();
    }

    @Test
    public void testTransitionFromHiddenToExtendingByKeyboard() {
        // Prepare a tab and register a new tab, so there is a reason to display the bar.
        addBrowserTab(mMediator, 1111, null);
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());
        mModel.set(SHOW_WHEN_VISIBLE, true);
        mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);

        // Showing the keyboard should now trigger a transition into EXTENDING state.
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(any(), any())).thenReturn(true);
        mMediator.onLayoutChange(mMockContentView, 0, 0, 320, 90, 0, 0, 320, 180);

        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(EXTENDING_KEYBOARD));
    }

    @Test
    public void testTransitionFromHiddenToExtendingByAvailableData() {
        // Prepare a tab and register a new tab, so there is a reason to display the bar.
        addBrowserTab(mMediator, 1111, null);
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());
        mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(any(), any())).thenReturn(true);

        // Showing the keyboard should now trigger a transition into EXTENDING state.
        mController.show(true);

        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(EXTENDING_KEYBOARD));
    }

    @Test
    public void testTransitionFromHiddenToFloatingBarByAvailableData() {
        // Prepare a tab and register a new tab, so there is a reason to display the bar.
        addBrowserTab(mMediator, 1111, null);
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());
        mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);

        // Showing the keyboard should now trigger a transition into EXTENDING state.
        mController.show(true);

        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_BAR));
    }

    @Test
    public void testTransitionFromFloatingBarToExtendingByKeyboard() {
        // Prepare a tab and register a new tab, so there is a reason to display the bar.
        addBrowserTab(mMediator, 1111, null);
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());
        mModel.set(SHOW_WHEN_VISIBLE, true);
        mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_BAR);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);
        when(mMockKeyboardAccessory.isShown()).thenReturn(true);

        // Simulate opening a keyboard:
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(any(), any())).thenReturn(true);
        mMediator.onLayoutChange(mMockContentView, 0, 0, 320, 90, 0, 0, 320, 180);

        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(EXTENDING_KEYBOARD));
    }

    @Test
    public void testTransitionFromFloatingBarToFloatingSheetByActivatingTab() {
        // Prepare a tab and register a new tab, so there is a reason to display the bar.
        addBrowserTab(mMediator, 1111, null);
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());
        mModel.set(SHOW_WHEN_VISIBLE, true);
        mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_BAR);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);
        when(mMockKeyboardAccessory.isShown()).thenReturn(true);

        // Simulate selecting a bottom sheet:
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true);
        mMediator.onChangeAccessorySheet(0);

        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_SHEET));
    }

    @Test
    public void testTransitionFromFloatingSheetToFloatingBarByClosingSheet() {
        // Prepare a tab and register a new tab, so there is a reason to display the bar.
        addBrowserTab(mMediator, 1111, null);
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());
        mModel.set(SHOW_WHEN_VISIBLE, true);
        mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_SHEET);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);
        when(mMockKeyboardAccessory.isShown()).thenReturn(true);
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true);

        // Simulate closing the bottom sheet:
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(false);
        mMediator.onCloseAccessorySheet();

        // This will cause a temporary floating sheet state which allows a nicer animation:
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_BAR));
    }

    @Test
    public void testTransitionFromExtendingToReplacingKeyboardByActivatingSheet() {
        // Prepare a tab and register a new tab, so there is a reason to display the bar.
        addBrowserTab(mMediator, 1111, null);
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());
        mModel.set(SHOW_WHEN_VISIBLE, true);
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(any(), any())).thenReturn(true);
        mModel.set(KEYBOARD_EXTENSION_STATE, EXTENDING_KEYBOARD);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);
        when(mMockKeyboardAccessory.isShown()).thenReturn(true);

        // Simulate selecting a bottom sheet:
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true);
        mMediator.onChangeAccessorySheet(0);

        // Now the filling component waits for the keyboard to disappear before changing the stat:
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(WAITING_TO_REPLACE));
        // Layout changes but the keyboard is still there, so nothing happens:
        mMediator.onLayoutChange(mMockContentView, 0, 0, 320, 90, 0, 0, 320, 90);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(WAITING_TO_REPLACE));

        // The keyboard finally hides completely and the state changes to REPLACING.
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(any(), any())).thenReturn(false);
        mMediator.onLayoutChange(mMockContentView, 0, 0, 320, 90, 0, 0, 320, 180);
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(REPLACING_KEYBOARD));
    }

    @Test
    public void testTransitionFromReplacingKeyboardToExtendingByClosingSheet() {
        // Prepare a tab and register a new tab, so there is a reason to display the bar.
        addBrowserTab(mMediator, 1111, null);
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());
        mModel.set(SHOW_WHEN_VISIBLE, true);
        mModel.set(KEYBOARD_EXTENSION_STATE, REPLACING_KEYBOARD);
        reset(mMockKeyboardAccessory, mMockAccessorySheet);
        when(mMockKeyboardAccessory.empty()).thenReturn(false);
        when(mMockKeyboardAccessory.isShown()).thenReturn(true);
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true);

        // Simulate closing the bottom sheet:
        when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(false);
        mMediator.onCloseAccessorySheet();

        // This will cause a temporary floating sheet state which allows a nicer animation:
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_SHEET));
        // This must trigger the keyboard to open, so the transition into EXTENDING can proceed.
        verify(mMockSoftKeyboardDelegate).showSoftKeyboard(any());

        // Simulate the keyboard opening:
        when(mMockSoftKeyboardDelegate.isSoftKeyboardShowing(any(), any())).thenReturn(true);
        mMediator.onLayoutChange(mMockContentView, 0, 0, 320, 90, 0, 0, 320, 180);

        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(EXTENDING_KEYBOARD));
    }

    @Test
    public void testCallsHelperToConfirmDeletion() {
        Runnable testConfirmRunnable = () -> {};
        Runnable testDeclineRunnable = () -> {};
        mMediator.confirmOperation(
                "Suggestion", "Delete it?", testConfirmRunnable, testDeclineRunnable);
        verify(mMockConfirmationHelper)
                .showConfirmation(
                        "Suggestion",
                        "Delete it?",
                        R.string.ok,
                        testConfirmRunnable,
                        testDeclineRunnable);
    }

    @Test
    public void testScrollsPageUpAfterBarIsFullyShown() {
        mMediator.onBarFadeInAnimationEnd();
        verify(mLastMockWebContents).scrollFocusedEditableNodeIntoView();
    }

    @Test
    public void testShowAccessorySheetTab() {
        // Prepare a tab and register a new tab, so there is a reason to display the bar.
        addBrowserTab(mMediator, 1111, null);
        mController.registerSheetDataProvider(
                mLastMockWebContents, AccessoryTabType.PASSWORDS, new PropertyProvider<>());
        assertThat(mModel.get(SHOW_WHEN_VISIBLE), is(false));
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(HIDDEN));

        mController.showAccessorySheetTab(AccessoryTabType.PASSWORDS);

        // Verify that the states are updated correctly and the active tab is set.
        assertThat(mModel.get(SHOW_WHEN_VISIBLE), is(true));
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(REPLACING_KEYBOARD));
        verify(mMockKeyboardAccessory, times(1)).setActiveTab(AccessoryTabType.PASSWORDS);

        // Simulate the callback once active tab is set.
        mMediator.onChangeAccessorySheet(0);

        // Assert tha the keyboard extension state continues to be REPLACING_KEYBOARD as we're
        // showing the sheet.
        assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(REPLACING_KEYBOARD));
    }

    /**
     * Creates a tab and calls the observer events as if it was just created and switched to.
     *
     * @param mediator The {@link ManualFillingMediator} whose observers should be triggered.
     * @param id The id of the new browser tab.
     * @param lastTab A previous mocked {@link Tab} to be hidden. Needs |getId()|. May be null.
     * @return Returns a mock of the newly added {@link Tab}. Provides |getId()|.
     */
    private Tab addBrowserTab(ManualFillingMediator mediator, int id, @Nullable Tab lastTab) {
        int lastId = INVALID_TAB_ID;
        if (lastTab != null) {
            lastId = lastTab.getId();
            mediator.getTabObserverForTesting().onHidden(lastTab, TabHidingType.CHANGED_TABS);
            mCache.getStateFor(mLastMockWebContents).getWebContentsObserverForTesting().wasHidden();
        }
        Tab tab = mock(Tab.class);
        when(tab.getId()).thenReturn(id);
        when(tab.getUserDataHost()).thenReturn(mUserDataHost);
        mLastMockWebContents = mock(WebContents.class);
        when(tab.getWebContents()).thenReturn(mLastMockWebContents);
        mCache.getStateFor(tab).getWebContentsObserverForTesting().wasShown();
        when(tab.getContentView()).thenReturn(mMockContentView);
        when(mMockTabModelSelector.getCurrentTab()).thenReturn(tab);
        mActivityTabProvider.set(tab);
        mediator.getTabModelObserverForTesting()
                .didAddTab(tab, FROM_BROWSER_ACTIONS, TabCreationState.LIVE_IN_FOREGROUND, false);
        mediator.getTabObserverForTesting().onShown(tab, FROM_NEW);
        mediator.getTabModelObserverForTesting().didSelectTab(tab, FROM_NEW, lastId);
        mInsetSupplier.setVirtualKeyboardMode(VirtualKeyboardMode.RESIZES_CONTENT);
        simulateLayoutSizeChange(
                2.f, 300, 128, /* keyboardShown= */ true, VirtualKeyboardMode.RESIZES_VISUAL);
        return tab;
    }

    /**
     * Simulates switching to a different tab by calling observer events on the given |mediator|.
     *
     * @param mediator The mediator providing the observer instances.
     * @param from The mocked {@link Tab} to be switched from. Needs |getId()|. May be null.
     * @param to The mocked {@link Tab} to be switched to. Needs |getId()|.
     */
    private void switchBrowserTab(ManualFillingMediator mediator, @Nullable Tab from, Tab to) {
        int lastId = INVALID_TAB_ID;
        if (from != null) {
            lastId = from.getId();
            mediator.getTabObserverForTesting().onHidden(from, TabHidingType.CHANGED_TABS);
            mCache.getStateFor(mLastMockWebContents).getWebContentsObserverForTesting().wasHidden();
        }
        mLastMockWebContents = to.getWebContents();
        mCache.getStateFor(to).getWebContentsObserverForTesting().wasShown();
        when(mMockTabModelSelector.getCurrentTab()).thenReturn(to);
        mediator.getTabModelObserverForTesting().didSelectTab(to, FROM_USER, lastId);
        mediator.getTabObserverForTesting().onShown(to, FROM_USER);
    }

    /**
     * Simulates destroying the given tab by calling observer events on the given |mediator|.
     *
     * @param mediator The mediator providing the observer instances.
     * @param tabToBeClosed The mocked {@link Tab} to be closed. Needs |getId()|.
     */
    private void closeBrowserTab(ManualFillingMediator mediator, Tab tabToBeClosed) {
        mediator.getTabModelObserverForTesting().willCloseTab(tabToBeClosed, true);
        mediator.getTabObserverForTesting().onHidden(tabToBeClosed, TabHidingType.CHANGED_TABS);
        mCache.getStateFor(mLastMockWebContents).getWebContentsObserverForTesting().wasHidden();
        mLastMockWebContents = null;
        mediator.getTabModelObserverForTesting().tabClosureCommitted(tabToBeClosed);
        mediator.getTabObserverForTesting().onDestroyed(tabToBeClosed);
    }

    /**
     * Prefer to use simulateLayoutSizeChange which more faithfully sets the WebContents and layout
     * sizes in the presence of a keyboard.
     */
    private void setContentAreaDimensions(float density, int widthDp, int heightDp) {
        setContentAreaDimensions(density, widthDp, heightDp, Surface.ROTATION_0);
    }

    private void setContentAreaDimensions(float density, int widthDp, int heightDp, int rotation) {
        DisplayAndroid mockDisplay = mock(DisplayAndroid.class);
        when(mockDisplay.getDipScale()).thenReturn(density);
        when(mockDisplay.getRotation()).thenReturn(rotation);
        when(mMockWindow.getDisplay()).thenReturn(mockDisplay);
        when(mLastMockWebContents.getHeight()).thenReturn(heightDp);
        when(mLastMockWebContents.getWidth()).thenReturn(widthDp);
        // Return the correct keyboard_accessory_height for the current density:
        when(mMockResources.getDimensionPixelSize(anyInt())).thenReturn((int) (density * 48));
    }

    /**
     * This function initializes mocks and then calls the given mediator events in the order of a
     * layout resize event (e.g. when extending/shrinking a multi-window window). It sets the
     * correct {@link WebContents} size according to the current VirtualKeyboardMode and calls
     * |onLayoutChange| with the new bounds.
     *
     * @param density The logical screen density (e.g. 1.f).
     * @param width The new mediator layout width in dp.
     * @param height The new mediator layout height in dp.
     * @param keyboardShown Whether the keyboard is considered shown - if true, the WebContents will
     *     be adjusted by the sKeyboardHeightDp depending on the vkMode.
     * @param vkMode The current virtual keyboard mode, affecting how WebContents reacts to the View
     *     size.
     */
    private void simulateLayoutSizeChange(
            float density,
            int width,
            int height,
            boolean keyboardShown,
            @VirtualKeyboardMode.EnumType int vkMode) {
        mInsetSupplier.setVirtualKeyboardMode(vkMode);
        int oldHeight = mLastMockWebContents.getHeight();
        int oldWidth = mLastMockWebContents.getWidth();

        int webContentsHeight = height;
        // In VISUAL/OVERLAYS, the keyboard shouldn't resize the WebContents so it must be
        // outsetted from the layout height by the keyboard. Otherwise, we must add to the
        // View's existing keyboard inset by insetting the accessory height as well. See
        // ApplicationViewportInsetSupplier for details on how this works.
        if (vkMode == VirtualKeyboardMode.RESIZES_VISUAL
                || vkMode == VirtualKeyboardMode.OVERLAYS_CONTENT) {
            webContentsHeight += keyboardShown ? sKeyboardHeightDp : 0;
        } else {
            int manualFillingInset =
                    Math.round(mController.getBottomInsetSupplier().get() / density);
            webContentsHeight -= manualFillingInset;
        }
        setContentAreaDimensions(2.f, width, webContentsHeight);

        int newHeight = (int) (density * height);
        int newWidth = (int) (density * width);
        mMediator.onLayoutChange(
                mMockContentView, 0, 0, newWidth, newHeight, 0, 0, oldWidth, oldHeight);
    }

    /**
     * @return A {@link ManualFillingState} that is never null.
     */
    private ManualFillingState getStateForBrowserTab() {
        assert mLastMockWebContents != null : "In testing, WebContents should never be null!";
        return mCache.getStateFor(mLastMockWebContents);
    }
}