chromium/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiMediatorUnitTest.java

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

package org.chromium.chrome.browser.tasks.tab_management;

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

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.view.View;

import androidx.annotation.Nullable;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.LooperMode;

import org.chromium.base.Callback;
import org.chromium.base.supplier.LazyOneshotSupplier;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.layouts.LayoutStateProvider.LayoutStateObserver;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.IncognitoStateProvider;
import org.chromium.chrome.browser.tabmodel.IncognitoStateProvider.IncognitoStateObserver;
import org.chromium.chrome.browser.tabmodel.TabCreator;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelFilterProvider;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorImpl;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilterObserver;
import org.chromium.chrome.browser.toolbar.bottom.BottomControlsCoordinator;
import org.chromium.chrome.tab_ui.R;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/** Tests for {@link TabGroupUiMediator}. */
@SuppressWarnings({"ResultOfMethodCallIgnored", "ArraysAsListWithZeroOrOneArgument", "unchecked"})
@RunWith(BaseRobolectricTestRunner.class)
@LooperMode(LooperMode.Mode.LEGACY)
public class TabGroupUiMediatorUnitTest {
    private static final int TAB1_ID = 456;
    private static final int TAB2_ID = 789;
    private static final int TAB3_ID = 123;
    private static final int TAB4_ID = 357;
    private static final int POSITION1 = 0;
    private static final int POSITION2 = 1;
    private static final int POSITION3 = 2;
    private static final int TAB1_ROOT_ID = TAB1_ID;
    private static final int TAB2_ROOT_ID = TAB2_ID;
    private static final int TAB3_ROOT_ID = TAB2_ID;
    private static final int PRIMARY_BACKGROUND_COLOR = Color.WHITE;
    private static final int INCOGNITO_BACKGROUND_COLOR = Color.GRAY;

    @Mock BottomControlsCoordinator.BottomControlsVisibilityController mVisibilityController;
    @Mock TabGroupUiMediator.ResetHandler mResetHandler;
    @Mock TabModelSelectorImpl mTabModelSelector;
    @Mock TabContentManager mTabContentManager;
    @Mock TabCreatorManager mTabCreatorManager;
    @Mock TabCreator mTabCreator;
    @Mock LayoutStateProvider mLayoutManager;
    @Mock IncognitoStateProvider mIncognitoStateProvider;
    @Mock TabModel mTabModel;
    @Mock View mView;
    @Mock TabModelFilterProvider mTabModelFilterProvider;
    @Mock TabGroupModelFilter mTabGroupModelFilter;
    @Mock TabGridDialogMediator.DialogController mTabGridDialogController;
    @Mock Context mContext;
    @Mock ObservableSupplier<Boolean> mOmniboxFocusStateSupplier;
    @Mock private Resources mResources;
    @Mock private ObservableSupplierImpl<TabModel> mTabModelSupplier;
    @Captor private ArgumentCaptor<Callback<TabModel>> mTabModelSupplierObserverCaptor;
    @Captor ArgumentCaptor<TabModelObserver> mTabModelObserverArgumentCaptor;
    @Captor ArgumentCaptor<LayoutStateObserver> mLayoutStateObserverCaptor;
    @Captor ArgumentCaptor<IncognitoStateObserver> mIncognitoStateObserverArgumentCaptor;
    @Captor ArgumentCaptor<TabGroupModelFilterObserver> mTabGroupModelFilterObserverArgumentCaptor;
    @Captor ArgumentCaptor<TabObserver> mTabObserverCaptor;
    @Captor ArgumentCaptor<Callback<Boolean>> mOmniboxFocusObserverCaptor;

    private final ObservableSupplierImpl<Boolean> mHandleBackPressChangedSupplier =
            new ObservableSupplierImpl<>();
    private final ObservableSupplierImpl<Boolean> mTabGridDialogBackPressSupplier =
            new ObservableSupplierImpl<>();
    private final ObservableSupplierImpl<Boolean> mTabGridDialogShowingOrAnimationSupplier =
            new ObservableSupplierImpl<>();

    private Tab mTab1;
    private Tab mTab2;
    private Tab mTab3;
    private List<Tab> mTabGroup1;
    private List<Tab> mTabGroup2;
    private PropertyModel mModel;
    private TabGroupUiMediator mTabGroupUiMediator;
    private InOrder mResetHandlerInOrder;
    private InOrder mVisibilityControllerInOrder;
    private OneshotSupplierImpl<LayoutStateProvider> mLayoutStateProviderSupplier =
            new OneshotSupplierImpl<>();
    private LazyOneshotSupplier<TabGridDialogMediator.DialogController> mDialogControllerSupplier;

    private Tab prepareTab(int tabId, int rootId) {
        Tab tab = TabUiUnitTestUtils.prepareTab(tabId, rootId);
        doReturn(tab).when(mTabModelSelector).getTabById(tabId);
        return tab;
    }

    private TabModel prepareIncognitoTabModel() {
        Tab newTab = prepareTab(TAB4_ID, TAB4_ID);
        List<Tab> tabs = new ArrayList<>(Arrays.asList(newTab));
        doReturn(tabs).when(mTabGroupModelFilter).getRelatedTabList(TAB4_ID);
        TabModel incognitoTabModel = mock(TabModel.class);
        doReturn(newTab).when(incognitoTabModel).getTabAt(POSITION1);
        doReturn(true).when(incognitoTabModel).isIncognito();
        doReturn(1).when(incognitoTabModel).getCount();
        return incognitoTabModel;
    }

    private void verifyNeverReset() {
        mResetHandlerInOrder.verify(mResetHandler, never()).resetStripWithListOfTabs(any());
        mVisibilityControllerInOrder
                .verify(mVisibilityController, never())
                .setBottomControlsVisible(anyBoolean());
    }

