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

// Copyright 2024 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeastOnce;
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 static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.ALL_KEYS;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.BLOCK_TOUCH_INPUT;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.BROWSER_CONTROLS_STATE_PROVIDER;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.FOCUS_TAB_INDEX_FOR_ACCESSIBILITY;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.INITIAL_SCROLL_INDEX;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.MODE;

import android.view.View;
import android.view.ViewGroup;

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.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.shadows.ShadowLooper;

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.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.MockTab;
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.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tasks.tab_management.TabGridDialogMediator.DialogController;
import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode;
import org.chromium.chrome.browser.tasks.tab_management.TabListEditorCoordinator.TabListEditorController;
import org.chromium.chrome.browser.tasks.tab_management.TabListMediator.TabActionListener;
import org.chromium.chrome.browser.tasks.tab_management.TabSwitcherPaneMediator.TabIndexLookup;
import org.chromium.chrome.test.util.browser.tabmodel.MockTabModel;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler.BackPressResult;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.List;

/** Unit tests for {@link TabSwitcherPaneMediator}. */
@RunWith(BaseRobolectricTestRunner.class)
public class TabSwitcherPaneMediatorUnitTest {
    private static final int UNGROUPED_TAB_ID = 1;
    private static final int GROUPED_TAB_1_ID = 2;
    private static final int GROUPED_TAB_2_ID = 3;

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

    @Mock private TabSwitcherResetHandler mResetHandler;
    @Mock private DialogController mTabGridDialogController;
    @Mock private Runnable mOnTabSwitcherShownRunnable;
    @Mock private Profile mProfile;
    @Mock private TabModelFilter mTabModelFilter;
    @Mock private TabListEditorController mTabListEditorController;
    @Mock private ViewGroup mContainerView;
    @Mock private View mCustomView;
    @Mock private Runnable mCustomViewBackPressRunnable;
    @Mock private Callback<Integer> mOnTabClickedCallback;
    @Mock private TabIndexLookup mTabIndexLookup;

    @Captor private ArgumentCaptor<TabModelObserver> mTabModelObserverCaptor;

    private final ObservableSupplierImpl<TabModelFilter> mTabModelFilterSupplier =
            new ObservableSupplierImpl<>();
    private final ObservableSupplierImpl<Boolean> mDialogBackPressChangedSupplier =
            new ObservableSupplierImpl<>();
    private final ObservableSupplierImpl<Boolean> mShowingOrAnimationSupplier =
            new ObservableSupplierImpl<>(false);
    private final ObservableSupplierImpl<Boolean> mIsVisibleSupplier =
            new ObservableSupplierImpl<>();
    private final ObservableSupplierImpl<Boolean> mIsAnimatingSupplier =
            new ObservableSupplierImpl<>(false);
    private final ObservableSupplierImpl<TabListEditorController> mTabListEditorControllerSupplier =
            new ObservableSupplierImpl<>();
    private final ObservableSupplierImpl<Boolean> mTabListEditorBackPressChangedSupplier =
            new ObservableSupplierImpl<>();

    private LazyOneshotSupplier<DialogController> mTabGridDialogControllerSupplier;
    private PropertyModel mModel;
    private MockTabModel mTabModel;
    private Tab mUngroupedTab;
    private Tab mGroupedTab1;
    private Tab mGroupedTab2;
    private TabSwitcherPaneMediator mMediator;

