chromium/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPaneCoordinatorUnitTest.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.Mockito.any;
import static org.mockito.Mockito.eq;
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.FOCUS_TAB_INDEX_FOR_ACCESSIBILITY;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.INITIAL_SCROLL_INDEX;
import static org.chromium.ui.test.util.MockitoHelper.doCallback;

import android.app.Activity;
import android.graphics.Bitmap;
import android.view.ViewStub;
import android.widget.FrameLayout;

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

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.base.supplier.Supplier;
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.base.test.util.JniMocker;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.data_sharing.DataSharingServiceFactory;
import org.chromium.chrome.browser.data_sharing.DataSharingTabManager;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.price_tracking.PriceTrackingFeatures;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.tab.MockTab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab_group_sync.TabGroupSyncFeatures;
import org.chromium.chrome.browser.tab_group_sync.TabGroupSyncFeaturesJni;
import org.chromium.chrome.browser.tab_group_sync.TabGroupSyncServiceFactory;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tab_ui.TabSwitcherCustomViewManager;
import org.chromium.chrome.browser.tab_ui.TabThumbnailView;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
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.ui.favicon.FaviconHelper;
import org.chromium.chrome.browser.ui.favicon.FaviconHelperJni;
import org.chromium.chrome.tab_ui.R;
import org.chromium.chrome.test.util.browser.tabmodel.MockTabModel;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler.BackPressResult;
import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
import org.chromium.components.data_sharing.DataSharingService;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.components.tab_group_sync.TabGroupSyncService;
import org.chromium.ui.base.TestActivity;
import org.chromium.ui.modaldialog.ModalDialogManager;

import java.util.Collections;

/**
 * Unit tests for {@link TabSwitcherPaneCoordinator}. These are mostly for coverage and to confirm
 * nothing will crash since the bulk of the behaviors from the coordinator are either unit tested by
 * classes hosted insider the coordinator or have to be verified in an integration test.
 */
@RunWith(BaseRobolectricTestRunner.class)
public class TabSwitcherPaneCoordinatorUnitTest {
    @Rule public JniMocker mJniMocker = new JniMocker();

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

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

    @Mock private ProfileProvider mProfileProvider;
    @Mock private Profile mProfile;
    @Mock private TabGroupSyncFeatures.Natives mTabGroupSyncFeaturesJniMock;
    @Mock private TabGroupModelFilter mTabModelFilter;
    @Mock private TabContentManager mTabContentManager;
    @Mock private TabCreatorManager mTabCreatorManager;
    @Mock private BrowserControlsStateProvider mBrowserControlsStateProvider;
    @Mock private ScrimCoordinator mScrimCoordinator;
    @Mock private ModalDialogManager mModalDialogManager;
    @Mock private TabSwitcherMessageManager mMessageManager;
    @Mock private TabSwitcherResetHandler mResetHandler;
    @Mock private Callback<Integer> mOnTabClickedCallback;
    @Mock private Callback<Boolean> mHairlineVisibilityCallback;
    @Mock private FaviconHelper.Natives mFaviconHelperJniMock;
    @Mock private Tracker mTracker;
    @Mock private BottomSheetController mBottomSheetController;
    @Mock private DataSharingTabManager mDataSharingTabManager;
    @Mock private IdentityServicesProvider mIdentityServicesProvider;
    @Mock private IdentityManager mIdentityManager;
    @Mock private TabGroupSyncService mTabGroupSyncService;
    @Mock private DataSharingService mDataSharingService;

    private final OneshotSupplierImpl<ProfileProvider> mProfileProviderSupplier =
            new OneshotSupplierImpl<>();
    private final ObservableSupplierImpl<TabModelFilter> mTabModelFilterSupplier =
            new ObservableSupplierImpl<>();
    private final ObservableSupplierImpl<Boolean> mIsVisibleSupplier =
            new ObservableSupplierImpl<>();
    private final ObservableSupplierImpl<Boolean> mIsAnimatingSupplier =
            new ObservableSupplierImpl<>();

    private MockTabModel mTabModel;
    private Activity mActivity;
    private FrameLayout mRootView;
    private FrameLayout mContainerView;
    private FrameLayout mCoordinatorView;
    private TabSwitcherPaneCoordinator mCoordinator;
    private boolean mDestroyed;