    private void verifyResetStrip(boolean isVisible, @Nullable List<Tab> tabs) {
        mResetHandlerInOrder.verify(mResetHandler).resetStripWithListOfTabs(tabs);
        mVisibilityControllerInOrder
                .verify(mVisibilityController)
                .setBottomControlsVisible(isVisible);
    }

    private void initAndAssertProperties(@Nullable Tab currentTab) {
        doReturn(true).when(mTabModelSelector).isTabStateInitialized();
        if (currentTab == null) {
            doReturn(TabModel.INVALID_TAB_INDEX).when(mTabModel).index();
            doReturn(0).when(mTabModel).getCount();
            doReturn(0).when(mTabGroupModelFilter).getCount();
            doReturn(null).when(mTabModelSelector).getCurrentTab();
        } else {
            doReturn(mTabModel.indexOf(currentTab)).when(mTabModel).index();
            doReturn(currentTab).when(mTabModelSelector).getCurrentTab();
        }

        // Fake a similar behavior to the supplier in TabGroupUiCoordinator.
        mDialogControllerSupplier = LazyOneshotSupplier.fromValue(mTabGridDialogController);
        doReturn(mTabGridDialogBackPressSupplier)
                .when(mTabGridDialogController)
                .getHandleBackPressChangedSupplier();
        doReturn(mTabGridDialogShowingOrAnimationSupplier)
                .when(mTabGridDialogController)
                .getShowingOrAnimationSupplier();
        mTabGroupUiMediator =
                new TabGroupUiMediator(
                        mContext,
                        mVisibilityController,
                        mHandleBackPressChangedSupplier,
                        mResetHandler,
                        mModel,
                        mTabModelSelector,
                        mTabContentManager,
                        mTabCreatorManager,
                        mLayoutStateProviderSupplier,
                        mIncognitoStateProvider,
                        mDialogControllerSupplier,
                        mOmniboxFocusStateSupplier,
                        PRIMARY_BACKGROUND_COLOR,
                        INCOGNITO_BACKGROUND_COLOR);

        if (currentTab == null) {
            verifyNeverReset();
            return;
        }

        // Verify strip button content description setup.
        verify(mContext).getString(R.string.accessibility_bottom_tab_strip_expand_tab_sheet);

        verify(mTabModelSupplier).addObserver(mTabModelSupplierObserverCaptor.capture());

        // Verify strip initial reset.
        List<Tab> tabs = mTabGroupModelFilter.getRelatedTabList(currentTab.getId());
        if (mTabGroupModelFilter.isTabInTabGroup(currentTab)) {
            verifyResetStrip(true, tabs);
        } else {
            verifyResetStrip(false, null);
        }
    }

    @Before
    public void setUp() {
        // After setUp(), tabModel has 3 tabs in the following order: mTab1, mTab2 and mTab3. If
        // TabGroup is enabled, mTab2 and mTab3 are in a group. Each test must call
        // initAndAssertProperties(selectedTab) first, with selectedTab being the currently selected
        // tab when the TabGroupUiMediator is created.
        MockitoAnnotations.initMocks(this);

        when(mContext.getResources()).thenReturn(mResources);
        when(mResources.getInteger(org.chromium.ui.R.integer.min_screen_width_bucket))
                .thenReturn(1);

        // Set up Tabs
        mTab1 = prepareTab(TAB1_ID, TAB1_ROOT_ID);
        mTab2 = prepareTab(TAB2_ID, TAB2_ROOT_ID);
        mTab3 = prepareTab(TAB3_ID, TAB3_ROOT_ID);
        mTabGroup1 = new ArrayList<>(Arrays.asList(mTab1));
        mTabGroup2 = new ArrayList<>(Arrays.asList(mTab2, mTab3));

        // Setup TabModel.
        doReturn(mTabModel).when(mTabModel).getComprehensiveModel();
        doReturn(mTabModel).when(mTabModelSelector).getModel(false);
        doReturn(false).when(mTabModel).isIncognito();
        doReturn(mTabModel).when(mTabModelSelector).getModel(false);
        doReturn(3).when(mTabModel).getCount();
        doReturn(0).when(mTabModel).index();
        doReturn(mTab1).when(mTabModel).getTabAt(0);
        doReturn(mTab2).when(mTabModel).getTabAt(1);
        doReturn(mTab3).when(mTabModel).getTabAt(2);
        doReturn(POSITION1).when(mTabModel).indexOf(mTab1);
        doReturn(POSITION2).when(mTabModel).indexOf(mTab2);
        doReturn(POSITION3).when(mTabModel).indexOf(mTab3);
        doNothing().when(mTab1).addObserver(mTabObserverCaptor.capture());
        doNothing().when(mTab2).addObserver(mTabObserverCaptor.capture());
        doNothing().when(mTab3).addObserver(mTabObserverCaptor.capture());

        // Setup TabGroupModelFilter.
        doReturn(false).when(mTabGroupModelFilter).isIncognito();
        doReturn(2).when(mTabGroupModelFilter).getCount();
        doReturn(mTabGroup1).when(mTabGroupModelFilter).getRelatedTabList(TAB1_ID);
        doReturn(mTabGroup2).when(mTabGroupModelFilter).getRelatedTabList(TAB2_ID);
        doReturn(mTabGroup2).when(mTabGroupModelFilter).getRelatedTabList(TAB3_ID);
        doReturn(false).when(mTabGroupModelFilter).isTabInTabGroup(mTab1);
        doReturn(true).when(mTabGroupModelFilter).isTabInTabGroup(mTab2);
        doReturn(true).when(mTabGroupModelFilter).isTabInTabGroup(mTab3);

        doReturn(mTabGroupModelFilter).when(mTabModelFilterProvider).getCurrentTabModelFilter();
        doReturn(mTabGroupModelFilter).when(mTabModelFilterProvider).getTabModelFilter(true);
        doReturn(mTabGroupModelFilter).when(mTabModelFilterProvider).getTabModelFilter(false);
        doNothing()
                .when(mTabGroupModelFilter)
                .addTabGroupObserver(mTabGroupModelFilterObserverArgumentCaptor.capture());

        // Set up TabModelSelector and TabModelFilterProvider.
        List<TabModel> tabModelList = new ArrayList<>();
        tabModelList.add(mTabModel);
        doReturn(mTabModel).when(mTabModelSelector).getCurrentModel();
        doReturn(mTab1).when(mTabModelSelector).getCurrentTab();
        doReturn(TAB1_ID).when(mTabModelSelector).getCurrentTabId();
        doReturn(tabModelList).when(mTabModelSelector).getModels();
        when(mTabModelSelector.getCurrentTabModelSupplier()).thenReturn(mTabModelSupplier);

        doReturn(mTabModelFilterProvider).when(mTabModelSelector).getTabModelFilterProvider();
        doNothing()
                .when(mTabModelFilterProvider)
                .addTabModelFilterObserver(mTabModelObserverArgumentCaptor.capture());

        // Set up OverviewModeBehavior
        doNothing().when(mLayoutManager).addObserver(mLayoutStateObserverCaptor.capture());
        mLayoutStateProviderSupplier.set(mLayoutManager);

        // Set up IncognitoStateProvider
        doNothing()
                .when(mIncognitoStateProvider)
                .addIncognitoStateObserverAndTrigger(
                        mIncognitoStateObserverArgumentCaptor.capture());

        // Set up ResetHandler
        doNothing().when(mResetHandler).resetStripWithListOfTabs(any());
        doNothing().when(mResetHandler).resetGridWithListOfTabs(any());

        // Set up TabCreatorManager
        doReturn(mTabCreator).when(mTabCreatorManager).getTabCreator(anyBoolean());
        doReturn(null).when(mTabCreator).createNewTab(any(), anyInt(), any());

        // Set up omnibox focus state observer.
        doReturn(nullValue())
                .when(mOmniboxFocusStateSupplier)
                .addObserver(mOmniboxFocusObserverCaptor.capture());

        mResetHandlerInOrder = inOrder(mResetHandler);
        mVisibilityControllerInOrder = inOrder(mVisibilityController);
        mModel = new PropertyModel(TabGroupUiProperties.ALL_KEYS);
    }

