chromium/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/tab_strip/TabStripTransitionCoordinatorUnitTest.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.toolbar.top.tab_strip;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.FrameLayout;

import androidx.annotation.Nullable;
import androidx.test.ext.junit.rules.ActivityScenarioRule;

import org.junit.Assert;
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.Mockito;
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.OneshotSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.cc.input.BrowserControlsState;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.browser_controls.BrowserControlsVisibilityManager;
import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.tab.TabObscuringHandler;
import org.chromium.chrome.browser.tab.TabObscuringHandler.Target;
import org.chromium.chrome.browser.toolbar.ControlContainer;
import org.chromium.chrome.browser.toolbar.ToolbarFeatures;
import org.chromium.chrome.browser.toolbar.top.tab_strip.TabStripTransitionCoordinator.TabStripHeightObserver;
import org.chromium.chrome.browser.toolbar.top.tab_strip.TabStripTransitionCoordinator.TabStripTransitionDelegate;
import org.chromium.chrome.browser.ui.desktop_windowing.AppHeaderState;
import org.chromium.chrome.browser.ui.desktop_windowing.AppHeaderUtils.DesktopWindowModeState;
import org.chromium.chrome.browser.ui.desktop_windowing.DesktopWindowStateProvider;
import org.chromium.ui.base.TestActivity;
import org.chromium.ui.resources.Resource;
import org.chromium.ui.resources.dynamics.ViewResourceAdapter;

import java.util.concurrent.TimeUnit;

