chromium/chrome/android/java/src/org/chromium/chrome/browser/hub/HubLayoutUnitTest.java

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.hub;

import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNotNull;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static org.chromium.ui.test.util.MockitoHelper.doCallback;

import android.animation.AnimatorSet;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.RectF;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.FrameLayout.LayoutParams;

import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.filters.SmallTest;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.Callback;
import org.chromium.base.supplier.LazyOneshotSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.supplier.SyncOneshotSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.JniMocker;
import org.chromium.base.test.util.UserActionTester;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.compositor.layouts.Layout.ViewportMode;
import org.chromium.chrome.browser.compositor.layouts.LayoutRenderHost;
import org.chromium.chrome.browser.compositor.layouts.LayoutUpdateHost;
import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab;
import org.chromium.chrome.browser.compositor.scene_layer.SolidColorSceneLayer;
import org.chromium.chrome.browser.compositor.scene_layer.SolidColorSceneLayerJni;
import org.chromium.chrome.browser.compositor.scene_layer.StaticTabSceneLayer;
import org.chromium.chrome.browser.compositor.scene_layer.StaticTabSceneLayerJni;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.layouts.scene_layer.SceneLayer;
import org.chromium.chrome.browser.layouts.scene_layer.SceneLayerJni;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.ui.desktop_windowing.DesktopWindowStateProvider;
import org.chromium.ui.base.TestActivity;
import org.chromium.ui.resources.ResourceManager;

import java.util.Collections;
import java.util.function.DoubleConsumer;

/**
 * Unit tests for {@link HubLayout}.
 *
 * <p>TODO(crbug.com/40283200): Once integrated with LayoutManager we should also add integration
 * tests.
 */
@RunWith(BaseRobolectricTestRunner.class)
public class HubLayoutUnitTest {
    private static final int DEFAULT_COLOR = 0xFFABCDEF;
    private static final int INCOGNITO_COLOR = 0xFF001122;
    private static final long FAKE_NATIVE_ADDRESS_1 = 498723734L;
    private static final long FAKE_NATIVE_ADDRESS_2 = 123210L;
    private static final float FLOAT_ERROR = 0.001f;
    private static final int TAB_ID = 5;
    private static final int NEW_TAB_ID = 6;
    private static final int NEW_TAB_INDEX = 0;
    // This animation doesn't depend on time from the LayoutManager.
    private static final long FAKE_TIME = 0L;

    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
    @Rule public JniMocker mJniMocker = new JniMocker();

    @Rule
    public ActivityScenarioRule<TestActivity> mActivityScenarioRule =
            new ActivityScenarioRule<>(TestActivity.class);

    @Mock private LayoutUpdateHost mUpdateHost;
    @Mock private LayoutRenderHost mRenderHost;
    @Mock private LayoutStateProvider mLayoutStateProvider;
    @Mock private BrowserControlsStateProvider mBrowserControlsStateProvider;
    @Mock private ResourceManager mResourceManager;
    @Mock private SceneLayer.Natives mSceneLayerJni;
    @Mock private StaticTabSceneLayer.Natives mStaticTabSceneLayerJni;
    @Mock private SolidColorSceneLayer.Natives mSolidColorSceneLayerJni;
    @Mock private HubManager mHubManager;
    @Mock private HubController mHubController;
    @Mock private PaneManager mPaneManager;
    @Mock private HubLayoutScrimController mScrimController;
    @Mock private Pane mTabSwitcherPane;
    @Mock private Pane mIncognitoTabSwitcherPane;
    @Mock private Pane mTabGroupPane;
    @Mock private HubLayoutAnimator mHubLayoutAnimatorMock;
    @Mock private HubLayoutAnimatorProvider mHubLayoutAnimatorProviderMock;
    @Mock private Bitmap mBitmap;
    @Mock private Callback<Bitmap> mThumbnailCallback;
    @Mock private TabContentManager mTabContentManager;
    @Mock private TabModelSelector mTabModelSelector;
    @Mock private Tab mTab;
    @Mock private DoubleConsumer mOnAlphaChange;
    @Mock private DesktopWindowStateProvider mDesktopWindowStateProvider;

    private UserActionTester mActionTester;

    private Activity mActivity;
    private FrameLayout mFrameLayout;