    /*********************** Tab group related tests *************************/

    @Test
    public void verifyInitialization_NoTab_TabGroup() {
        initAndAssertProperties(null);
    }

    @Test
    public void verifyInitialization_SingleTab() {
        initAndAssertProperties(mTab1);
    }

    @Test
    public void verifyInitialization_TabGroup() {
        // Tab 2 is in a tab group.
        initAndAssertProperties(mTab2);
    }

    @Test
    public void onClickShowGroupDialogButton_TabGroup() {
        initAndAssertProperties(mTab2);

        View.OnClickListener listener =
                mModel.get(TabGroupUiProperties.SHOW_GROUP_DIALOG_BUTTON_ON_CLICK_LISTENER);
        assertThat(listener, instanceOf(View.OnClickListener.class));

        listener.onClick(mView);

        verify(mTabContentManager).cacheTabThumbnail(mTab2);
        verify(mResetHandler).resetGridWithListOfTabs(mTabGroup2);
    }

    @Test
    public void onClickShowGroupDialogButton_TabGroup_ShowingOrAnimation() {
        initAndAssertProperties(mTab2);
        mDialogControllerSupplier.get();
        mTabGridDialogShowingOrAnimationSupplier.set(true);

        View.OnClickListener listener =
                mModel.get(TabGroupUiProperties.SHOW_GROUP_DIALOG_BUTTON_ON_CLICK_LISTENER);
        assertThat(listener, instanceOf(View.OnClickListener.class));

        listener.onClick(mView);
        verify(mResetHandler, never()).resetGridWithListOfTabs(any());

        mTabGridDialogShowingOrAnimationSupplier.set(false);

        listener.onClick(mView);
        verify(mResetHandler).resetGridWithListOfTabs(mTabGroup2);
    }

    @Test
    public void onClickNewTabButton_TabGroup() {
        initAndAssertProperties(mTab1);

        View.OnClickListener listener =
                mModel.get(TabGroupUiProperties.NEW_TAB_BUTTON_ON_CLICK_LISTENER);
        assertThat(listener, instanceOf(View.OnClickListener.class));

        listener.onClick(mView);

        verify(mTabCreator)
                .createNewTab(
                        isA(LoadUrlParams.class), eq(TabLaunchType.FROM_TAB_GROUP_UI), eq(mTab1));
    }

    @Test
    public void tabSelection_NotSameGroup_GroupToSingleTab() {
        initAndAssertProperties(mTab2);

        // Mock selecting tab 1, and the last selected tab is tab 2 which is in different group.
        mTabModelObserverArgumentCaptor
                .getValue()
                .didSelectTab(mTab1, TabSelectionType.FROM_USER, TAB2_ID);

        // Strip should not be showing since tab 1 is a single tab.
        verifyResetStrip(false, null);
    }

    @Test
    public void tabSelection_NotSameGroup_GroupToGroup() {
        initAndAssertProperties(mTab2);

        // Mock that tab 1 is not a single tab.
        Tab newTab = prepareTab(TAB4_ID, TAB4_ID);
        List<Tab> tabs = new ArrayList<>(Arrays.asList(mTab1, newTab));
        doReturn(tabs).when(mTabGroupModelFilter).getRelatedTabList(TAB1_ID);
        doReturn(true).when(mTabGroupModelFilter).isTabInTabGroup(mTab1);
        doReturn(true).when(mTabGroupModelFilter).isTabInTabGroup(newTab);

        // Mock selecting tab 1, and the last selected tab is tab 2 which is in different group.
        mTabModelObserverArgumentCaptor
                .getValue()
                .didSelectTab(mTab1, TabSelectionType.FROM_USER, TAB2_ID);

        // Strip should be showing since we are selecting a group.
        verifyResetStrip(true, tabs);
    }