    @Before
    public void setUp() {
        when(mFaviconHelperJniMock.init()).thenReturn(1L);
        mJniMocker.mock(FaviconHelperJni.TEST_HOOKS, mFaviconHelperJniMock);

        mJniMocker.mock(TabGroupSyncFeaturesJni.TEST_HOOKS, mTabGroupSyncFeaturesJniMock);
        when(mTabGroupSyncFeaturesJniMock.isTabGroupSyncEnabled(mProfile)).thenReturn(true);
        TabGroupSyncServiceFactory.setForTesting(mTabGroupSyncService);

        TrackerFactory.setTrackerForTests(mTracker);

        when(mProfile.isNativeInitialized()).thenReturn(true);
        when(mProfile.isOffTheRecord()).thenReturn(false);
        when(mProfileProvider.getOriginalProfile()).thenReturn(mProfile);
        when(mProfile.getOriginalProfile()).thenReturn(mProfile);

        PriceTrackingFeatures.setPriceTrackingEnabledForTesting(true);
        PriceTrackingFeatures.setIsSignedInAndSyncEnabledForTesting(true);

        mTabModel = new MockTabModel(mProfile, null);
        when(mTabModelFilter.getTabModel()).thenReturn(mTabModel);
        when(mTabModelFilter.isTabModelRestored()).thenReturn(true);

        mProfileProviderSupplier.set(mProfileProvider);
        mTabModelFilterSupplier.set(mTabModelFilter);
        mIsVisibleSupplier.set(false);
        mIsAnimatingSupplier.set(false);

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

    private void onActivityCreated(Activity activity) {
        mActivity = activity;
        mRootView = new FrameLayout(activity);
        mCoordinatorView = new FrameLayout(activity);
        mContainerView = new FrameLayout(activity);
        mRootView.addView(mContainerView);
        mCoordinatorView.setId(R.id.coordinator);
        mRootView.addView(mCoordinatorView);
        activity.setContentView(mRootView);

        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.TabSwitcher.SetupRecyclerView.Time");
        mDestroyed = false;
        mCoordinator =
                new TabSwitcherPaneCoordinator(
                        activity,
                        mProfileProviderSupplier,
                        mTabModelFilterSupplier,
                        mTabContentManager,
                        mTabCreatorManager,
                        mBrowserControlsStateProvider,
                        mScrimCoordinator,
                        mModalDialogManager,
                        mBottomSheetController,
                        mDataSharingTabManager,
                        mMessageManager,
                        mContainerView,
                        mResetHandler,
                        mIsVisibleSupplier,
                        mIsAnimatingSupplier,
                        mOnTabClickedCallback,
                        mHairlineVisibilityCallback,
                        TabListMode.GRID,
                        /* supportsEmptyState= */ true,
                        /* onTabGroupCreation= */ null,
                        () -> {
                            mDestroyed = true;
                        });
        watcher.assertExpected();

        mCoordinator.initWithNative();

        mIsVisibleSupplier.set(true);

        verify(mMessageManager).registerMessages(any());
        verify(mMessageManager).bind(any(), any(), any(), any());
    }

    DialogController showTabGridDialogWithTabs() {
        ViewStub dialogStub = new ViewStub(mActivity);
        mCoordinatorView.addView(dialogStub);
        dialogStub.setId(R.id.tab_grid_dialog_stub);

        DialogController controller = mCoordinator.getTabGridDialogControllerForTesting();
        MockTab tab = MockTab.createAndInitialize(/* id= */ 1, mProfile);
        tab.setIsInitialized(true);
        int index = 0;
        mTabModel.addTab(
                tab, index, TabLaunchType.FROM_CHROME_UI, TabCreationState.LIVE_IN_FOREGROUND);
        when(mTabModelFilter.indexOf(tab)).thenReturn(index);
        when(mTabModelFilter.getTabAt(index)).thenReturn(tab);
        controller.resetWithListOfTabs(Collections.singletonList(tab));

        return controller;
    }

    @After
    public void tearDown() {
        mCoordinator.destroy();
        assertTrue(mDestroyed);
    }

    @Test
    @SmallTest
    public void testShowTabListEditor() {
        ObservableSupplier<Boolean> handlesBackPressSupplier =
                mCoordinator.getHandleBackPressChangedSupplier();
        assertFalse(handlesBackPressSupplier.get());

        mCoordinator.showTabListEditor();
        assertTrue(handlesBackPressSupplier.get());

        assertNotNull(mActivity.findViewById(R.id.selectable_list));

        assertEquals(BackPressResult.SUCCESS, mCoordinator.handleBackPress());
        assertFalse(handlesBackPressSupplier.get());

        assertNull(mActivity.findViewById(R.id.selectable_list));
    }

    @Test
    @SmallTest
    public void testSetInitialScrollIndexOffset() {
        int index = 8;
        when(mTabModelFilter.index()).thenReturn(index);
        mCoordinator.setInitialScrollIndexOffset();

        assertEquals(
                index,
                mCoordinator
                        .getContainerViewModelForTesting()
                        .get(INITIAL_SCROLL_INDEX)
                        .intValue());
    }

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

        assertEquals(
                index,
                mCoordinator
                        .getContainerViewModelForTesting()
                        .get(FOCUS_TAB_INDEX_FOR_ACCESSIBILITY)
                        .intValue());
    }