    private HubLayout mHubLayout;
    private HubContainerView mHubContainerView;

    private SyncOneshotSupplierImpl<HubLayoutAnimator> mHubLayoutAnimatorSupplier;
    private Supplier<TabModelSelector> mTabModelSelectorSupplier;
    private ObservableSupplierImpl<Pane> mPaneSupplier = new ObservableSupplierImpl<>();
    private HubShowPaneHelper mHubShowPaneHelper;

    @Before
    public void setUp() {
        mJniMocker.mock(SceneLayerJni.TEST_HOOKS, mSceneLayerJni);
        mJniMocker.mock(StaticTabSceneLayerJni.TEST_HOOKS, mStaticTabSceneLayerJni);
        mJniMocker.mock(SolidColorSceneLayerJni.TEST_HOOKS, mSolidColorSceneLayerJni);

        mActionTester = new UserActionTester();
        ShadowLooper.runUiThreadTasks();

        when(mTabSwitcherPane.getPaneId()).thenReturn(PaneId.TAB_SWITCHER);
        when(mTabSwitcherPane.getColorScheme()).thenReturn(HubColorScheme.DEFAULT);
        when(mTabSwitcherPane.createShowHubLayoutAnimatorProvider(any()))
                .thenReturn(mHubLayoutAnimatorProviderMock);
        when(mTabSwitcherPane.createHideHubLayoutAnimatorProvider(any()))
                .thenReturn(mHubLayoutAnimatorProviderMock);
        when(mTabGroupPane.getPaneId()).thenReturn(PaneId.TAB_GROUPS);
        when(mTabGroupPane.getColorScheme()).thenReturn(HubColorScheme.DEFAULT);
        when(mTabGroupPane.createShowHubLayoutAnimatorProvider(any()))
                .thenReturn(mHubLayoutAnimatorProviderMock);
        when(mTabSwitcherPane.createHideHubLayoutAnimatorProvider(any()))
                .thenReturn(mHubLayoutAnimatorProviderMock);
        when(mIncognitoTabSwitcherPane.getPaneId()).thenReturn(PaneId.INCOGNITO_TAB_SWITCHER);
        when(mIncognitoTabSwitcherPane.getColorScheme()).thenReturn(HubColorScheme.INCOGNITO);
        when(mIncognitoTabSwitcherPane.createShowHubLayoutAnimatorProvider(any()))
                .thenReturn(mHubLayoutAnimatorProviderMock);
        when(mIncognitoTabSwitcherPane.createHideHubLayoutAnimatorProvider(any()))
                .thenReturn(mHubLayoutAnimatorProviderMock);

        when(mSceneLayerJni.init(any()))
                .thenReturn(FAKE_NATIVE_ADDRESS_1)
                .thenReturn(FAKE_NATIVE_ADDRESS_2);
        // Fake proper cleanup of the native ptr.
        doCallback(
                        /* index= */ 1,
                        (SceneLayer sceneLayer) -> {
                            sceneLayer.setNativePtr(0L);
                        })
                .when(mSceneLayerJni)
                .destroy(anyLong(), any());
        // Ensure each SceneLayer has a native ptr.
        doAnswer(
                        invocation -> {
                            ((SceneLayer) invocation.getArguments()[0])
                                    .setNativePtr(FAKE_NATIVE_ADDRESS_1);
                            return FAKE_NATIVE_ADDRESS_1;
                        })
                .when(mStaticTabSceneLayerJni)
                .init(any());
        doAnswer(
                        invocation -> {
                            ((SceneLayer) invocation.getArguments()[0])
                                    .setNativePtr(FAKE_NATIVE_ADDRESS_2);
                            return FAKE_NATIVE_ADDRESS_2;
                        })
                .when(mSolidColorSceneLayerJni)
                .init(any());

        when(mPaneManager.getFocusedPaneSupplier()).thenReturn(mPaneSupplier);
        doAnswer(
                        invocation -> {
                            int paneId = ((Integer) invocation.getArguments()[0]).intValue();
                            switch (paneId) {
                                case PaneId.TAB_SWITCHER:
                                    mPaneSupplier.set(mTabSwitcherPane);
                                    break;
                                case PaneId.INCOGNITO_TAB_SWITCHER:
                                    mPaneSupplier.set(mIncognitoTabSwitcherPane);
                                    break;
                                case PaneId.TAB_GROUPS:
                                    mPaneSupplier.set(mTabGroupPane);
                                    break;
                                default:
                                    fail("Invalid pane id" + paneId);
                            }
                            return true;
                        })
                .when(mPaneManager)
                .focusPane(anyInt());
        when(mHubManager.getPaneManager()).thenReturn(mPaneManager);
        when(mHubManager.getHubController()).thenReturn(mHubController);
        mHubShowPaneHelper = new HubShowPaneHelper();
        when(mHubManager.getHubShowPaneHelper()).thenReturn(mHubShowPaneHelper);
        doAnswer(
                        invocation -> {
                            Pane pane = (Pane) invocation.getArguments()[0];
                            if (pane == null) return DEFAULT_COLOR;

                            switch (pane.getColorScheme()) {
                                case HubColorScheme.DEFAULT:
                                    return DEFAULT_COLOR;
                                case HubColorScheme.INCOGNITO:
                                    return INCOGNITO_COLOR;
                                default:
                                    fail("Unexpected colorscheme " + pane.getColorScheme());
                                    return Color.TRANSPARENT;
                            }
                        })
                .when(mHubController)
                .getBackgroundColor(any());

        mActivityScenarioRule.getScenario().onActivity(this::onActivityCreated);

        doAnswer(
                        invocation -> {
                            var args = invocation.getArguments();
                            return new LayoutTab(
                                    (Integer) args[0],
                                    (Boolean) args[1],
                                    ((Float) args[2]).intValue(),
                                    ((Float) args[3]).intValue());
                        })
                .when(mUpdateHost)
                .createLayoutTab(anyInt(), anyBoolean(), anyFloat(), anyFloat());
        when(mTab.getId()).thenReturn(TAB_ID);
        when(mTab.isNativePage()).thenReturn(false);
        when(mTabModelSelector.getCurrentTab()).thenReturn(mTab);

        mHubLayoutAnimatorSupplier = new SyncOneshotSupplierImpl<HubLayoutAnimator>();
        when(mHubLayoutAnimatorProviderMock.getAnimatorSupplier())
                .thenReturn(mHubLayoutAnimatorSupplier);
    }