    @Test
    public void tabSelection_NotSameGroup_SingleTabToGroup() {
        initAndAssertProperties(mTab1);

        // Mock that tab 2 is not a single tab.
        List<Tab> tabGroup = mTabGroupModelFilter.getRelatedTabList(TAB2_ID);
        assertThat(tabGroup.size(), equalTo(2));

        // Mock selecting tab 2, and the last selected tab is tab 1 which is a single tab.
        mTabModelObserverArgumentCaptor
                .getValue()
                .didSelectTab(mTab2, TabSelectionType.FROM_USER, TAB1_ID);

        // Strip should be showing since we are selecting a group.
        verifyResetStrip(true, tabGroup);
    }

    @Test
    public void tabSelection_NotSameGroup_SingleTabToSingleTab() {
        initAndAssertProperties(mTab1);

        // Mock that new tab is a single tab.
        Tab newTab = prepareTab(TAB4_ID, TAB4_ID);
        List<Tab> tabs = new ArrayList<>(Arrays.asList(newTab));
        doReturn(tabs).when(mTabGroupModelFilter).getRelatedTabList(TAB4_ID);

        // Mock selecting new tab, and the last selected tab is tab 1 which is also a single tab.
        mTabModelObserverArgumentCaptor
                .getValue()
                .didSelectTab(newTab, TabSelectionType.FROM_USER, TAB1_ID);

        // Strip should not be showing since new tab is a single tab.
        verifyResetStrip(false, null);
    }

    @Test
    public void tabSelection_SameGroup_TabGroup() {
        initAndAssertProperties(mTab2);

        // Mock selecting tab 3, and the last selected tab is tab 2 which is in the same group.
        mTabModelObserverArgumentCaptor
                .getValue()
                .didSelectTab(mTab3, TabSelectionType.FROM_USER, TAB2_ID);

        // Strip should not be reset since we are selecting in one group.
        verifyNeverReset();
    }

    @Test
    public void tabSelection_ScrollToSelectedIndex() {
        initAndAssertProperties(mTab1);
        assertThat(mModel.get(TabGroupUiProperties.INITIAL_SCROLL_INDEX), equalTo(null));

        // Mock that {tab2, tab3} are in the same tab group.
        List<Tab> tabGroup = mTabGroupModelFilter.getRelatedTabList(TAB2_ID);
        assertThat(tabGroup.size(), equalTo(2));

        // Mock selecting tab 3, and the last selected tab is tab 1 which is a single tab.
        doReturn(mTab3).when(mTabModelSelector).getCurrentTab();
        mTabModelObserverArgumentCaptor
                .getValue()
                .didSelectTab(mTab3, TabSelectionType.FROM_USER, TAB1_ID);

        // Strip should be showing since we are selecting a group, and it should scroll to the index
        // of currently selected tab.
        verifyResetStrip(true, tabGroup);
        assertThat(mModel.get(TabGroupUiProperties.INITIAL_SCROLL_INDEX), equalTo(1));
    }

    @Test
    public void tabClosure_NotLastTabInGroup_Selection_SingleTabGroupsDisabled() {
        initAndAssertProperties(mTab2);

        doReturn(mTab3).when(mTabGroupModelFilter).getGroupLastShownTab(TAB2_ROOT_ID);
        doReturn(false).when(mTabGroupModelFilter).isTabInTabGroup(mTab2);
        doReturn(false).when(mTabGroupModelFilter).isTabInTabGroup(mTab3);

        // Mock closing tab 2, and tab 3 then gets selected. They are in the same group assume that
        // that Tab 3 is the last tab in the group.
        mTabModelObserverArgumentCaptor.getValue().willCloseTab(mTab2, true);
        verifyResetStrip(false, null);

        mTabModelObserverArgumentCaptor
                .getValue()
                .didSelectTab(mTab3, TabSelectionType.FROM_CLOSE, TAB2_ID);

        // Strip should not be reset again.
        verifyNeverReset();
    }

    @Test
    public void tabClosure_NotLastTabInGroup_Selection_SingleTabGroupsEnabled() {
        initAndAssertProperties(mTab2);

        doReturn(mTab3).when(mTabGroupModelFilter).getGroupLastShownTab(TAB2_ROOT_ID);
        doReturn(false).when(mTabGroupModelFilter).isTabInTabGroup(mTab2);
        doReturn(true).when(mTabGroupModelFilter).isTabInTabGroup(mTab3);

        // Mock closing tab 2, and tab 3 then gets selected. They are in the same group assume that
        // that Tab 3 is the last tab in the group, but tab groups of size 1 are supported.
        mTabModelObserverArgumentCaptor.getValue().willCloseTab(mTab2, true);
        mTabModelObserverArgumentCaptor
                .getValue()
                .didSelectTab(mTab3, TabSelectionType.FROM_CLOSE, TAB2_ID);

        // Strip should not be reset since we are still in this group.
        verifyNeverReset();
    }

    @Test
    public void tabClosure_NotLastTabInGroup_NoSelection_SingleTabGroupsDisabled() {
        initAndAssertProperties(mTab2);

        doReturn(mTab2).when(mTabGroupModelFilter).getGroupLastShownTab(TAB3_ROOT_ID);
        doReturn(false).when(mTabGroupModelFilter).isTabInTabGroup(mTab2);
        doReturn(false).when(mTabGroupModelFilter).isTabInTabGroup(mTab3);

        // Mock closing tab 3, and tab 2 remains selected.
        mTabModelObserverArgumentCaptor.getValue().willCloseTab(mTab3, true);

        // Strip should reset since since we don't have a group anymore.
        verifyResetStrip(false, null);
    }