    @Before
    public void setUp() {
        when(mProfile.isOffTheRecord()).thenReturn(false);
        when(mTabIndexLookup.getNthTabIndexInModel(anyInt())).thenAnswer(i -> i.getArguments()[0]);
        mTabModel = new MockTabModel(mProfile, null);
        mTabModel.addTab(
                new MockTab(UNGROUPED_TAB_ID, mProfile, TabLaunchType.FROM_CHROME_UI),
                /* index= */ 0,
                TabLaunchType.FROM_CHROME_UI,
                TabCreationState.LIVE_IN_FOREGROUND);
        mTabModel.addTab(GROUPED_TAB_1_ID);
        mTabModel.addTab(GROUPED_TAB_2_ID);
        mUngroupedTab = mTabModel.getTabAt(0);
        mGroupedTab1 = mTabModel.getTabAt(1);
        mGroupedTab2 = mTabModel.getTabAt(2);
        mUngroupedTab.setRootId(UNGROUPED_TAB_ID);
        mGroupedTab1.setRootId(GROUPED_TAB_1_ID);
        mGroupedTab2.setRootId(GROUPED_TAB_1_ID);
        when(mTabModelFilter.getRelatedTabList(UNGROUPED_TAB_ID))
                .thenReturn(List.of(mUngroupedTab));
        when(mTabModelFilter.getRelatedTabList(GROUPED_TAB_1_ID))
                .thenReturn(List.of(mGroupedTab1, mGroupedTab2));
        when(mTabModelFilter.isTabInTabGroup(mUngroupedTab)).thenReturn(false);
        when(mTabModelFilter.isTabInTabGroup(mGroupedTab1)).thenReturn(true);
        when(mTabModelFilter.isTabInTabGroup(mGroupedTab2)).thenReturn(true);
        when(mTabModelFilter.getTabModel()).thenReturn(mTabModel);
        when(mTabModelFilter.indexOf(mUngroupedTab)).thenReturn(0);
        when(mTabModelFilter.indexOf(mGroupedTab1)).thenReturn(1);
        when(mTabModelFilter.indexOf(mGroupedTab2)).thenReturn(2);

        when(mTabGridDialogController.getHandleBackPressChangedSupplier())
                .thenReturn(mDialogBackPressChangedSupplier);
        when(mTabGridDialogController.getShowingOrAnimationSupplier())
                .thenReturn(mShowingOrAnimationSupplier);
        when(mTabListEditorController.getHandleBackPressChangedSupplier())
                .thenReturn(mTabListEditorBackPressChangedSupplier);

        when(mTabGridDialogController.isVisible()).thenReturn(false);
        when(mTabListEditorController.isVisible()).thenReturn(false);

        mModel =
                new PropertyModel.Builder(ALL_KEYS)
                        .with(BROWSER_CONTROLS_STATE_PROVIDER, null)
                        .with(MODE, TabListMode.GRID)
                        .build();
        mTabGridDialogControllerSupplier = LazyOneshotSupplier.fromValue(mTabGridDialogController);
        mMediator =
                new TabSwitcherPaneMediator(
                        mResetHandler,
                        mTabModelFilterSupplier,
                        mTabGridDialogControllerSupplier,
                        mModel,
                        mContainerView,
                        mOnTabSwitcherShownRunnable,
                        mIsVisibleSupplier,
                        mIsAnimatingSupplier,
                        mOnTabClickedCallback,
                        mTabIndexLookup);

        assertTrue(mTabModelFilterSupplier.hasObservers());
        assertTrue(mIsVisibleSupplier.hasObservers());
        assertTrue(mIsAnimatingSupplier.hasObservers());

        mTabModelFilterSupplier.set(mTabModelFilter);
        verify(mTabModelFilter).addObserver(mTabModelObserverCaptor.capture());

        mMediator.setTabListEditorControllerSupplier(mTabListEditorControllerSupplier);
        assertTrue(mTabListEditorControllerSupplier.hasObservers());
        mTabListEditorControllerSupplier.set(mTabListEditorController);

        mTabGridDialogControllerSupplier.get();
        ShadowLooper.runUiThreadTasks();
        assertTrue(mDialogBackPressChangedSupplier.hasObservers());

        verify(mOnTabSwitcherShownRunnable, never()).run();
        mIsVisibleSupplier.set(true);
        mIsAnimatingSupplier.set(false);
        verify(mOnTabSwitcherShownRunnable).run();
    }

    @After
    public void tearDown() {
        mMediator.destroy();

        verify(mTabModelFilter, atLeastOnce()).removeObserver(mTabModelObserverCaptor.getValue());
        verify(mTabGridDialogController, atLeastOnce()).hideDialog(false);

        assertFalse(mTabModelFilterSupplier.hasObservers());
        assertFalse(mIsVisibleSupplier.hasObservers());
        assertFalse(mIsAnimatingSupplier.hasObservers());
        assertFalse(mDialogBackPressChangedSupplier.hasObservers());
        assertFalse(mTabListEditorControllerSupplier.hasObservers());
        assertFalse(mTabListEditorBackPressChangedSupplier.hasObservers());
    }