    private void onActivityCreated(Activity activity) {
        mActivity = activity;
        mFrameLayout = new FrameLayout(mActivity);
        mHubContainerView = new HubContainerView(mActivity);
        View hubLayout = LayoutInflater.from(activity).inflate(R.layout.hub_layout, null);
        mHubContainerView.setLayoutParams(
                new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
        mHubContainerView.addView(hubLayout);
        mActivity.setContentView(mFrameLayout);

        View paneHostView = hubLayout.findViewById(R.id.hub_pane_host);
        when(mHubController.getContainerView()).thenReturn(mHubContainerView);
        when(mHubController.getPaneHostView()).thenReturn(paneHostView);

        LazyOneshotSupplier<HubManager> hubManagerSupplier =
                LazyOneshotSupplier.fromValue(mHubManager);
        LazyOneshotSupplier<ViewGroup> rootViewSupplier =
                LazyOneshotSupplier.fromValue(mFrameLayout);
        HubLayoutDependencyHolder dependencyHolder =
                new HubLayoutDependencyHolder(
                        hubManagerSupplier, rootViewSupplier, mScrimController, mOnAlphaChange);

        mTabModelSelectorSupplier = () -> mTabModelSelector;
        mHubLayout =
                spy(
                        new HubLayout(
                                mActivity,
                                mUpdateHost,
                                mRenderHost,
                                mLayoutStateProvider,
                                dependencyHolder,
                                mTabModelSelectorSupplier,
                                mDesktopWindowStateProvider));
        mHubLayout.setTabModelSelector(mTabModelSelector);
        mHubLayout.setTabContentManager(mTabContentManager);
        mHubLayout.onFinishNativeInitialization();
    }

    @After
    public void tearDown() {
        mHubLayout.destroy();
        mActionTester.tearDown();
    }

    @Test
    @SmallTest
    public void testFixedReturnValues() {
        // These are not expected to change. This is here to get unit test coverage.
        assertEquals(ViewportMode.ALWAYS_FULLSCREEN, mHubLayout.getViewportMode());
        assertTrue(mHubLayout.handlesTabClosing());
        assertTrue(mHubLayout.handlesTabCreating());
        assertNull(mHubLayout.getEventFilter());
        assertEquals(LayoutType.TAB_SWITCHER, mHubLayout.getLayoutType());

        // TODO(crbug.com/40283200): These may be dynamic after further development.
        assertFalse(mHubLayout.onBackPressed());
        assertTrue(mHubLayout.canHostBeFocusable());
    }

    @Test
    @SmallTest
    public void testUpdateSceneLayerAndLayoutTabsDuringShow() {
        setupHubLayoutAnimatorAndProvider(HubLayoutAnimationType.FADE_IN);
        animateCheckingSceneLayerAndLayoutTabs(
                () -> startShowing(LayoutType.BROWSING, true), TAB_ID);
        verify(mTabContentManager)
                .updateVisibleIds(eq(Collections.emptyList()), eq(Tab.INVALID_TAB_ID));
    }

    @Test
    @SmallTest
    public void testUpdateSceneLayerAndLayoutTabsDuringHide() {
        setupHubLayoutAnimatorAndProvider(HubLayoutAnimationType.FADE_OUT);
        animateCheckingSceneLayerAndLayoutTabs(
                () -> startHiding(LayoutType.BROWSING, NEW_TAB_ID), NEW_TAB_ID);
        verify(mTabContentManager, never())
                .updateVisibleIds(eq(Collections.emptyList()), eq(Tab.INVALID_TAB_ID));
    }

    @Test
    @SmallTest
    @Config(qualifiers = "sw600dp")
    public void testShowTablet() {
        show(LayoutType.BROWSING, true, HubLayoutAnimationType.TRANSLATE_UP);
        verify(mTabContentManager).cacheTabThumbnailWithCallback(any(), anyBoolean(), any());
    }

    @Test
    @SmallTest
    public void testShowWithNoSelectedPane() {
        setupHubLayoutAnimatorAndProvider(HubLayoutAnimationType.SHRINK_TAB);
        when(mTabModelSelector.isIncognitoSelected()).thenReturn(false);
        show(LayoutType.BROWSING, true, HubLayoutAnimationType.SHRINK_TAB);
        verify(mTabContentManager).cacheTabThumbnailWithCallback(any(), anyBoolean(), any());
        verify(mPaneManager).focusPane(PaneId.TAB_SWITCHER);

        verify(mSolidColorSceneLayerJni).setBackgroundColor(FAKE_NATIVE_ADDRESS_2, DEFAULT_COLOR);
    }

    @Test
    @SmallTest
    public void testShowWithSelectedPane() {
        setupHubLayoutAnimatorAndProvider(HubLayoutAnimationType.SHRINK_TAB);

        when(mTabModelSelector.isIncognitoSelected()).thenReturn(false);
        mHubShowPaneHelper.setPaneToShow(PaneId.TAB_GROUPS);
        show(LayoutType.BROWSING, true, HubLayoutAnimationType.SHRINK_TAB);
        verify(mTabContentManager).cacheTabThumbnailWithCallback(any(), anyBoolean(), any());
        verify(mPaneManager).focusPane(PaneId.TAB_GROUPS);

        verify(mSolidColorSceneLayerJni).setBackgroundColor(FAKE_NATIVE_ADDRESS_2, DEFAULT_COLOR);
    }

    @Test
    @SmallTest
    public void testShowWithIncognitoPane() {
        setupHubLayoutAnimatorAndProvider(HubLayoutAnimationType.SHRINK_TAB);
        when(mTabModelSelector.isIncognitoSelected()).thenReturn(true);
        show(LayoutType.BROWSING, true, HubLayoutAnimationType.SHRINK_TAB);
        verify(mTabContentManager).cacheTabThumbnailWithCallback(any(), anyBoolean(), any());
        verify(mPaneManager).focusPane(PaneId.INCOGNITO_TAB_SWITCHER);

        verify(mSolidColorSceneLayerJni).setBackgroundColor(FAKE_NATIVE_ADDRESS_2, INCOGNITO_COLOR);
    }

    @Test
    @SmallTest
    public void testShowFromBrowsingWithThumbnailCallback() {
        setupHubLayoutAnimatorAndProvider(HubLayoutAnimationType.SHRINK_TAB);
        when(mHubLayoutAnimatorProviderMock.getThumbnailCallback()).thenReturn(mThumbnailCallback);

        // Successfully capture a bitmap.
        doCallback(
                        /* index= */ 2,
                        (Callback<Bitmap> bitmapCallback) -> {
                            bitmapCallback.onResult(mBitmap);
                        })
                .when(mTabContentManager)
                .cacheTabThumbnailWithCallback(any(), eq(true), any());

        show(LayoutType.BROWSING, true, HubLayoutAnimationType.SHRINK_TAB);

        InOrder inOrder = inOrder(mTabContentManager, mHubController);
        inOrder.verify(mTabContentManager).cacheTabThumbnailWithCallback(any(), eq(true), any());
        inOrder.verify(mHubController).onHubLayoutShow();

        verify(mThumbnailCallback).bind(isNotNull());
    }

    @Test
    @SmallTest
    public void testShowFromBrowsingWithFallbackNativePageThumbnailCallback() {
        setupHubLayoutAnimatorAndProvider(HubLayoutAnimationType.SHRINK_TAB);
        when(mHubLayoutAnimatorProviderMock.getThumbnailCallback()).thenReturn(mThumbnailCallback);
        when(mTab.isNativePage()).thenReturn(true);

        // Fail to capture a bitmap.
        doCallback(
                        /* index= */ 2,
                        (Callback<Bitmap> bitmapCallback) -> {
                            bitmapCallback.onResult(null);
                        })
                .when(mTabContentManager)
                .cacheTabThumbnailWithCallback(any(), eq(true), any());

        // Succeed on the NativePage fallback thumbnail attempt.
        doCallback(
                        /* index= */ 1,
                        (Callback<Bitmap> bitmapCallback) -> {
                            bitmapCallback.onResult(mBitmap);
                        })
                .when(mTabContentManager)
                .getEtc1TabThumbnailWithCallback(eq(TAB_ID), any());

        show(LayoutType.BROWSING, true, HubLayoutAnimationType.SHRINK_TAB);

        InOrder inOrder = inOrder(mTabContentManager, mHubController);
        inOrder.verify(mTabContentManager).cacheTabThumbnailWithCallback(any(), eq(true), any());
        inOrder.verify(mHubController).onHubLayoutShow();

        verify(mThumbnailCallback).bind(isNotNull());
    }

    @Test
    @SmallTest
    public void testShowFromBrowsingWithoutFallbackThumbnailCallback() {
        setupHubLayoutAnimatorAndProvider(HubLayoutAnimationType.SHRINK_TAB);
        when(mHubLayoutAnimatorProviderMock.getThumbnailCallback()).thenReturn(mThumbnailCallback);

        // Fail to capture the bitmap and since this is not a native page there is no fallback.
        doCallback(
                        /* index= */ 2,
                        (Callback<Bitmap> bitmapCallback) -> {
                            bitmapCallback.onResult(null);
                        })
                .when(mTabContentManager)
                .cacheTabThumbnailWithCallback(any(), eq(true), any());

        show(LayoutType.BROWSING, true, HubLayoutAnimationType.SHRINK_TAB);

        InOrder inOrder = inOrder(mTabContentManager, mHubController);
        inOrder.verify(mTabContentManager).cacheTabThumbnailWithCallback(any(), eq(true), any());
        inOrder.verify(mHubController).onHubLayoutShow();

        verify(mThumbnailCallback).bind(isNull());
        verify(mTabContentManager, never()).getEtc1TabThumbnailWithCallback(anyInt(), any());
    }

    @Test
    @SmallTest
    @Config(qualifiers = "sw600dp")
    public void testHideTablet() {
        hide(
                LayoutType.BROWSING,
                TAB_ID,
                /* skipStartHiding= */ false,
                HubLayoutAnimationType.TRANSLATE_DOWN);
        verify(mTabContentManager, never()).getEtc1TabThumbnailWithCallback(anyInt(), any());
    }

    @Test
    @SmallTest
    public void testHideWithNoPane() {
        hide(
                LayoutType.BROWSING,
                Tab.INVALID_TAB_ID,
                /* skipStartHiding= */ false,
                HubLayoutAnimationType.FADE_OUT);
        verify(mTabContentManager, never()).getEtc1TabThumbnailWithCallback(anyInt(), any());
    }

    @Test
    @SmallTest
    public void testHideViaNewTab() {
        forceLayout();
        mHubLayout.onTabCreated(FAKE_TIME, NEW_TAB_ID, NEW_TAB_INDEX, TAB_ID, false, false, 0, 0);
        hide(
                LayoutType.BROWSING,
                NEW_TAB_ID,
                /* skipStartHiding= */ true,
                HubLayoutAnimationType.EXPAND_NEW_TAB);
        verify(mTabContentManager, never()).getEtc1TabThumbnailWithCallback(anyInt(), any());
    }

    @Test
    @SmallTest
    @Config(qualifiers = "sw600dp")
    public void testHideViaNewTabTablet() {
        mHubLayout.onTabCreated(FAKE_TIME, NEW_TAB_ID, NEW_TAB_INDEX, TAB_ID, false, false, 0, 0);
        hide(
                LayoutType.BROWSING,
                NEW_TAB_ID,
                /* skipStartHiding= */ true,
                HubLayoutAnimationType.TRANSLATE_DOWN);
        verify(mTabContentManager, never()).getEtc1TabThumbnailWithCallback(anyInt(), any());
    }

    @Test
    @SmallTest
    public void testHideToBrowsingThumbnailCallback() {
        setupHubLayoutAnimatorAndProvider(HubLayoutAnimationType.EXPAND_TAB);
        mPaneSupplier.set(mTabSwitcherPane);
        when(mHubLayoutAnimatorProviderMock.getThumbnailCallback()).thenReturn(mThumbnailCallback);
        when(mTab.isNativePage()).thenReturn(true);

        // Succeed on the thumbnail attempt
        doCallback(
                        /* index= */ 1,
                        (Callback<Bitmap> bitmapCallback) -> {
                            bitmapCallback.onResult(mBitmap);
                        })
                .when(mTabContentManager)
                .getEtc1TabThumbnailWithCallback(eq(TAB_ID), any());

        hide(
                LayoutType.BROWSING,
                TAB_ID,
                /* skipStartHiding= */ false,
                HubLayoutAnimationType.EXPAND_TAB);

        verify(mThumbnailCallback).onResult(isNotNull());
    }

    @Test
    @SmallTest
    public void testHideToBrowsingThumbnailCallbackNoTabIdInStartHiding() {
        when(mTabModelSelector.getCurrentTabId()).thenReturn(TAB_ID);

        setupHubLayoutAnimatorAndProvider(HubLayoutAnimationType.EXPAND_TAB);
        mPaneSupplier.set(mTabSwitcherPane);
        when(mHubLayoutAnimatorProviderMock.getThumbnailCallback()).thenReturn(mThumbnailCallback);
        doReturn(mHubLayoutAnimatorProviderMock).when(mHubLayout).createHideAnimatorProvider(any());
        when(mTab.isNativePage()).thenReturn(true);

        // Succeed on the thumbnail attempt
        doCallback(
                        /* index= */ 1,
                        (Callback<Bitmap> bitmapCallback) -> {
                            bitmapCallback.onResult(mBitmap);
                        })
                .when(mTabContentManager)
                .getEtc1TabThumbnailWithCallback(eq(TAB_ID), any());

        hide(
                LayoutType.BROWSING,
                Tab.INVALID_TAB_ID,
                /* skipStartHiding= */ false,
                HubLayoutAnimationType.EXPAND_TAB);

        verify(mThumbnailCallback).onResult(isNotNull());
    }

    @Test
    @SmallTest
    public void testShowInterruptedByHide() {
        mPaneSupplier.set(mTabSwitcherPane);
        assertFalse(mHubLayout.isRunningAnimations());
        assertFalse(mHubLayout.onUpdateAnimation(FAKE_TIME, false));

        setupHubLayoutAnimatorAndProvider(HubLayoutAnimationType.FADE_IN);
        startShowing(LayoutType.BROWSING, true);

        verify(mHubController, times(1)).onHubLayoutShow();
        assertEquals(1, mFrameLayout.getChildCount());

        assertEquals(HubLayoutAnimationType.FADE_IN, mHubLayout.getCurrentAnimationType());
        assertTrue(mHubLayout.isRunningAnimations());
        assertTrue(mHubLayout.onUpdateAnimation(FAKE_TIME, false));

        setupHubLayoutAnimatorAndProvider(HubLayoutAnimationType.FADE_OUT);
        startHiding(LayoutType.BROWSING, NEW_TAB_ID);
        verify(mHubLayout).doneShowing();
        verify(mTab, never()).hide(anyInt());
        verify(mScrimController).forceAnimationToFinish();

        assertEquals(HubLayoutAnimationType.FADE_OUT, mHubLayout.getCurrentAnimationType());
        assertTrue(mHubLayout.isRunningAnimations());
        assertTrue(mHubLayout.onUpdateAnimation(FAKE_TIME, false));

        ShadowLooper.runUiThreadTasks();

        assertFalse(mHubLayout.isRunningAnimations());
        assertFalse(mHubLayout.onUpdateAnimation(FAKE_TIME, false));

        verify(mHubController, times(1)).onHubLayoutDoneHiding();
        assertEquals(0, mFrameLayout.getChildCount());
        verify(mHubLayout).doneHiding();
        verify(mTab, never()).hide(anyInt());
    }

    private void show(
            @LayoutType int fromLayout,
            boolean animate,
            @HubLayoutAnimationType int expectedAnimationType) {
        assertFalse(mHubLayout.isRunningAnimations());
        assertFalse(mHubLayout.onUpdateAnimation(FAKE_TIME, false));
        assertFalse(mHubLayout.forceHideBrowserControlsAndroidView());

        startShowing(fromLayout, animate);

        verify(mHubController, times(1)).onHubLayoutShow();
        assertEquals(1, mFrameLayout.getChildCount());

        if (animate) {
            assertEquals(expectedAnimationType, mHubLayout.getCurrentAnimationType());
            assertTrue(mHubLayout.isRunningAnimations());
            assertTrue(mHubLayout.onUpdateAnimation(FAKE_TIME, false));
        } else {
            assertFalse(mHubLayout.isRunningAnimations());
        }

        ShadowLooper.runUiThreadTasks();

        assertFalse(mHubLayout.isRunningAnimations());
        assertFalse(mHubLayout.onUpdateAnimation(FAKE_TIME, false));
        verify(mHubLayout).doneShowing();
        assertTrue(mHubLayout.forceHideBrowserControlsAndroidView());
        assertEquals(1, mActionTester.getActionCount("MobileToolbarShowStackView"));
        verify(mTab).hide(eq(TabHidingType.TAB_SWITCHER_SHOWN));
        verify(mScrimController, never()).forceAnimationToFinish();
    }

    private void hide(
            @LayoutType int nextLayout,
            int nextTabId,
            boolean skipStartHiding,
            @HubLayoutAnimationType int expectedAnimationType) {
        if (skipStartHiding) {
            assertTrue(mHubLayout.isRunningAnimations());
            assertTrue(mHubLayout.onUpdateAnimation(FAKE_TIME, false));
        } else {
            assertFalse(mHubLayout.isRunningAnimations());
            assertFalse(mHubLayout.onUpdateAnimation(FAKE_TIME, false));
            startHiding(nextLayout, nextTabId);
            assertFalse(mHubLayout.forceHideBrowserControlsAndroidView());
        }

        assertEquals(expectedAnimationType, mHubLayout.getCurrentAnimationType());
        assertTrue(mHubLayout.isRunningAnimations());
        assertTrue(mHubLayout.onUpdateAnimation(FAKE_TIME, false));
        forceLayout();

        ShadowLooper.runUiThreadTasks();

        assertFalse(mHubLayout.isRunningAnimations());
        assertFalse(mHubLayout.onUpdateAnimation(FAKE_TIME, false));

        verify(mHubController, times(1)).onHubLayoutDoneHiding();
        assertEquals(0, mFrameLayout.getChildCount());
        verify(mHubLayout).doneHiding();
        assertFalse(mHubLayout.forceHideBrowserControlsAndroidView());
        assertEquals(1, mActionTester.getActionCount("MobileExitStackView"));

        verify(mScrimController, never()).forceAnimationToFinish();
    }

    private void startShowing(@LayoutType int fromLayout, boolean animate) {
        when(mLayoutStateProvider.getActiveLayoutType()).thenReturn(fromLayout);
        mHubLayout.contextChanged(mActivity);
        assertEquals(fromLayout, mHubLayout.getPreviousLayoutTypeSupplier().get().intValue());

        mHubLayout.show(FAKE_TIME, animate);
    }

    private void startHiding(@LayoutType int nextLayout, int nextTabId) {
        @LayoutType int layoutType = mHubLayout.getLayoutType();
        when(mLayoutStateProvider.getActiveLayoutType()).thenReturn(layoutType);
        when(mLayoutStateProvider.getNextLayoutType()).thenReturn(nextLayout);

        // This selection happens before anything else in selectTabAndHideHubLayout. Mock it before
        // the call.
        if (nextTabId != Tab.INVALID_TAB_ID) {
            when(mTabModelSelector.getCurrentTabId()).thenReturn(nextTabId);
        }
        mHubLayout.selectTabAndHideHubLayout(nextTabId);
    }

    private void animateCheckingSceneLayerAndLayoutTabs(
            Runnable startAnimationRunnable, int tabId) {
        assertThat(mHubLayout.getSceneLayer(), instanceOf(SolidColorSceneLayer.class));
        LayoutTab[] layoutTabs = mHubLayout.getLayoutTabsToRender();
        assertNull(layoutTabs);

        mHubLayout.updateLayout(FAKE_TIME, FAKE_TIME);
        verify(mUpdateHost, never()).requestUpdate();

        startAnimationRunnable.run();

        assertThat(mHubLayout.getSceneLayer(), instanceOf(StaticTabSceneLayer.class));
        layoutTabs = mHubLayout.getLayoutTabsToRender();
        assertEquals(1, layoutTabs.length);
        assertEquals(tabId, layoutTabs[0].getId());
        verify(mTabContentManager)
                .updateVisibleIds(eq(Collections.singletonList(tabId)), eq(Tab.INVALID_TAB_ID));

        assertEquals(0f, layoutTabs[0].get(LayoutTab.CONTENT_OFFSET), FLOAT_ERROR);

        float contentOffset = 100f;
        when(mBrowserControlsStateProvider.getContentOffset())
                .thenReturn(Math.round(contentOffset));
        mHubLayout.updateSceneLayer(
                new RectF(),
                new RectF(),
                mTabContentManager,
                mResourceManager,
                mBrowserControlsStateProvider);
        assertEquals(contentOffset, layoutTabs[0].get(LayoutTab.CONTENT_OFFSET), FLOAT_ERROR);

        // Change this so updateSnap() returns true.
        layoutTabs[0].set(LayoutTab.RENDER_X, 5);
        mHubLayout.updateLayout(FAKE_TIME, FAKE_TIME);
        verify(mUpdateHost).requestUpdate();

        mHubContainerView.runOnNextLayoutRunnables();
        ShadowLooper.runUiThreadTasks();

        assertThat(mHubLayout.getSceneLayer(), instanceOf(SolidColorSceneLayer.class));
        layoutTabs = mHubLayout.getLayoutTabsToRender();
        assertNull(layoutTabs);
    }

    private void setupHubLayoutAnimatorAndProvider(@HubLayoutAnimationType int animationType) {
        AnimatorSet animatorSet = new AnimatorSet();
        when(mHubLayoutAnimatorMock.getAnimationType()).thenReturn(animationType);
        when(mHubLayoutAnimatorMock.getAnimatorSet()).thenReturn(animatorSet);
        when(mHubLayoutAnimatorProviderMock.getPlannedAnimationType()).thenReturn(animationType);
        mHubLayoutAnimatorSupplier = new SyncOneshotSupplierImpl<>();
        mHubLayoutAnimatorSupplier.set(mHubLayoutAnimatorMock);
        when(mHubLayoutAnimatorProviderMock.getAnimatorSupplier())
                .thenReturn(mHubLayoutAnimatorSupplier);
    }

    private void forceLayout() {
        // Force any layout delayed animations to run.
        mHubContainerView.layout(0, 0, 100, 100);
        for (int i = 0; i < mHubContainerView.getChildCount(); i++) {
            mHubContainerView.getChildAt(i).layout(0, 0, 100, 100);
        }
    }
}