    @Test
    public void tabClosure_NotLastTabInGroup_NoSelection_SingleTabGroupsEnabled() {
        initAndAssertProperties(mTab2);

        doReturn(mTab2).when(mTabGroupModelFilter).getGroupLastShownTab(TAB3_ROOT_ID);
        doReturn(true).when(mTabGroupModelFilter).isTabInTabGroup(mTab2);
        doReturn(false).when(mTabGroupModelFilter).isTabInTabGroup(mTab3);

        // Mock closing tab 3, and tab 2 remains selected.
        mTabModelObserverArgumentCaptor.getValue().willCloseTab(mTab3, true);

        // Strip should not be reset since we are still in this group.
        verifyNeverReset();
    }

    @Test
    public void tabClosure_LastTabInGroup_GroupUiNotVisible() {
        initAndAssertProperties(mTab1);

        // Mock closing tab 1, and tab 2 then gets selected. They are in different group.
        mTabModelObserverArgumentCaptor.getValue().willCloseTab(mTab1, true);
        mTabModelObserverArgumentCaptor
                .getValue()
                .didSelectTab(mTab2, TabSelectionType.FROM_CLOSE, TAB1_ID);

        // Strip should be reset since we are switching to a different group.
        verifyResetStrip(true, mTabGroup2);
    }

    // TODO(crbug.com/40637854): Ignore this test until we have a conclusion from the attached bug.
    @Ignore
    @Test
    public void tabClosure_LastTabInGroup_GroupUiVisible() {
        initAndAssertProperties(mTab2);

        // Mock closing tab 2 and tab, then tab 1 gets selected. They are in different group. Right
        // now tab group UI is visible.
        mTabModelObserverArgumentCaptor.getValue().willCloseTab(mTab2, true);
        mTabModelObserverArgumentCaptor
                .getValue()
                .didSelectTab(mTab3, TabSelectionType.FROM_CLOSE, TAB2_ID);
        doReturn(new ArrayList<>()).when(mTabGroupModelFilter).getRelatedTabList(TAB3_ID);
        mTabModelObserverArgumentCaptor.getValue().willCloseTab(mTab3, true);
        mTabModelObserverArgumentCaptor
                .getValue()
                .didSelectTab(mTab1, TabSelectionType.FROM_CLOSE, TAB3_ID);

        // Strip should be reset since tab group UI was visible and now we are switching to a
        // different group.
        verifyResetStrip(false, null);
    }

    @Test
    public void tabAddition_SingleTab() {
        initAndAssertProperties(mTab1);

        Tab newTab = prepareTab(TAB4_ID, TAB4_ID);
        List<Tab> tabs = new ArrayList<>(Arrays.asList(newTab));
        doReturn(tabs).when(mTabGroupModelFilter).getRelatedTabList(TAB4_ID);

        mTabModelObserverArgumentCaptor
                .getValue()
                .didAddTab(
                        newTab,
                        TabLaunchType.FROM_CHROME_UI,
                        TabCreationState.LIVE_IN_FOREGROUND,
                        false);
        mTabModelObserverArgumentCaptor
                .getValue()
                .didAddTab(
                        newTab,
                        TabLaunchType.FROM_RESTORE,
                        TabCreationState.FROZEN_ON_RESTORE,
                        false);

        // Strip should be not be reset when adding a single new tab.
        verifyNeverReset();
    }

    @Test
    public void tabAddition_SingleTab_Refresh_WithoutAutoGroupCreation() {
        initAndAssertProperties(mTab1);

        Tab newTab = prepareTab(TAB4_ID, TAB4_ID);
        List<Tab> tabs = new ArrayList<>(Arrays.asList(mTab1, newTab));
        doReturn(tabs).when(mTabGroupModelFilter).getRelatedTabList(TAB4_ID);
        doReturn(true).when(mTabGroupModelFilter).isTabInTabGroup(mTab1);
        doReturn(true).when(mTabGroupModelFilter).isTabInTabGroup(newTab);

        mTabModelObserverArgumentCaptor
                .getValue()
                .didAddTab(
                        newTab,
                        TabLaunchType.FROM_LONGPRESS_BACKGROUND_IN_GROUP,
                        TabCreationState.LIVE_IN_FOREGROUND,
                        false);

        // Strip should be be reset when long pressing a link and add a tab into group.
        verifyResetStrip(true, tabs);
    }

    @Test
    public void tabAddition_TabGroup_NoRefresh() {
        initAndAssertProperties(mTab2);

        Tab newTab = prepareTab(TAB4_ID, TAB4_ID);
        mTabGroup2.add(newTab);
        doReturn(mTabGroup1).when(mTabGroupModelFilter).getRelatedTabList(TAB4_ID);

        mTabModelObserverArgumentCaptor
                .getValue()
                .didAddTab(
                        newTab,
                        TabLaunchType.FROM_CHROME_UI,
                        TabCreationState.LIVE_IN_FOREGROUND,
                        false);
        mTabModelObserverArgumentCaptor
                .getValue()
                .didAddTab(
                        newTab,
                        TabLaunchType.FROM_RESTORE,
                        TabCreationState.FROZEN_ON_RESTORE,
                        false);
        mTabModelObserverArgumentCaptor
                .getValue()
                .didAddTab(
                        newTab,
                        TabLaunchType.FROM_LONGPRESS_BACKGROUND,
                        TabCreationState.LIVE_IN_FOREGROUND,
                        false);

        // Strip should be not be reset through these two types of launching.
        verifyNeverReset();
    }