    @Test
    @SmallTest
    public void testTabModelObserver() {
        // This observer is only used to update the back press state.
        TabModelObserver observer = mTabModelObserverCaptor.getValue();

        // Dialog visibility supplier is updated when back press internal state changes. Use this as
        // a proxy to detect that back press state updated.
        ObservableSupplier<Boolean> dialogVisibilitySupplier =
                mMediator.getIsDialogVisibleSupplier();
        assertFalse(dialogVisibilitySupplier.get());

        when(mTabListEditorController.isVisible()).thenReturn(true);
        observer.tabClosureUndone(null);
        assertTrue(dialogVisibilitySupplier.get());

        when(mTabListEditorController.isVisible()).thenReturn(false);
        observer.tabPendingClosure(null);
        assertFalse(dialogVisibilitySupplier.get());

        when(mTabListEditorController.isVisible()).thenReturn(true);
        observer.onFinishingTabClosure(null);
        assertTrue(dialogVisibilitySupplier.get());

        when(mTabListEditorController.isVisible()).thenReturn(false);
        observer.tabRemoved(null);
        assertFalse(dialogVisibilitySupplier.get());

        when(mTabListEditorController.isVisible()).thenReturn(true);
        observer.multipleTabsPendingClosure(null, false);
        assertTrue(dialogVisibilitySupplier.get());
    }

    @Test
    @SmallTest
    public void testLateTabModelFilterWhileVisible() {
        when(mTabListEditorController.isVisible()).thenReturn(true);
        // Reset to simulate the UI is shown with no tab model filter set.
        mIsVisibleSupplier.set(false);
        verify(mTabGridDialogController).hideDialog(false);
        verify(mTabListEditorController).hide();
        when(mTabListEditorController.isVisible()).thenReturn(false);
        mTabModelFilterSupplier.set(null);
        verify(mTabModelFilter, times(1)).addObserver(mTabModelObserverCaptor.capture());

        mMediator.destroy();

        mMediator =
                new TabSwitcherPaneMediator(
                        mResetHandler,
                        mTabModelFilterSupplier,
                        mTabGridDialogControllerSupplier,
                        mModel,
                        mContainerView,
                        mOnTabSwitcherShownRunnable,
                        mIsVisibleSupplier,
                        mIsAnimatingSupplier,
                        mOnTabClickedCallback,
                        mTabIndexLookup);
        ShadowLooper.runUiThreadTasks();

        mIsVisibleSupplier.set(true);

        // When the filter is set we need to show tabs when visible if the restore already finished.
        mTabModelFilterSupplier.set(mTabModelFilter);
        verify(mTabModelFilter, times(2)).addObserver(mTabModelObserverCaptor.capture());
        verify(mResetHandler).resetWithTabList(mTabModelFilter, false);
    }

    @Test
    @SmallTest
    public void testTabModelObserverOnRestore() {
        TabModelObserver observer = mTabModelObserverCaptor.getValue();

        observer.restoreCompleted();
        verify(mResetHandler).resetWithTabList(mTabModelFilter, false);
    }

    @Test
    @SmallTest
    public void testIsDialogVisibleSupplier() {
        ObservableSupplier<Boolean> dialogVisibilitySupplier =
                mMediator.getIsDialogVisibleSupplier();
        assertFalse(dialogVisibilitySupplier.get());

        when(mTabListEditorController.isVisible()).thenReturn(true);
        mTabListEditorBackPressChangedSupplier.set(true);
        assertTrue(dialogVisibilitySupplier.get());

        when(mTabListEditorController.isVisible()).thenReturn(false);
        mTabListEditorBackPressChangedSupplier.set(false);
        assertFalse(dialogVisibilitySupplier.get());

        when(mTabGridDialogController.isVisible()).thenReturn(true);
        mDialogBackPressChangedSupplier.set(true);
        assertTrue(dialogVisibilitySupplier.get());

        when(mTabGridDialogController.isVisible()).thenReturn(false);
        mDialogBackPressChangedSupplier.set(false);
        assertFalse(dialogVisibilitySupplier.get());
    }