/** Unit test for {@link TabStripTransitionCoordinator}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(qualifiers = "w600dp-h800dp", shadows = ShadowLooper.class)
@DisableFeatures(ChromeFeatureList.TAB_STRIP_LAYOUT_OPTIMIZATION)
public class TabStripTransitionCoordinatorUnitTest {
    private static final int TEST_TAB_STRIP_HEIGHT = 40;
    private static final int TEST_TOOLBAR_HEIGHT = 56;
    private static final int NOTHING_OBSERVED = -1;
    private static final int NARROW_NORMAL_WINDOW_WIDTH = 411;
    private static final int NARROW_DESKTOP_WINDOW_WIDTH = 283;

    @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();

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

    @Mock private BrowserControlsVisibilityManager mBrowserControlsVisibilityManager;
    @Mock private BrowserStateBrowserControlsVisibilityDelegate mVisibilityDelegate;
    @Mock private ControlContainer mControlContainer;
    @Mock private ViewResourceAdapter mViewResourceAdapter;
    @Mock private DesktopWindowStateProvider mDesktopWindowStateProvider;
    @Captor private ArgumentCaptor<BrowserControlsStateProvider.Observer> mBrowserControlsObserver;
    @Captor private ArgumentCaptor<Callback<Resource>> mOnCaptureReadyCallback;

    private TestControlContainerView mSpyControlContainer;
    private TabStripTransitionCoordinator mCoordinator;
    private TestActivity mActivity;
    private TabObscuringHandler mTabObscuringHandler = new TabObscuringHandler();
    private TestObserver mObserver;
    private TestDelegate mDelegate;
    private OneshotSupplierImpl<TabStripTransitionDelegate> mDelegateSupplier;
    private int mReservedTopPadding;

    // Test variables
    private int mTopControlsContentOffset;
    private AppHeaderState mAppHeaderState;

    @Before
    public void setup() {
        mActivityScenario.getScenario().onActivity(activity -> mActivity = activity);
        mSpyControlContainer = TestControlContainerView.createSpy(mActivity);
        mActivity.setContentView(mSpyControlContainer);
        mReservedTopPadding =
                mActivity
                        .getResources()
                        .getDimensionPixelSize(R.dimen.tab_strip_reserved_top_padding);

        // Set the mocks for control container and its view resource adapter.
        doReturn(mSpyControlContainer).when(mControlContainer).getView();
        doReturn(mViewResourceAdapter).when(mControlContainer).getToolbarResourceAdapter();
        doNothing()
                .when(mViewResourceAdapter)
                .addOnResourceReadyCallback(mOnCaptureReadyCallback.capture());
        doAnswer(inv -> triggerCapture()).when(mViewResourceAdapter).triggerBitmapCapture();

        // Set up test browser controls manger.
        mTopControlsContentOffset = TEST_TAB_STRIP_HEIGHT + TEST_TOOLBAR_HEIGHT;
        doNothing()
                .when(mBrowserControlsVisibilityManager)
                .addObserver(mBrowserControlsObserver.capture());
        doReturn(View.VISIBLE)
                .when(mBrowserControlsVisibilityManager)
                .getAndroidControlsVisibility();
        doAnswer(invocationOnMock -> mTopControlsContentOffset)
                .when(mBrowserControlsVisibilityManager)
                .getContentOffset();
        doReturn(mVisibilityDelegate)
                .when(mBrowserControlsVisibilityManager)
                .getBrowserVisibilityDelegate();
        doReturn(BrowserControlsState.BOTH).when(mVisibilityDelegate).get();

        // Setup other mocks.
        doAnswer((arg) -> mAppHeaderState).when(mDesktopWindowStateProvider).getAppHeaderState();

        mDelegate = new TestDelegate();
        mDelegateSupplier = new OneshotSupplierImpl<>();
        mDelegateSupplier.set(mDelegate);

        setUpTabStripTransitionCoordinator();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
    }

    @Test
    public void initWithWideWindow() {
        Assert.assertEquals(
                "Tab strip height is wrong.",
                TEST_TAB_STRIP_HEIGHT,
                mCoordinator.getTabStripHeight());

        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);
        Assert.assertEquals("Tab strip height is wrong.", 0, mObserver.heightRequested);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void initWithNarrowWindow() {
        Assert.assertEquals(
                "Init will not change the tab strip height.",
                TEST_TAB_STRIP_HEIGHT,
                mCoordinator.getTabStripHeight());
        Assert.assertEquals(
                "Tab strip height requested changing to 0.", 0, mObserver.heightRequested);

        setDeviceWidthDp(600);
        Assert.assertEquals(
                "Changing the window to wide will request for full-size tab strip.",
                TEST_TAB_STRIP_HEIGHT,
                mObserver.heightRequested);
    }

    @Test
    public void hideTabStrip() {
        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);

        doReturn(TEST_TOOLBAR_HEIGHT)
                .when(mBrowserControlsVisibilityManager)
                .getTopControlsHeight();
        runOffsetTransitionForBrowserControlManager(
                /* beginOffset= */ TEST_TAB_STRIP_HEIGHT + TEST_TOOLBAR_HEIGHT,
                /* endOffset= */ TEST_TOOLBAR_HEIGHT);
        assertTabStripHeightForMargins(0);
        assertObservedHeight(0);
    }

    @Test
    public void hideTabStripWithOffsetOverride() {
        // Simulate top controls size change from browser.
        doReturn(true).when(mBrowserControlsVisibilityManager).offsetOverridden();
        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);
        assertTabStripHeightForMargins(0);
        assertObservedHeight(0);
    }

    @Test
    public void hideTabStripWithForceBrowserControlShown() {
        doReturn(BrowserControlsState.SHOWN).when(mVisibilityDelegate).get();
        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);
        assertTabStripHeightForMargins(0);
        assertObservedHeight(0);
    }

    @Test
    public void hideTabStripWithForceBrowserControlHidden() {
        doReturn(BrowserControlsState.HIDDEN).when(mVisibilityDelegate).get();
        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);
        assertTabStripHeightForMargins(0);
        assertObservedHeight(0);
    }

    @Test
    public void hideTabStripWhileTopControlsHidden() {
        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);

        // Assume the top control is hidden and content is at the top.
        doReturn(0).when(mBrowserControlsVisibilityManager).getContentOffset();
        getBrowserControlsObserver().onControlsOffsetChanged(0, 0, 0, 0, false, false);

        assertTabStripHeightForMargins(0);
        assertObservedHeight(0);
        assertObservedTransitionFinished(true);
    }

    @Test
    public void hideTabStripWhileUrlBarFocused() {
        mCoordinator.onUrlFocusChange(true);
        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);
        Assert.assertEquals(
                "Height request should be blocked by the url bar focus.",
                NOTHING_OBSERVED,
                mObserver.heightRequested);

        // Url focus animation finished to unblock the transition.
        mCoordinator.onUrlAnimationFinished(false);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Assert.assertEquals(
                "Height request should go through after the url bar focus.",
                0,
                mObserver.heightRequested);
    }

    @Test
    public void hideTabStripWhileTabObscured() {
        TabObscuringHandler.Token token = mTabObscuringHandler.obscure(Target.TAB_CONTENT);
        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);
        Assert.assertEquals(
                "Height request should be blocked after tab obscured.",
                NOTHING_OBSERVED,
                mObserver.heightRequested);

        // Url focus animation finished to unblock the transition
        mTabObscuringHandler.unobscure(token);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Assert.assertEquals(
                "Height request should go through after tab unobscured.",
                0,
                mObserver.heightRequested);
    }

    @Test
    public void hideTabStripWhileTabAndToolbarObscured() {
        mTabObscuringHandler.obscure(Target.ALL_TABS_AND_TOOLBAR);
        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);
        Assert.assertEquals(
                "Height request should go through when tab and toolbar are obscured.",
                0,
                mObserver.heightRequested);
    }

    @Test
    @EnableFeatures(ChromeFeatureList.TAB_STRIP_TRANSITION_IN_DESKTOP_WINDOW)
    public void hideTabStripDisabledInDesktopWindow() {
        mAppHeaderState = new AppHeaderState(new Rect(), new Rect(), /* isInDesktopWindow= */ true);
        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);
        Assert.assertEquals(
                "Height transition to hide strip is disabled in a desktop window.",
                NOTHING_OBSERVED,
                mObserver.heightRequested);
    }

    @Test
    public void hideTabStripBeforeLayout() {
        // Simulate the control container hasn't been measured yet.
        doReturn(0).when(mSpyControlContainer).getWidth();
        doReturn(0).when(mSpyControlContainer).getHeight();

        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);
        Assert.assertEquals(
                "Height request should be ignored if control container hasn't been measured.",
                NOTHING_OBSERVED,
                mObserver.heightRequested);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void showTabStrip() {
        settleTransitionDuringInitForNarrowWindow();
        setDeviceWidthDp(600);

        doReturn(TEST_TAB_STRIP_HEIGHT + TEST_TOOLBAR_HEIGHT)
                .when(mBrowserControlsVisibilityManager)
                .getTopControlsHeight();
        runOffsetTransitionForBrowserControlManager(
                /* beginOffset= */ TEST_TOOLBAR_HEIGHT,
                /* endOffset= */ TEST_TAB_STRIP_HEIGHT + TEST_TOOLBAR_HEIGHT);
        assertTabStripHeightForMargins(TEST_TAB_STRIP_HEIGHT);
        assertObservedHeight(TEST_TAB_STRIP_HEIGHT);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void showTabStripWithOffsetOverride() {
        settleTransitionDuringInitForNarrowWindow();
        // Simulate top controls size change from browser.
        doReturn(true).when(mBrowserControlsVisibilityManager).offsetOverridden();
        setDeviceWidthDp(600);
        assertTabStripHeightForMargins(TEST_TAB_STRIP_HEIGHT);
        assertObservedHeight(TEST_TAB_STRIP_HEIGHT);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void showTabStripWithBrowserControlForceShown() {
        settleTransitionDuringInitForNarrowWindow();
        doReturn(BrowserControlsState.SHOWN).when(mVisibilityDelegate).get();
        setDeviceWidthDp(600);
        assertTabStripHeightForMargins(TEST_TAB_STRIP_HEIGHT);
        assertObservedHeight(TEST_TAB_STRIP_HEIGHT);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void showTabStripWithBrowserControlForceHidden() {
        settleTransitionDuringInitForNarrowWindow();
        doReturn(BrowserControlsState.HIDDEN).when(mVisibilityDelegate).get();
        setDeviceWidthDp(600);
        assertTabStripHeightForMargins(TEST_TAB_STRIP_HEIGHT);
        assertObservedHeight(TEST_TAB_STRIP_HEIGHT);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void showTabStripWhileTopControlsHidden() {
        settleTransitionDuringInitForNarrowWindow();
        setDeviceWidthDp(600);

        // Assume the top control is hidden and content is at the top.
        doReturn(0).when(mBrowserControlsVisibilityManager).getContentOffset();
        getBrowserControlsObserver().onControlsOffsetChanged(0, 0, 0, 0, false, false);

        assertTabStripHeightForMargins(TEST_TAB_STRIP_HEIGHT);
        assertObservedHeight(TEST_TAB_STRIP_HEIGHT);
        assertObservedTransitionFinished(true);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void showTabStripWhileUrlBarFocused() {
        settleTransitionDuringInitForNarrowWindow();
        mCoordinator.onUrlFocusChange(true);
        setDeviceWidthDp(600);
        Assert.assertEquals(
                "Height request should be blocked by the url bar focus.",
                NOTHING_OBSERVED,
                mObserver.heightRequested);

        // Url focus animation finished to unblock the transition
        mCoordinator.onUrlAnimationFinished(false);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Assert.assertEquals(
                "Height request should go through after the url bar focus.",
                TEST_TAB_STRIP_HEIGHT,
                mObserver.heightRequested);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void showTabStripWhileTabObscured() {
        settleTransitionDuringInitForNarrowWindow();
        TabObscuringHandler.Token token = mTabObscuringHandler.obscure(Target.TAB_CONTENT);
        setDeviceWidthDp(600);
        Assert.assertEquals(
                "Height request should be blocked after tab obscured.",
                NOTHING_OBSERVED,
                mObserver.heightRequested);

        // Url focus animation finished to unblock the transition
        mTabObscuringHandler.unobscure(token);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Assert.assertEquals(
                "Height request should go through after the tab unobscured.",
                TEST_TAB_STRIP_HEIGHT,
                mObserver.heightRequested);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void showTabStripWhileTabAndToolbarObscured() {
        settleTransitionDuringInitForNarrowWindow();
        mTabObscuringHandler.obscure(Target.ALL_TABS_AND_TOOLBAR);
        setDeviceWidthDp(600);
        Assert.assertEquals(
                "Height request should go through if both the tab and toolbar are obscured.",
                TEST_TAB_STRIP_HEIGHT,
                mObserver.heightRequested);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void showTabStrip_TokenBeforeLayout() {
        settleTransitionDuringInitForNarrowWindow();
        int token = mCoordinator.requestDeferTabStripTransitionToken();
        setDeviceWidthDp(600);
        Assert.assertEquals(
                "Height request should be blocked by the token.",
                NOTHING_OBSERVED,
                mObserver.heightRequested);

        mCoordinator.releaseTabStripToken(token);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Assert.assertEquals(
                "Height request should go through after the token released.",
                TEST_TAB_STRIP_HEIGHT,
                mObserver.heightRequested);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void showTabStrip_TokenDuringLayout() {
        settleTransitionDuringInitForNarrowWindow();
        setConfigurationWithNewWidth(600);

        // Layout pass will trigger the delayed task for layout transition.
        simulateLayoutChange(600);
        ShadowLooper.idleMainLooper(100, TimeUnit.MILLISECONDS);
        int token = mCoordinator.requestDeferTabStripTransitionToken();
        Assert.assertEquals(
                "Height request should be blocked by the token.",
                NOTHING_OBSERVED,
                mObserver.heightRequested);

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Assert.assertEquals(
                "Height request should be blocked by the token.",
                NOTHING_OBSERVED,
                mObserver.heightRequested);

        mCoordinator.releaseTabStripToken(token);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Assert.assertEquals(
                "Height request should go through after the token released.",
                TEST_TAB_STRIP_HEIGHT,
                mObserver.heightRequested);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void showTabStrip_TokenReleaseEarly() {
        settleTransitionDuringInitForNarrowWindow();
        int token = mCoordinator.requestDeferTabStripTransitionToken();
        setConfigurationWithNewWidth(600);
        simulateLayoutChange(600);
        ShadowLooper.idleMainLooper(100, TimeUnit.MILLISECONDS);
        mCoordinator.releaseTabStripToken(token);
        Assert.assertEquals(
                "Height request should be blocked by the delayed layout request.",
                NOTHING_OBSERVED,
                mObserver.heightRequested);

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Assert.assertEquals(
                "Height request should go through after the token released.",
                TEST_TAB_STRIP_HEIGHT,
                mObserver.heightRequested);
    }

    @Test
    @Config(qualifiers = "w320dp")
    public void showTabStripBeforeLayout() {
        settleTransitionDuringInitForNarrowWindow();

        // Simulate the control container hasn't been measured yet.
        doReturn(0).when(mSpyControlContainer).getWidth();
        doReturn(0).when(mSpyControlContainer).getHeight();

        setDeviceWidthDp(600);
        Assert.assertEquals(
                "Height request should be ignored if control container hasn't been measured.",
                NOTHING_OBSERVED,
                mObserver.heightRequested);
    }

    @Test
    public void configurationChangedDuringDelayedTask() {
        setConfigurationWithNewWidth(NARROW_NORMAL_WINDOW_WIDTH);
        simulateLayoutChange(NARROW_NORMAL_WINDOW_WIDTH);
        ShadowLooper.idleMainLooper(100, TimeUnit.MILLISECONDS);
        // Tab strip still visible before the delayed transition started.
        assertTabStripHeightForMargins(TEST_TAB_STRIP_HEIGHT);

        setDeviceWidthDp(600);
        assertTabStripHeightForMargins(TEST_TAB_STRIP_HEIGHT);
    }

    @Test
    public void destroyDuringDelayedTask() {
        setConfigurationWithNewWidth(NARROW_NORMAL_WINDOW_WIDTH);
        simulateLayoutChange(NARROW_NORMAL_WINDOW_WIDTH);
        ShadowLooper.idleMainLooper(100, TimeUnit.MILLISECONDS);
        // Tab strip still visible before the delayed transition started.
        assertTabStripHeightForMargins(TEST_TAB_STRIP_HEIGHT);

        // Destroy the coordinator so the transition task is canceled.
        mCoordinator.destroy();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertTabStripHeightForMargins(TEST_TAB_STRIP_HEIGHT);
    }

    @Test
    public void destroyBeforeCapture() {
        setConfigurationWithNewWidth(NARROW_NORMAL_WINDOW_WIDTH);
        simulateLayoutChange(NARROW_NORMAL_WINDOW_WIDTH);
        ShadowLooper.idleMainLooper(300, TimeUnit.MILLISECONDS);
        // Tab strip still visible.
        assertTabStripHeightForMargins(TEST_TAB_STRIP_HEIGHT);
        // The capture task is scheduled.
        verify(mViewResourceAdapter).addOnResourceReadyCallback(any());

        // Destroy the coordinator so the capture task won't go through.
        mCoordinator.destroy();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertTabStripHeightForMargins(TEST_TAB_STRIP_HEIGHT);
    }

    @Test
    public void viewStubInflated() {
        doReturn(mSpyControlContainer.findToolbar)
                .when(mSpyControlContainer)
                .findViewById(R.id.find_toolbar);
        doReturn(mSpyControlContainer.dropTargetView)
                .when(mSpyControlContainer)
                .findViewById(R.id.toolbar_drag_drop_target_view);

        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);
        doReturn(TEST_TOOLBAR_HEIGHT)
                .when(mBrowserControlsVisibilityManager)
                .getTopControlsHeight();
        runOffsetTransitionForBrowserControlManager(
                /* beginOffset= */ TEST_TAB_STRIP_HEIGHT + TEST_TOOLBAR_HEIGHT,
                /* endOffset= */ TEST_TOOLBAR_HEIGHT);
        assertTabStripHeightForMargins(0);
    }

    @Test
    public void transitionFinishedUMASuccess() {
        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);
        doReturn(TEST_TOOLBAR_HEIGHT)
                .when(mBrowserControlsVisibilityManager)
                .getTopControlsHeight();

        try (HistogramWatcher ignored =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.DynamicTopChrome.TabStripTransition.Finished", true)) {
            runOffsetTransitionForBrowserControlManager(
                    /* beginOffset= */ TEST_TAB_STRIP_HEIGHT + TEST_TOOLBAR_HEIGHT,
                    /* endOffset= */ TEST_TOOLBAR_HEIGHT);
        }
    }

    @Test
    public void transitionFinishedUMAInterrupted() {
        setDeviceWidthDp(NARROW_NORMAL_WINDOW_WIDTH);
        doReturn(TEST_TOOLBAR_HEIGHT)
                .when(mBrowserControlsVisibilityManager)
                .getTopControlsHeight();

        int midOffset = TEST_TOOLBAR_HEIGHT + TEST_TAB_STRIP_HEIGHT / 2;
        mTopControlsContentOffset = midOffset;
        getBrowserControlsObserver().onControlsOffsetChanged(0, 0, 0, 0, false, false);

        try (HistogramWatcher ignored =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.DynamicTopChrome.TabStripTransition.Finished", false)) {
            setDeviceWidthDp(600);
        }
    }

    @Test
    @EnableFeatures(ChromeFeatureList.TAB_STRIP_TRANSITION_IN_DESKTOP_WINDOW)
    public void enterDesktopWindow_IncreaseHeight() {
        ToolbarFeatures.setIsTabStripLayoutOptimizationEnabledForTesting(true);
        // Simulate a rect update.
        int newHeight = 10 + TEST_TAB_STRIP_HEIGHT;
        Rect appHeaderRect = new Rect(0, 0, 600, newHeight);
        mAppHeaderState = new AppHeaderState(appHeaderRect, appHeaderRect, true);
        mCoordinator.onAppHeaderStateChanged(mAppHeaderState);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        Assert.assertEquals(
                "Height request should include the top padding.",
                newHeight,
                mObserver.heightRequested);

        // Push a browser control height update to kick off the height transition.
        doReturn(TEST_TOOLBAR_HEIGHT).when(mBrowserControlsVisibilityManager).getContentOffset();
        getBrowserControlsObserver().onControlsOffsetChanged(0, 0, 0, 0, false, false);

        assertTabStripHeightForMargins(newHeight);
        assertObservedHeight(newHeight);
    }

    @Test
    @EnableFeatures(ChromeFeatureList.TAB_STRIP_TRANSITION_IN_DESKTOP_WINDOW)
    public void enterDesktopWindow_DecreaseHeight() {
        ToolbarFeatures.setIsTabStripLayoutOptimizationEnabledForTesting(true);
        // Simulate a rect update that has a smaller height.
        int newHeight = TEST_TAB_STRIP_HEIGHT - 10;
        int expectedHeight = mReservedTopPadding + TEST_TAB_STRIP_HEIGHT;
        Rect appHeaderRect = new Rect(0, 0, 600, newHeight);
        mAppHeaderState = new AppHeaderState(appHeaderRect, appHeaderRect, true);
        mCoordinator.onAppHeaderStateChanged(mAppHeaderState);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        Assert.assertEquals(
                "When new height is less than height with reserved padding, use that instead.",
                expectedHeight,
                mObserver.heightRequested);

        // Push a browser control height update to kick off the height transition.
        doReturn(TEST_TOOLBAR_HEIGHT).when(mBrowserControlsVisibilityManager).getContentOffset();
        getBrowserControlsObserver().onControlsOffsetChanged(0, 0, 0, 0, false, false);

        assertTabStripHeightForMargins(expectedHeight);
        assertObservedHeight(expectedHeight);
    }

    @Test
    @EnableFeatures(ChromeFeatureList.TAB_STRIP_TRANSITION_IN_DESKTOP_WINDOW)
    public void enterDesktopWindow_DecreaseWidth() {
        ToolbarFeatures.setIsTabStripLayoutOptimizationEnabledForTesting(true);
        // Simulate a rect update that has a smaller width.
        int newHeight = TEST_TAB_STRIP_HEIGHT + 10;
        Rect appHeaderRect = new Rect(0, 0, NARROW_DESKTOP_WINDOW_WIDTH, newHeight);
        mAppHeaderState = new AppHeaderState(appHeaderRect, appHeaderRect, true);
        mCoordinator.onAppHeaderStateChanged(mAppHeaderState);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        Assert.assertEquals(
                "Narrow width does not trigger tab strip height transition.",
                newHeight,
                mObserver.heightRequested);
        Assert.assertEquals(
                "Fade transition start opacity is incorrect.",
                0f,
                mDelegate.fadeTransitionStartOpacity,
                0f);
        Assert.assertEquals(
                "Fade transition end opacity is incorrect.",
                1f,
                mDelegate.fadeTransitionEndOpacity,
                0f);
    }

    @Test
    @EnableFeatures(ChromeFeatureList.TAB_STRIP_TRANSITION_IN_DESKTOP_WINDOW)
    public void enterDesktopWindow_NarrowInitialWidth() {
        ToolbarFeatures.setIsTabStripLayoutOptimizationEnabledForTesting(true);
        // Simulate a rect update that has a smaller width.
        int newHeight = TEST_TAB_STRIP_HEIGHT + 10;
        Rect appHeaderRect = new Rect(0, 0, NARROW_DESKTOP_WINDOW_WIDTH, newHeight);
        mAppHeaderState = new AppHeaderState(appHeaderRect, appHeaderRect, true);

        // Create the transition coordinator again with initial value of AppHeaderState
        setUpTabStripTransitionCoordinator();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        Assert.assertEquals(
                "Narrow width does not trigger tab strip height transition.",
                newHeight,
                mObserver.heightRequested);
        Assert.assertEquals(
                "Fade transition start opacity is incorrect.",
                0f,
                mDelegate.fadeTransitionStartOpacity,
                0f);
        Assert.assertEquals(
                "Fade transition end opacity is incorrect.",
                1f,
                mDelegate.fadeTransitionEndOpacity,
                0f);
    }

    @Test
    @EnableFeatures(ChromeFeatureList.TAB_STRIP_TRANSITION_IN_DESKTOP_WINDOW)
    public void enterDesktopWindow_WideInitialWidth() {
        ToolbarFeatures.setIsTabStripLayoutOptimizationEnabledForTesting(true);
        // Simulate a rect update that has a larger width.
        int newHeight = TEST_TAB_STRIP_HEIGHT + 10;
        Rect appHeaderRect = new Rect(0, 0, 600, newHeight);
        mAppHeaderState = new AppHeaderState(appHeaderRect, appHeaderRect, true);

        // Create the transition coordinator again with initial value of AppHeaderState
        setUpTabStripTransitionCoordinator();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        Assert.assertEquals(
                "Fade transition should not be triggered.",
                NOTHING_OBSERVED,
                mDelegate.fadeTransitionStartOpacity,
                0f);
    }

    @Test
    @EnableFeatures(ChromeFeatureList.TAB_STRIP_TRANSITION_IN_DESKTOP_WINDOW)
    public void enterDesktopWindow_WithouControlContainerLayout() {
        ToolbarFeatures.setIsTabStripLayoutOptimizationEnabledForTesting(true);
        // Simulate a rect update that has a smaller width.
        int newHeight = TEST_TAB_STRIP_HEIGHT + 10;
        Rect appHeaderRect = new Rect(0, 0, NARROW_NORMAL_WINDOW_WIDTH, newHeight);
        mAppHeaderState = new AppHeaderState(appHeaderRect, appHeaderRect, true);

        // Set the height as if the first measure pass hasn't happened yet.
        doReturn(0).when(mSpyControlContainer).getHeight();
        doReturn(0).when(mSpyControlContainer).getWidth();

        // Create the transition coordinator again with initial value of AppHeaderState.
        setUpTabStripTransitionCoordinator();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        Assert.assertEquals(
                "Height request should be ignored if control container hasn't been measured.",
                NOTHING_OBSERVED,
                mObserver.heightRequested);
    }

    @Test
    public void recordHistogramWindowResize_LayoutChangeInDesktopWindow() {
        // Simulate desktop windowing mode.
        mAppHeaderState = new AppHeaderState(new Rect(), new Rect(), /* isInDesktopWindow= */ true);
        var watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.DynamicTopChrome.WindowResize.DesktopWindowModeState",
                        DesktopWindowModeState.ACTIVE);
        // Histogram should be emitted only when the strip size is changing across multiple layout
        // changes.
        simulateLayoutChange(NARROW_NORMAL_WINDOW_WIDTH);
        simulateLayoutChange(NARROW_NORMAL_WINDOW_WIDTH);
        watcher.assertExpected();
    }

    @Test
    public void recordHistogramWindowResize_LayoutChangeNotInDesktopWindow_SupportedDevice() {
        // Simulate non-desktop windowing mode on a supported device.
        mAppHeaderState =
                new AppHeaderState(new Rect(), new Rect(), /* isInDesktopWindow= */ false);
        var watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.DynamicTopChrome.WindowResize.DesktopWindowModeState",
                        DesktopWindowModeState.INACTIVE);
        simulateLayoutChange(NARROW_NORMAL_WINDOW_WIDTH);
        watcher.assertExpected();
    }

    @Test
    public void recordHistogramWindowResize_LayoutChangeNotInDesktopWindow_UnsupportedDevice() {
        // Create the transition coordinator with an initial null value of
        // DesktopWindowStateProvider that is representative of an unsupported device.
        mDesktopWindowStateProvider = null;
        setUpTabStripTransitionCoordinator();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        var watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.DynamicTopChrome.WindowResize.DesktopWindowModeState",
                        DesktopWindowModeState.UNAVAILABLE);
        simulateLayoutChange(NARROW_NORMAL_WINDOW_WIDTH);
        watcher.assertExpected();
    }

    private void setUpTabStripTransitionCoordinator() {
        mCoordinator =
                new TabStripTransitionCoordinator(
                        mBrowserControlsVisibilityManager,
                        mControlContainer,
                        mSpyControlContainer.toolbarLayout,
                        TEST_TAB_STRIP_HEIGHT,
                        mTabObscuringHandler,
                        mDesktopWindowStateProvider,
                        mDelegateSupplier);
        mObserver = new TestObserver();
        mCoordinator.addObserver(mObserver);
    }

    /** Run #onControlsOffsetChanged, changing content offset from |beginOffset| to |endOffset|. */
    private void runOffsetTransitionForBrowserControlManager(int beginOffset, int endOffset) {
        mTopControlsContentOffset = beginOffset;

        final int step = (beginOffset - endOffset) / 10;
        for (int turns = 0; turns <= 10; turns++) {
            // Simulate top controls size change from browser. Input values doesn't matter in this
            // call.
            getBrowserControlsObserver().onControlsOffsetChanged(0, 0, 0, 0, false, false);
            if (mTopControlsContentOffset == endOffset) break;

            assertObservedTransitionFinished(false);
            if (step > 0) {
                mTopControlsContentOffset = Math.max(endOffset, mTopControlsContentOffset - step);
            } else {
                mTopControlsContentOffset = Math.min(endOffset, mTopControlsContentOffset - step);
            }
        }
        assertObservedTransitionFinished(true);
    }

    private void setDeviceWidthDp(int widthDp) {
        Configuration configuration = setConfigurationWithNewWidth(widthDp);
        simulateConfigurationChanged(configuration);
        simulateLayoutChange(widthDp);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
    }

    private Configuration setConfigurationWithNewWidth(int widthDp) {
        Resources res = mActivity.getResources();
        DisplayMetrics displayMetrics = res.getDisplayMetrics();
        displayMetrics.widthPixels = (int) displayMetrics.density * widthDp;

        Configuration configuration = res.getConfiguration();
        configuration.screenWidthDp = widthDp;
        mActivity.createConfigurationContext(configuration);
        return configuration;
    }

    private void assertTabStripHeightForMargins(int tabStripHeight) {
        Assert.assertEquals(
                "Top margin is wrong for toolbarLayout.",
                tabStripHeight,
                mSpyControlContainer.toolbarLayout.getTopMargin());
        Assert.assertEquals(
                "Top margin is wrong for findToolbar.",
                tabStripHeight + TEST_TOOLBAR_HEIGHT,
                mSpyControlContainer.findToolbar.getTopMargin());
        Assert.assertEquals(
                "Top margin is wrong for dropTargetView.",
                tabStripHeight,
                mSpyControlContainer.dropTargetView.getTopMargin());
        Assert.assertEquals(
                "Top margin is wrong for toolbarHairline.",
                tabStripHeight + TEST_TOOLBAR_HEIGHT,
                mSpyControlContainer.toolbarHairline.getTopMargin());
    }

    private void assertObservedHeight(int tabStripHeight) {
        Assert.assertEquals(
                "#getHeight has a different value.",
                tabStripHeight,
                mCoordinator.getTabStripHeight());

        Assert.assertEquals(
                "Delegate#onHeightChanged received a different value.",
                tabStripHeight,
                mDelegate.heightChanged);
    }

    private void assertObservedTransitionFinished(boolean finished) {
        Assert.assertEquals(
                "Transition finished signal not dispatched. Current contentOffset: "
                        + mTopControlsContentOffset,
                finished,
                mDelegate.heightTransitionFinished);
    }

    private BrowserControlsStateProvider.Observer getBrowserControlsObserver() {
        var observer = mBrowserControlsObserver.getValue();
        Assert.assertNotNull("Browser controls observer not attached.", observer);
        return observer;
    }

    private Void triggerCapture() {
        var callback = mOnCaptureReadyCallback.getValue();
        Assert.assertNotNull("Capture callback is null.", callback);
        callback.onResult(null);
        return null;
    }

    // For test cases init with narrow width, the initialization will create an transition request.
    private void settleTransitionDuringInitForNarrowWindow() {
        mTopControlsContentOffset = TEST_TOOLBAR_HEIGHT;
        doReturn(TEST_TOOLBAR_HEIGHT)
                .when(mBrowserControlsVisibilityManager)
                .getTopControlsHeight();
        getBrowserControlsObserver().onControlsOffsetChanged(0, 0, 0, 0, false, false);
        getBrowserControlsObserver().onControlsOffsetChanged(0, 0, 0, 0, false, false);
        mObserver = new TestObserver();
        mCoordinator.addObserver(mObserver);
        mDelegate.reset();
    }

    private void simulateLayoutChange(int width) {
        Assert.assertNotNull(mSpyControlContainer.onLayoutChangeListener);
        mSpyControlContainer.onLayoutChangeListener.onLayoutChange(
                mSpyControlContainer,
                /* left= */ 0,
                /* top= */ 0,
                /* right= */ width,
                /* bottom= */ 0,
                0,
                0,
                0,
                0);
    }

    private void simulateConfigurationChanged(Configuration newConfig) {
        mCoordinator.onConfigurationChanged(newConfig != null ? newConfig : new Configuration());
    }

    // Due to the complexity to use the real views for top toolbar in robolectric tests, use view
    // mocks for the sake of unit tests.
    static class TestControlContainerView extends FrameLayout {
        public TestView toolbarLayout;
        public TestView toolbarHairline;
        public TestView findToolbar;
        public TestView dropTargetView;

        @Nullable public View.OnLayoutChangeListener onLayoutChangeListener;

        static TestControlContainerView createSpy(Context context) {
            TestControlContainerView controlContainer =
                    Mockito.spy(new TestControlContainerView(context, null));

            doReturn(controlContainer.toolbarLayout)
                    .when(controlContainer)
                    .findViewById(R.id.toolbar);
            doReturn(controlContainer.toolbarHairline)
                    .when(controlContainer)
                    .findViewById(R.id.toolbar_hairline);
            doReturn(controlContainer.findToolbar)
                    .when(controlContainer)
                    .findViewById(R.id.find_toolbar_tablet_stub);
            doReturn(controlContainer.dropTargetView)
                    .when(controlContainer)
                    .findViewById(R.id.target_view_stub);

            doAnswer(args -> context.getResources().getDisplayMetrics().widthPixels)
                    .when(controlContainer)
                    .getWidth();
            // Set a test height for the control container as if it's already being measured.
            doReturn(TEST_TOOLBAR_HEIGHT + TEST_TAB_STRIP_HEIGHT)
                    .when(controlContainer)
                    .getHeight();
            doAnswer(
                            args -> {
                                controlContainer.onLayoutChangeListener = args.getArgument(0);
                                return null;
                            })
                    .when(controlContainer)
                    .addOnLayoutChangeListener(any());

            return controlContainer;
        }

        public TestControlContainerView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);

            toolbarLayout = Mockito.spy(new TestView(context, attrs));
            findToolbar = Mockito.spy(new TestView(context, attrs));
            dropTargetView = Mockito.spy(new TestView(context, attrs));
            when(toolbarLayout.getHeight()).thenReturn(TEST_TOOLBAR_HEIGHT);
            when(findToolbar.getHeight()).thenReturn(TEST_TOOLBAR_HEIGHT);
            when(dropTargetView.getHeight()).thenReturn(TEST_TOOLBAR_HEIGHT);
            toolbarHairline = new TestView(context, attrs);

            MarginLayoutParams sourceParams =
                    new MarginLayoutParams(
                            ViewGroup.LayoutParams.MATCH_PARENT,
                            ViewGroup.LayoutParams.MATCH_PARENT);
            sourceParams.topMargin = TEST_TAB_STRIP_HEIGHT + TEST_TOOLBAR_HEIGHT;
            addView(toolbarHairline, new MarginLayoutParams(sourceParams));
            addView(findToolbar, new MarginLayoutParams(sourceParams));

            sourceParams.topMargin = TEST_TAB_STRIP_HEIGHT;
            sourceParams.height = TEST_TOOLBAR_HEIGHT;
            addView(toolbarLayout, new MarginLayoutParams(sourceParams));
            addView(dropTargetView, new MarginLayoutParams(sourceParams));
        }
    }

    static class TestView extends View {
        public TestView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }

        public int getTopMargin() {
            return ((MarginLayoutParams) getLayoutParams()).topMargin;
        }
    }

    static class TestObserver implements TabStripHeightObserver {
        public int heightRequested = NOTHING_OBSERVED;

        @Override
        public void onTransitionRequested(int newHeight) {
            heightRequested = newHeight;
        }
    }

    static class TestDelegate implements TabStripTransitionDelegate {
        public int heightChanged = NOTHING_OBSERVED;
        public boolean heightTransitionFinished;
        public float fadeTransitionStartOpacity = NOTHING_OBSERVED;
        public float fadeTransitionEndOpacity = NOTHING_OBSERVED;

        void reset() {
            heightChanged = NOTHING_OBSERVED;
            heightTransitionFinished = false;
            fadeTransitionStartOpacity = NOTHING_OBSERVED;
            fadeTransitionEndOpacity = NOTHING_OBSERVED;
        }

        @Override
        public void onHeightChanged(int newHeight) {
            heightChanged = newHeight;
        }

        @Override
        public void onHeightTransitionFinished() {
            heightTransitionFinished = true;
        }

        @Override
        public void onFadeTransitionRequested(
                float startOpacity, float endOpacity, int durationMs) {
            fadeTransitionStartOpacity = startOpacity;
            fadeTransitionEndOpacity = endOpacity;
        }
    }
}