    @Test
    public void tabAddition_TabGroup_ScrollToTheLast() {
        initAndAssertProperties(mTab2);
        assertThat(mModel.get(TabGroupUiProperties.INITIAL_SCROLL_INDEX), equalTo(0));

        Tab newTab = prepareTab(TAB4_ID, TAB4_ID);
        mTabGroup2.add(newTab);
        doReturn(mTabGroup2).when(mTabGroupModelFilter).getRelatedTabList(TAB4_ID);

        mTabModelObserverArgumentCaptor
                .getValue()
                .didAddTab(
                        newTab,
                        TabLaunchType.FROM_TAB_GROUP_UI,
                        TabCreationState.LIVE_IN_FOREGROUND,
                        false);

        // Strip should be not be reset through adding tab from UI.
        verifyNeverReset();
        assertThat(mTabGroupModelFilter.getRelatedTabList(TAB4_ID).size(), equalTo(3));
        assertThat(mModel.get(TabGroupUiProperties.INITIAL_SCROLL_INDEX), equalTo(2));
    }

    @Test
    public void restoreCompleted_TabModelNoTab() {
        // Simulate no tab in current TabModel.
        initAndAssertProperties(null);

        // Simulate restore finished.
        mTabModelObserverArgumentCaptor.getValue().restoreCompleted();

        mVisibilityControllerInOrder
                .verify(mVisibilityController, never())
                .setBottomControlsVisible(anyBoolean());
    }

    @Test
    public void restoreCompleted_UiNotVisible() {
        // Assume mTab1 is selected, and it does not have related tabs.
        initAndAssertProperties(mTab1);
        doReturn(POSITION1).when(mTabModel).index();
        doReturn(mTab1).when(mTabModelSelector).getCurrentTab();
        // Simulate restore finished.
        mTabModelObserverArgumentCaptor.getValue().restoreCompleted();

        mVisibilityControllerInOrder.verify(mVisibilityController).setBottomControlsVisible(false);
    }

    @Test
    public void restoreCompleted_UiVisible() {
        // Assume mTab2 is selected, and it has related tabs mTab2 and mTab3.
        initAndAssertProperties(mTab2);
        doReturn(POSITION2).when(mTabModel).index();
        doReturn(mTab2).when(mTabModelSelector).getCurrentTab();
        // Simulate restore finished.
        mTabModelObserverArgumentCaptor.getValue().restoreCompleted();

        mVisibilityControllerInOrder.verify(mVisibilityController).setBottomControlsVisible(true);
    }

    @Test
    public void restoreCompleted_OverviewModeVisible() {
        // Assume mTab2 is selected, and it has related tabs mTab2 and mTab3. Also, the overview
        // mode is visible when restoring completed.
        initAndAssertProperties(mTab2);
        doReturn(POSITION2).when(mTabModel).index();
        doReturn(mTab2).when(mTabModelSelector).getCurrentTab();
        doReturn(true).when(mLayoutManager).isLayoutVisible(LayoutType.TAB_SWITCHER);
        // Simulate restore finished.
        mTabModelObserverArgumentCaptor.getValue().restoreCompleted();

        mVisibilityControllerInOrder
                .verify(mVisibilityController, never())
                .setBottomControlsVisible(anyBoolean());
    }

    @Test
    public void tabClosureUndone_UiVisible_NotShowingOverviewMode() {
        // Assume mTab2 is selected, and it has related tabs mTab2 and mTab3.
        initAndAssertProperties(mTab2);
        // OverviewMode is hiding by default.
        assertThat(mTabGroupUiMediator.getIsShowingOverViewModeForTesting(), equalTo(false));

        // Simulate that another member of this group, newTab, is being undone from closure.
        Tab newTab = prepareTab(TAB4_ID, TAB4_ID);
        doReturn(new ArrayList<>(Arrays.asList(mTab2, mTab3, newTab)))
                .when(mTabGroupModelFilter)
                .getRelatedTabList(TAB4_ID);

        mTabModelObserverArgumentCaptor.getValue().tabClosureUndone(newTab);

        // Since the strip is already visible, no resetting.
        mVisibilityControllerInOrder
                .verify(mVisibilityController, never())
                .setBottomControlsVisible(anyBoolean());
    }

    @Test
    public void tabClosureUndone_UiNotVisible_NotShowingOverviewMode_TabNotInGroup() {
        // Assume mTab1 is selected. Since mTab1 is now a single tab, the strip is invisible.
        initAndAssertProperties(mTab1);
        // OverviewMode is hiding by default.
        assertThat(mTabGroupUiMediator.getIsShowingOverViewModeForTesting(), equalTo(false));

        // Simulate mTab2 and mTab3 being undone from closure with mTab1 still selected.
        doReturn(new ArrayList<>(Arrays.asList(mTab2, mTab3)))
                .when(mTabGroupModelFilter)
                .getRelatedTabList(TAB2_ID);
        doReturn(new ArrayList<>(Arrays.asList(mTab2, mTab3)))
                .when(mTabGroupModelFilter)
                .getRelatedTabList(TAB3_ID);
        doReturn(new ArrayList<>(Arrays.asList(mTab1)))
                .when(mTabGroupModelFilter)
                .getRelatedTabList(TAB1_ID);
        doReturn(mTab1).when(mTabModelSelector).getCurrentTab();

        mTabModelObserverArgumentCaptor.getValue().tabClosureUndone(mTab2);
        mTabModelObserverArgumentCaptor.getValue().tabClosureUndone(mTab3);

        // Strip should remain invisible.
        assertThat(mTabGroupUiMediator.getIsShowingOverViewModeForTesting(), equalTo(false));
    }