    @Test
    @SmallTest
    public void testRequestAccessibilityFocusOnCurrentTab() {
        int index = 5;
        when(mTabModelFilter.index()).thenReturn(index);
        mMediator.requestAccessibilityFocusOnCurrentTab();

        assertEquals(index, mModel.get(FOCUS_TAB_INDEX_FOR_ACCESSIBILITY).intValue());
    }

    @Test
    @SmallTest
    @DisableFeatures(ChromeFeatureList.BACK_GESTURE_REFACTOR)
    public void testBackPress() {
        assertFalse(mMediator.getHandleBackPressChangedSupplier().get());
        assertEquals(BackPressResult.FAILURE, mMediator.handleBackPress());

        when(mTabListEditorController.isVisible()).thenReturn(true);
        when(mTabListEditorController.handleBackPressed()).thenReturn(true);
        mTabListEditorBackPressChangedSupplier.set(true);
        assertTrue(mMediator.getHandleBackPressChangedSupplier().get());
        assertEquals(BackPressResult.SUCCESS, mMediator.handleBackPress());
        verify(mTabListEditorController, times(2)).handleBackPressed();
        when(mTabListEditorController.isVisible()).thenReturn(false);
        when(mTabListEditorController.handleBackPressed()).thenReturn(false);
        mTabListEditorBackPressChangedSupplier.set(false);
        assertFalse(mMediator.getHandleBackPressChangedSupplier().get());

        mIsAnimatingSupplier.set(true);
        verify(mTabGridDialogController).hideDialog(true);
        assertTrue(mMediator.getHandleBackPressChangedSupplier().get());
        assertEquals(BackPressResult.SUCCESS, mMediator.handleBackPress());
        mIsAnimatingSupplier.set(false);
        assertFalse(mMediator.getHandleBackPressChangedSupplier().get());

        when(mTabGridDialogController.isVisible()).thenReturn(true);
        when(mTabGridDialogController.handleBackPressed()).thenReturn(true);
        mDialogBackPressChangedSupplier.set(true);
        assertTrue(mMediator.getHandleBackPressChangedSupplier().get());
        assertEquals(BackPressResult.SUCCESS, mMediator.handleBackPress());
        verify(mTabGridDialogController, times(2)).handleBackPressed();
        when(mTabGridDialogController.isVisible()).thenReturn(false);
        when(mTabGridDialogController.handleBackPressed()).thenReturn(false);
        mDialogBackPressChangedSupplier.set(false);
        assertFalse(mMediator.getHandleBackPressChangedSupplier().get());

        // Remove this section when removing the BACK_GESTURE_REFACTOR_ANDROID annotation. This is
        // here to assert in a legacy edgecase where back could be called without checking if it is
        // supported.
        mIsVisibleSupplier.set(false);
        assertFalse(mMediator.getHandleBackPressChangedSupplier().get());
        assertEquals(BackPressResult.FAILURE, mMediator.handleBackPress());
        mIsVisibleSupplier.set(true);
        assertFalse(mMediator.getHandleBackPressChangedSupplier().get());
    }

    @Test
    @SmallTest
    public void testOpenTabGridDialog() {
        TabActionListener listener = mMediator.openTabGridDialog(mGroupedTab1);
        assertNotNull(listener);
        listener.run(mCustomView, mGroupedTab1.getId());

        verify(mTabGridDialogController).resetWithListOfTabs(List.of(mGroupedTab1, mGroupedTab2));
    }

    @Test
    @SmallTest
    public void testOpenTabGridDialog_SingleTab() {
        assertNull(mMediator.openTabGridDialog(mUngroupedTab));
    }