    @Test
    @SmallTest
    @DisableFeatures({ChromeFeatureList.DATA_SHARING})
    @EnableFeatures(ChromeFeatureList.TAB_GROUP_PARITY_ANDROID)
    public void testTabGridDialogVisibilitySupplier() {

        Supplier<Boolean> tabGridDialogVisibilitySupplier =
                mCoordinator.getTabGridDialogVisibilitySupplier();

        assertFalse(tabGridDialogVisibilitySupplier.get());

        DialogController controller = showTabGridDialogWithTabs();
        assertTrue(tabGridDialogVisibilitySupplier.get());

        controller.hideDialog(false);
        assertFalse(tabGridDialogVisibilitySupplier.get());
    }

    @Test
    @SmallTest
    public void testCustomViewManager() {
        TabSwitcherCustomViewManager.Delegate customViewManagerDelegate =
                mCoordinator.getTabSwitcherCustomViewManagerDelegate();
        assertNotNull(customViewManagerDelegate);

        FrameLayout customView = new FrameLayout(mActivity);
        customViewManagerDelegate.addCustomView(customView, null, false);
        boolean found = false;
        for (int i = 0; i < mContainerView.getChildCount(); i++) {
            if (mContainerView.getChildAt(i) == customView) {
                found = true;
            }
        }
        assertTrue("Did not find added custom view.", found);

        customViewManagerDelegate.removeCustomView(customView);
        found = false;
        for (int i = 0; i < mContainerView.getChildCount(); i++) {
            if (mContainerView.getChildAt(i) == customView) {
                found = true;
            }
        }
        assertFalse("Did not remove custom view", found);
    }

    @Test
    @SmallTest
    public void testShowTab() {
        int tabId = 1;
        MockTab tab = MockTab.createAndInitialize(tabId, mProfile);
        tab.setIsInitialized(true);
        int index = 0;
        mTabModel.addTab(
                tab, index, TabLaunchType.FROM_CHROME_UI, TabCreationState.LIVE_IN_FOREGROUND);
        when(mTabModelFilter.indexOf(tab)).thenReturn(index);
        when(mTabModelFilter.getTabAt(index)).thenReturn(tab);
        when(mTabModelFilter.getCount()).thenReturn(1);
        when(mTabModelFilter.getRelatedTabList(tabId)).thenReturn(Collections.singletonList(tab));

        Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
        doCallback(2, (Callback<Bitmap> callback) -> callback.onResult(bitmap))
                .when(mTabContentManager)
                .getTabThumbnailWithCallback(eq(tabId), any(), any());
        mCoordinator.resetWithTabList(mTabModelFilter);

        TabListRecyclerView recyclerView = mActivity.findViewById(R.id.tab_list_recycler_view);
        // Manually size the view so that the children get added this is to work around robolectric
        // view testing limitations.
        recyclerView.measure(0, 0);
        recyclerView.layout(0, 0, 100, 1000);

        assertEquals(1, recyclerView.getAdapter().getItemCount());
        assertEquals(1, recyclerView.getChildCount());
        // This gets called three times
        // 1) Once when the fetcher is set.
        // 2) Twice due to thumbnail size changes on initial and repeat layout.
        verify(mTabContentManager, times(3)).getTabThumbnailWithCallback(eq(tabId), any(), any());

        TabThumbnailView thumbnailView = mActivity.findViewById(R.id.tab_thumbnail);
        assertNotNull(thumbnailView);
        assertFalse(thumbnailView.isPlaceholder());

        mIsVisibleSupplier.set(false);

        verify(mMessageManager, times(2)).unbind(any());

        mCoordinator.softCleanup();
        assertTrue(thumbnailView.isPlaceholder());

        mCoordinator.hardCleanup();
        assertEquals(0, recyclerView.getAdapter().getItemCount());
        // Don't assert on the actual child count, robolectric isn't removing the child view for
        // some reason.
    }

    @Test
    @SmallTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID, ChromeFeatureList.DATA_SHARING})
    public void testOpenInvitationModal() {
        IdentityServicesProvider.setInstanceForTests(mIdentityServicesProvider);
        when(mIdentityServicesProvider.getIdentityManager(any())).thenReturn(mIdentityManager);
        DataSharingServiceFactory.setForTesting(mDataSharingService);

        DialogController controller = showTabGridDialogWithTabs();

        assertTrue(controller.isVisible());

        mCoordinator.openInvitationModal("");
        assertFalse(controller.isVisible());
    }
}