    @Test
    public void tabClosureUndone_UiNotVisible_NotShowingOverviewMode_TabInGroup() {
        // Assume mTab1 is selected. Since mTab1 is now a single tab, the strip is invisible.
        initAndAssertProperties(mTab1);
        // OverviewMode is hiding by default.
        assertThat(mTabGroupUiMediator.getIsShowingOverViewModeForTesting(), equalTo(false));

        // Simulate that newTab which was a tab in the same group as mTab1 is being undone from
        // closure.
        Tab newTab = prepareTab(TAB4_ID, TAB4_ID);
        List<Tab> tabs = new ArrayList<>(Arrays.asList(mTab1, newTab));
        doReturn(tabs).when(mTabGroupModelFilter).getRelatedTabList(TAB1_ID);
        doReturn(tabs).when(mTabGroupModelFilter).getRelatedTabList(TAB4_ID);
        doReturn(true).when(mTabGroupModelFilter).isTabInTabGroup(mTab1);
        doReturn(true).when(mTabGroupModelFilter).isTabInTabGroup(newTab);
        doReturn(mTab1).when(mTabModelSelector).getCurrentTab();

        mTabModelObserverArgumentCaptor.getValue().tabClosureUndone(newTab);

        // Strip should reset to be visible.
        mVisibilityControllerInOrder
                .verify(mVisibilityController)
                .setBottomControlsVisible(eq(true));
    }

    @Test
    public void tabClosureUndone_UiNotVisible_ShowingTabSwitcherMode() {
        // Assume mTab1 is selected.
        initAndAssertProperties(mTab1);
        // OverviewMode is hiding by default.
        assertThat(mTabGroupUiMediator.getIsShowingOverViewModeForTesting(), equalTo(false));

        // Simulate the overview mode is showing, which hides the strip.
        mLayoutStateObserverCaptor.getValue().onStartedShowing(LayoutType.TAB_SWITCHER);
        assertThat(mTabGroupUiMediator.getIsShowingOverViewModeForTesting(), equalTo(true));
        mVisibilityControllerInOrder.verify(mVisibilityController).setBottomControlsVisible(false);

        // Simulate that we undo a group closure of {mTab2, mTab3}.
        mTabModelObserverArgumentCaptor.getValue().tabClosureUndone(mTab3);
        mTabModelObserverArgumentCaptor.getValue().tabClosureUndone(mTab2);

        // Since overview mode is showing, we should not show strip.
        mVisibilityControllerInOrder
                .verify(mVisibilityController, times(2))
                .setBottomControlsVisible(eq(false));
    }

    @Test
    public void overViewStartedShowing() {
        initAndAssertProperties(mTab1);

        mLayoutStateObserverCaptor.getValue().onStartedShowing(LayoutType.TAB_SWITCHER);

        verifyResetStrip(false, null);
    }

    @Test
    public void overViewFinishedHiding_NoCurrentTab() {
        initAndAssertProperties(null);

        mLayoutStateObserverCaptor.getValue().onFinishedHiding(LayoutType.TAB_SWITCHER);

        verifyNeverReset();
    }

    @Test
    public void overViewFinishedHiding_CurrentTabSingle() {
        initAndAssertProperties(mTab1);

        mLayoutStateObserverCaptor.getValue().onFinishedHiding(LayoutType.TAB_SWITCHER);

        verifyResetStrip(false, null);
    }

    @Test
    public void overViewFinishedHiding_CurrentTabInGroup() {
        initAndAssertProperties(mTab2);

        mLayoutStateObserverCaptor.getValue().onFinishedHiding(LayoutType.TAB_SWITCHER);

        verifyResetStrip(true, mTabGroup2);
    }

    @Test
    public void destroy_TabGroup() {
        initAndAssertProperties(mTab1);

        mTabGroupUiMediator.destroy();

        verify(mTabModelFilterProvider)
                .removeTabModelFilterObserver(mTabModelObserverArgumentCaptor.capture());
        verify(mLayoutManager).removeObserver(mLayoutStateObserverCaptor.capture());
        verify(mIncognitoStateProvider)
                .removeObserver(mIncognitoStateObserverArgumentCaptor.capture());
        verify(mTabModelSupplier).removeObserver(mTabModelSupplierObserverCaptor.capture());
        verify(mTabGroupModelFilter, times(2))
                .removeTabGroupObserver(mTabGroupModelFilterObserverArgumentCaptor.capture());
    }

    @Test
    public void uiNotVisibleAfterDragCurrentTabOutOfGroup() {
        initAndAssertProperties(mTab3);

        List<Tab> tabs = new ArrayList<>(Arrays.asList(mTab3));
        doReturn(tabs).when(mTabGroupModelFilter).getRelatedTabList(TAB3_ID);
        doReturn(false).when(mTabGroupModelFilter).isTabInTabGroup(mTab3);
        mTabGroupModelFilterObserverArgumentCaptor.getValue().didMoveTabOutOfGroup(mTab3, 1);

        verifyResetStrip(false, null);
    }

    @Test
    public void uiVisibleAfterDragCurrentTabOutOfGroup_GroupSize1() {
        initAndAssertProperties(mTab3);

        List<Tab> tabs = new ArrayList<>(Arrays.asList(mTab3));
        doReturn(tabs).when(mTabGroupModelFilter).getRelatedTabList(TAB3_ID);
        doReturn(true).when(mTabGroupModelFilter).isTabInTabGroup(mTab3);
        mTabGroupModelFilterObserverArgumentCaptor.getValue().didMoveTabOutOfGroup(mTab3, 1);

        verifyResetStrip(true, tabs);
    }

    @Test
    public void backButtonPress_ShouldHandle() {
        initAndAssertProperties(mTab1);
        mDialogControllerSupplier.get();
        doReturn(true).when(mTabGridDialogController).handleBackPressed();
        mTabGridDialogBackPressSupplier.set(true);

        var groupUiBackPressSupplier = mTabGroupUiMediator.getHandleBackPressChangedSupplier();
        Assert.assertEquals(Boolean.TRUE, groupUiBackPressSupplier.get());

        assertThat(mTabGroupUiMediator.onBackPressed(), equalTo(true));
        verify(mTabGridDialogController).handleBackPressed();
    }