    @Test
    @SmallTest
    public void testOpenTabGridDialog_SingleTabGroup() {
        when(mTabModelFilter.isTabInTabGroup(mUngroupedTab)).thenReturn(true);

        TabActionListener listener = mMediator.openTabGridDialog(mUngroupedTab);
        assertNotNull(listener);
        listener.run(mCustomView, mUngroupedTab.getId());
        verify(mTabGridDialogController).resetWithListOfTabs(List.of(mUngroupedTab));
    }

    @Test
    @SmallTest
    public void testOnTabSelecting() {
        mMediator.onTabSelecting(mUngroupedTab.getId(), /* fromActionButton= */ true);
        verify(mOnTabClickedCallback).onResult(UNGROUPED_TAB_ID);
    }

    @Test
    @SmallTest
    public void testScrollToTab() {
        int index = 5;
        when(mTabModelFilter.index()).thenReturn(index);

        mMediator.setInitialScrollIndexOffset();
        assertEquals(index, mModel.get(INITIAL_SCROLL_INDEX).intValue());

        index = 3;
        mMediator.scrollToTab(index);
        assertEquals(index, mModel.get(INITIAL_SCROLL_INDEX).intValue());

        mMediator.scrollToTabById(GROUPED_TAB_2_ID);
        assertEquals(1, mModel.get(INITIAL_SCROLL_INDEX).intValue());

        int overrideIndex = 7;
        when(mTabIndexLookup.getNthTabIndexInModel(anyInt())).thenReturn(overrideIndex);

        mMediator.setInitialScrollIndexOffset();
        assertEquals(overrideIndex, mModel.get(INITIAL_SCROLL_INDEX).intValue());

        mMediator.scrollToTab(index);
        assertEquals(index, mModel.get(INITIAL_SCROLL_INDEX).intValue());

        mMediator.scrollToTabById(GROUPED_TAB_2_ID);
        assertEquals(overrideIndex, mModel.get(INITIAL_SCROLL_INDEX).intValue());
    }

    @Test
    @SmallTest
    public void testCustomViewWithClearTabList() {
        when(mTabListEditorController.isVisible()).thenReturn(true);

        mMediator.addCustomView(
                mCustomView, mCustomViewBackPressRunnable, /* clearTabList= */ true);
        verify(mResetHandler).resetWithTabList(null, false);
        verify(mContainerView).addView(mCustomView);
        verify(mTabListEditorController).hide();
        when(mTabListEditorController.isVisible()).thenReturn(false);

        assertTrue(mMediator.getHandleBackPressChangedSupplier().get());
        assertEquals(BackPressResult.SUCCESS, mMediator.handleBackPress());
        verify(mCustomViewBackPressRunnable).run();

        mMediator.removeCustomView(mCustomView);
        verify(mContainerView).removeView(mCustomView);
        assertFalse(mMediator.getHandleBackPressChangedSupplier().get());
    }

    @Test
    @SmallTest
    public void testCustomViewWithoutClearTabList() {
        when(mTabGridDialogController.isVisible()).thenReturn(true);

        mMediator.addCustomView(
                mCustomView, mCustomViewBackPressRunnable, /* clearTabList= */ false);
        verify(mResetHandler, never()).resetWithTabList(null, false);
        verify(mContainerView).addView(mCustomView);
        verify(mTabGridDialogController).hideDialog(false);
        when(mTabGridDialogController.isVisible()).thenReturn(false);

        assertTrue(mMediator.getHandleBackPressChangedSupplier().get());
        assertEquals(BackPressResult.SUCCESS, mMediator.handleBackPress());
        verify(mCustomViewBackPressRunnable).run();

        mMediator.removeCustomView(mCustomView);
        verify(mContainerView).removeView(mCustomView);
        assertFalse(mMediator.getHandleBackPressChangedSupplier().get());
    }

    @Test
    @SmallTest
    public void testBlockTouchInput() {
        assertFalse(mModel.get(BLOCK_TOUCH_INPUT));
        mShowingOrAnimationSupplier.set(true);
        assertTrue(mModel.get(BLOCK_TOUCH_INPUT));
        mShowingOrAnimationSupplier.set(false);
        assertFalse(mModel.get(BLOCK_TOUCH_INPUT));
    }
}