    @Test
    public void backButtonPress_ShouldNotHandle() {
        initAndAssertProperties(mTab1);
        mDialogControllerSupplier.get();
        doReturn(false).when(mTabGridDialogController).handleBackPressed();
        mTabGridDialogBackPressSupplier.set(false);
        var groupUiBackPressSupplier = mTabGroupUiMediator.getHandleBackPressChangedSupplier();

        Assert.assertNotEquals(Boolean.TRUE, groupUiBackPressSupplier.get());
        assertThat(mTabGroupUiMediator.onBackPressed(), equalTo(false));
        verify(mTabGridDialogController).handleBackPressed();
    }

    @Test
    public void backButtonPress_LateInitController() {
        initAndAssertProperties(mTab1);

        var groupUiBackPressSupplier = mTabGroupUiMediator.getHandleBackPressChangedSupplier();

        // Not initialized yet.
        Assert.assertNotEquals(Boolean.TRUE, groupUiBackPressSupplier.get());

        // Late init.
        mDialogControllerSupplier.get();
        doReturn(false).when(mTabGridDialogController).handleBackPressed();
        mTabGridDialogBackPressSupplier.set(false);

        Assert.assertFalse(groupUiBackPressSupplier.get());

        mTabGridDialogBackPressSupplier.set(true);
        doReturn(true).when(mTabGridDialogController).handleBackPressed();
        Assert.assertTrue(groupUiBackPressSupplier.get());
    }

    @Test
    public void switchTabModel_UiVisible_TabGroup() {
        initAndAssertProperties(mTab1);
        assertThat(mTabGroupUiMediator.getIsShowingOverViewModeForTesting(), equalTo(false));
        TabModel incognitoTabModel = prepareIncognitoTabModel();

        // Mock that tab2 is selected after tab model switch, and tab2 is in a group.
        doReturn(TAB2_ID).when(mTabModelSelector).getCurrentTabId();
        mTabModelSupplierObserverCaptor.getValue().onResult(mTabModel);

        verifyResetStrip(true, mTabGroup2);
    }

    @Test
    public void switchTabModel_UiNotVisible_TabGroup() {
        initAndAssertProperties(mTab1);
        assertThat(mTabGroupUiMediator.getIsShowingOverViewModeForTesting(), equalTo(false));
        TabModel incognitoTabModel = prepareIncognitoTabModel();

        // Mock that tab1 is selected after tab model switch, and tab1 is a single tab.
        doReturn(TAB1_ID).when(mTabModelSelector).getCurrentTabId();
        mTabModelSupplierObserverCaptor.getValue().onResult(mTabModel);

        verifyResetStrip(false, null);
    }

    /*********************** Class common tests *************************/

    @Test
    public void incognitoChange() {
        initAndAssertProperties(mTab1);
        mModel.set(TabGroupUiProperties.IS_INCOGNITO, false);

        mIncognitoStateObserverArgumentCaptor.getValue().onIncognitoStateChanged(true);

        assertThat(mModel.get(TabGroupUiProperties.IS_INCOGNITO), equalTo(true));
        assertThat(
                mModel.get(TabGroupUiProperties.BACKGROUND_COLOR),
                equalTo(INCOGNITO_BACKGROUND_COLOR));
        mVisibilityControllerInOrder
                .verify(mVisibilityController)
                .setBottomControlsColor(INCOGNITO_BACKGROUND_COLOR);

        mIncognitoStateObserverArgumentCaptor.getValue().onIncognitoStateChanged(false);

        assertThat(mModel.get(TabGroupUiProperties.IS_INCOGNITO), equalTo(false));
        assertThat(
                mModel.get(TabGroupUiProperties.BACKGROUND_COLOR),
                equalTo(PRIMARY_BACKGROUND_COLOR));
        mVisibilityControllerInOrder
                .verify(mVisibilityController)
                .setBottomControlsColor(PRIMARY_BACKGROUND_COLOR);
    }

    @Test
    public void testSetShowGroupDialogButtonOnClickListener() {
        initAndAssertProperties(mTab3);
        View.OnClickListener listener = v -> {};

        mModel.set(TabGroupUiProperties.SHOW_GROUP_DIALOG_BUTTON_ON_CLICK_LISTENER, null);

        mModel.set(TabGroupUiProperties.SHOW_GROUP_DIALOG_BUTTON_ON_CLICK_LISTENER, listener);

        assertThat(
                mModel.get(TabGroupUiProperties.SHOW_GROUP_DIALOG_BUTTON_ON_CLICK_LISTENER),
                equalTo(listener));
    }

    @Test
    public void testTabModelSelectorTabObserverDestroyWhenDetach() {
        InOrder tabObserverDestroyInOrder = inOrder(mTab1);
        initAndAssertProperties(mTab1);

        mTabObserverCaptor.getValue().onActivityAttachmentChanged(mTab1, null);

        tabObserverDestroyInOrder.verify(mTab1).removeObserver(mTabObserverCaptor.capture());

        mTabGroupUiMediator.destroy();

        tabObserverDestroyInOrder
                .verify(mTab1, never())
                .removeObserver(mTabObserverCaptor.capture());
    }

    @Test
    public void testOmniboxFocusChange() {
        initAndAssertProperties(mTab2);

        mOmniboxFocusObserverCaptor.getValue().onResult(true);
        verifyResetStrip(false, null);

        doReturn(TAB2_ID).when(mTabModelSelector).getCurrentTabId();
        mOmniboxFocusObserverCaptor.getValue().onResult(false);
        verifyResetStrip(true, mTabGroup2);
    }
}