chromium/chrome/android/junit/src/org/chromium/chrome/browser/tabmodel/TabModelImplUnitTest.java

// Copyright 2022 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.tabmodel;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
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.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Activity;
import android.content.Context;

import androidx.test.filters.SmallTest;

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.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.flags.ActivityType;
import org.chromium.chrome.browser.homepage.HomepageManager;
import org.chromium.chrome.browser.price_tracking.PriceTrackingFeatures;
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.TabDelegateFactory;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.ui.base.WindowAndroid;

import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.List;

/** Unit tests for {@link TabModelImpl}. */
@RunWith(BaseRobolectricTestRunner.class)
public class TabModelImplUnitTest {
    private static final long FAKE_NATIVE_ADDRESS = 123L;

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

    /** Disable native calls from {@link TabModelJniBridge}. */
    @Rule public JniMocker mJniMocker = new JniMocker();

    @Mock private TabModelJniBridge.Natives mTabModelJniBridge;

    /** Required to be non-null for {@link TabModelJniBridge}. */
    @Mock private Profile mProfile;

    @Mock private Profile mIncognitoProfile;

    /** Required to simulate tab thumbnail deletion. */
    @Mock private TabContentManager mTabContentManager;

    /** Required to handle some tab lookup actions. */
    @Mock private TabModelDelegate mTabModelDelegate;

    /** Required to handle some actions and initialize {@link TabModelOrderControllerImpl}. */
    @Mock private TabModelSelector mTabModelSelector;

    @Mock private TabModelFilterProvider mTabModelFilterProvider;
    @Mock private TabModelFilter mTabModelFilter;

    @Mock private Callback<Tab> mTabSupplierObserver;
    @Mock private TabGroupModelFilter mTabGroupModelFilter;
    @Mock private WindowAndroid mWindowAndroid;
    @Mock private TabDelegateFactory mTabDelegateFactory;
    @Mock private WeakReference<Context> mWeakReferenceContext;
    @Mock private WeakReference<Activity> mWeakReferenceActivity;

    private int mNextTabId;

    @Before
    public void setUp() {
        // Disable HomepageManager#shouldCloseAppWithZeroTabs() for TabModelImpl#closeTabs().
        HomepageManager.getInstance().setPrefHomepageEnabled(false);

        when(mIncognitoProfile.isOffTheRecord()).thenReturn(true);
        PriceTrackingFeatures.setPriceTrackingEnabledForTesting(false);

        mJniMocker.mock(TabModelJniBridgeJni.TEST_HOOKS, mTabModelJniBridge);
        when(mTabModelJniBridge.init(any(), any(), anyInt(), anyBoolean()))
                .thenReturn(FAKE_NATIVE_ADDRESS);

        when(mTabModelDelegate.isReparentingInProgress()).thenReturn(false);

        when(mTabModelSelector.getTabModelFilterProvider()).thenReturn(mTabModelFilterProvider);
        when(mTabModelFilterProvider.getTabModelFilter(false)).thenReturn(mTabModelFilter);
        when(mTabModelFilterProvider.getTabModelFilter(true)).thenReturn(mTabModelFilter);
        when(mTabModelFilter.getValidPosition(any(), anyInt()))
                .thenAnswer(i -> i.getArguments()[1]);

        when(mWindowAndroid.getActivity()).thenReturn(mWeakReferenceActivity);
        when(mWindowAndroid.getContext()).thenReturn(mWeakReferenceContext);
        when(mTabGroupModelFilter.getValidPosition(any(), anyInt()))
                .thenAnswer(i -> i.getArguments()[1]);

        TabModelSelectorSupplier.setInstanceForTesting(mTabModelSelector);

        mNextTabId = 0;
    }

    private Tab createTab(final TabModel model) {
        return createTab(model, 0, Tab.INVALID_TAB_ID);
    }

    private Tab createTab(final TabModel model, long activeTimestampMillis, int parentId) {
        MockTab tab = MockTab.createAndInitialize(mNextTabId++, model.getProfile());
        tab.setTimestampMillis(activeTimestampMillis);
        tab.setParentId(parentId);
        tab.setIsInitialized(true);
        model.addTab(
                tab,
                TabList.INVALID_TAB_INDEX,
                TabLaunchType.FROM_CHROME_UI,
                TabCreationState.LIVE_IN_FOREGROUND);
        return tab;
    }

    private void selectTab(final TabModel model, final Tab tab) {
        model.setIndex(model.indexOf(tab), TabSelectionType.FROM_USER);
    }

    /** Create a {@link TabModel} to use for the test. */
    private TabModel createTabModel(boolean isActive, boolean isIncognito) {
        AsyncTabParamsManager realAsyncTabParamsManager =
                AsyncTabParamsManagerFactory.createAsyncTabParamsManager();
        TabModelOrderControllerImpl orderController =
                new TabModelOrderControllerImpl(mTabModelSelector);
        Profile profile = isIncognito ? mIncognitoProfile : mProfile;
        TabModel tabModel =
                new TabModelImpl(
                        profile,
                        ActivityType.TABBED,
                        /* regularTabCreator= */ null,
                        /* incognitoTabCreator= */ null,
                        orderController,
                        mTabContentManager,
                        () -> NextTabPolicy.HIERARCHICAL,
                        realAsyncTabParamsManager,
                        mTabModelDelegate,
                        /* supportUndo= */ true,
                        /* trackInNativeModelList= */ true);
        when(mTabModelSelector.getModel(isIncognito)).thenReturn(tabModel);
        tabModel.setActive(isActive);
        if (isActive) {
            when(mTabModelSelector.getCurrentModel()).thenReturn(tabModel);
            when(mTabModelDelegate.getCurrentModel()).thenReturn(tabModel);
        }
        when(mTabModelDelegate.getModel(isIncognito)).thenReturn(tabModel);
        return tabModel;
    }

    @Test
    @SmallTest
    public void testGetNextTabIfClosed_InactiveModel() {
        TabModel activeIncognito = createTabModel(true, true);
        TabModel inactiveNormal = createTabModel(false, false);

        Tab incognitoTab0 = createTab(activeIncognito);
        Tab incognitoTab1 = createTab(activeIncognito);
        Tab tab0 = createTab(inactiveNormal);
        Tab tab1 = createTab(inactiveNormal);

        selectTab(activeIncognito, incognitoTab1);
        selectTab(inactiveNormal, tab1);

        assertEquals(incognitoTab1, inactiveNormal.getNextTabIfClosed(tab1.getId(), false));
    }

    @Test
    @SmallTest
    public void testGetNextTabIfClosed_NotCurrentTab() {
        TabModel activeNormal = createTabModel(true, false);
        // Unused but required for correct mocking of mTabModelDelegate to avoid NPE.
        TabModel inactiveIncognito = createTabModel(false, true);

        Tab tab0 = createTab(activeNormal);
        Tab tab1 = createTab(activeNormal);
        Tab tab2 = createTab(activeNormal);

        selectTab(activeNormal, tab0);
        assertEquals(tab0, activeNormal.getNextTabIfClosed(tab1.getId(), false));
        assertEquals(tab0, activeNormal.getNextTabIfClosed(tab2.getId(), false));

        selectTab(activeNormal, tab1);
        assertEquals(tab1, activeNormal.getNextTabIfClosed(tab0.getId(), false));
        assertEquals(tab1, activeNormal.getNextTabIfClosed(tab2.getId(), false));

        selectTab(activeNormal, tab2);
        assertEquals(tab2, activeNormal.getNextTabIfClosed(tab0.getId(), false));
        assertEquals(tab2, activeNormal.getNextTabIfClosed(tab1.getId(), false));
    }

    @Test
    @SmallTest
    public void testGetNextTabIfClosed_ParentTab() {
        TabModel activeNormal = createTabModel(true, false);
        // Unused but required for correct mocking of mTabModelDelegate to avoid NPE.
        TabModel inactiveIncognito = createTabModel(false, true);

        Tab tab0 = createTab(activeNormal);
        Tab tab1 = createTab(activeNormal);
        Tab tab2 = createTab(activeNormal, 0, tab0.getId());

        selectTab(activeNormal, tab2);
        assertEquals(tab0, activeNormal.getNextTabIfClosed(tab2.getId(), false));
    }

    @Test
    @SmallTest
    public void testGetNextTabIfClosed_Adjacent() {
        TabModel activeNormal = createTabModel(true, false);
        // Unused but required for correct mocking of mTabModelDelegate to avoid NPE.
        TabModel inactiveIncognito = createTabModel(false, true);

        Tab tab0 = createTab(activeNormal);
        Tab tab1 = createTab(activeNormal);
        Tab tab2 = createTab(activeNormal);

        selectTab(activeNormal, tab0);
        assertEquals(tab1, activeNormal.getNextTabIfClosed(tab0.getId(), false));

        selectTab(activeNormal, tab1);
        assertEquals(tab0, activeNormal.getNextTabIfClosed(tab1.getId(), false));

        selectTab(activeNormal, tab2);
        assertEquals(tab1, activeNormal.getNextTabIfClosed(tab2.getId(), false));
    }

    @Test
    @SmallTest
    public void testGetNextTabIfClosed_LastIncognitoTab() {
        TabModel activeIncognito = createTabModel(true, true);
        TabModel inactiveNormal = createTabModel(false, false);

        Tab incognitoTab0 = createTab(activeIncognito);
        Tab tab0 = createTab(inactiveNormal);
        Tab tab1 = createTab(inactiveNormal);

        selectTab(inactiveNormal, tab0);
        assertEquals(tab0, activeIncognito.getNextTabIfClosed(incognitoTab0.getId(), false));

        selectTab(inactiveNormal, tab1);
        assertEquals(tab1, activeIncognito.getNextTabIfClosed(incognitoTab0.getId(), false));
    }

    @Test
    @SmallTest
    public void testGetNextTabIfClosed_MostRecentTab() {
        TabModel activeNormal = createTabModel(true, false);
        // Unused but required for correct mocking of mTabModelDelegate to avoid NPE.
        TabModel inactiveIncognito = createTabModel(false, true);

        // uponExit overrides parent selection..
        Tab tab0 = createTab(activeNormal, 10, Tab.INVALID_TAB_ID);
        Tab tab1 = createTab(activeNormal, 200, tab0.getId());
        Tab tab2 = createTab(activeNormal, 30, tab0.getId());

        selectTab(activeNormal, tab0);
        assertEquals(tab1, activeNormal.getNextTabIfClosed(tab0.getId(), true));

        selectTab(activeNormal, tab1);
        assertEquals(tab2, activeNormal.getNextTabIfClosed(tab1.getId(), true));

        selectTab(activeNormal, tab2);
        assertEquals(tab1, activeNormal.getNextTabIfClosed(tab2.getId(), true));
    }

    @Test
    @SmallTest
    public void testGetNextTabIfClosed_InvalidSelection() {
        TabModel activeNormal = createTabModel(true, false);
        // Unused but required for correct mocking of mTabModelDelegate to avoid NPE.
        TabModel inactiveIncognito = createTabModel(false, true);

        Tab tab0 = createTab(activeNormal);
        selectTab(activeNormal, tab0);
        assertNull(activeNormal.getNextTabIfClosed(tab0.getId(), false));
    }

    @Test
    @SmallTest
    public void testDontSwitchModelsIfIncognitoGroupClosed() {
        TabModel activeIncognito = createTabModel(true, true);
        TabModel inactiveNormal = createTabModel(false, false);

        Tab incognitoTab0 = createTab(activeIncognito);
        Tab incognitoTab1 = createTab(activeIncognito);
        Tab incognitoTab2 = createTab(activeIncognito);
        Tab tab0 = createTab(inactiveNormal);

        selectTab(activeIncognito, incognitoTab0);

        activeIncognito.closeTabs(
                TabClosureParams.closeTabs(List.of(incognitoTab0, incognitoTab1))
                        .allowUndo(false)
                        .build());
        verify(mTabModelSelector, never()).selectModel(anyBoolean());
        assertEquals(incognitoTab2, activeIncognito.getTabAt(activeIncognito.index()));
    }

    @Test
    @SmallTest
    public void testObserveCurrentTabSupplierActiveNormal() {
        TabModel activeNormal = createTabModel(true, false);
        // Unused but required for correct mocking of mTabModelDelegate to avoid NPE.
        TabModel inactiveIncognito = createTabModel(false, true);

        assertNull(activeNormal.getCurrentTabSupplier().get());
        assertEquals(0, activeNormal.getTabCountSupplier().get().intValue());
        activeNormal.getCurrentTabSupplier().addObserver(mTabSupplierObserver);

        Tab tab0 = createTab(activeNormal);
        assertEquals(tab0, activeNormal.getCurrentTabSupplier().get());
        assertEquals(1, activeNormal.getTabCountSupplier().get().intValue());
        verify(mTabSupplierObserver).onResult(eq(tab0));

        Tab tab1 = createTab(activeNormal);
        assertEquals(tab1, activeNormal.getCurrentTabSupplier().get());
        assertEquals(2, activeNormal.getTabCountSupplier().get().intValue());
        verify(mTabSupplierObserver).onResult(eq(tab1));

        selectTab(activeNormal, tab0);
        assertEquals(tab0, activeNormal.getCurrentTabSupplier().get());
        assertEquals(2, activeNormal.getTabCountSupplier().get().intValue());
        verify(mTabSupplierObserver, times(2)).onResult(eq(tab0));

        activeNormal.removeTab(tab0);
        assertEquals(tab1, activeNormal.getCurrentTabSupplier().get());
        assertEquals(1, activeNormal.getTabCountSupplier().get().intValue());
        verify(mTabSupplierObserver, times(2)).onResult(eq(tab1));
    }

    @Test
    @SmallTest
    public void testObserveCurrentTabSupplierInactiveNormal() {
        TabModel inactiveNormal = createTabModel(false, false);
        // Unused but required for correct mocking of mTabModelDelegate to avoid NPE.
        TabModel activeIncognito = createTabModel(true, true);

        assertNull(inactiveNormal.getCurrentTabSupplier().get());
        assertEquals(0, inactiveNormal.getTabCountSupplier().get().intValue());
        inactiveNormal.getCurrentTabSupplier().addObserver(mTabSupplierObserver);

        Tab tab0 = createTab(inactiveNormal);
        assertEquals(tab0, inactiveNormal.getCurrentTabSupplier().get());
        assertEquals(1, inactiveNormal.getTabCountSupplier().get().intValue());
        verify(mTabSupplierObserver).onResult(eq(tab0));

        Tab tab1 = createTab(inactiveNormal);
        assertEquals(tab1, inactiveNormal.getCurrentTabSupplier().get());
        assertEquals(2, inactiveNormal.getTabCountSupplier().get().intValue());
        verify(mTabSupplierObserver).onResult(eq(tab1));

        selectTab(inactiveNormal, tab0);
        assertEquals(tab0, inactiveNormal.getCurrentTabSupplier().get());
        assertEquals(2, inactiveNormal.getTabCountSupplier().get().intValue());
        verify(mTabSupplierObserver, times(2)).onResult(eq(tab0));

        inactiveNormal.removeTab(tab0);
        assertEquals(tab1, inactiveNormal.getCurrentTabSupplier().get());
        assertEquals(1, inactiveNormal.getTabCountSupplier().get().intValue());
        verify(mTabSupplierObserver, times(2)).onResult(eq(tab1));
    }

    @Test
    @SmallTest
    public void testGetTabById() {
        TabModel tabModel = createTabModel(/* isActive= */ true, /* isIncognito= */ false);
        createTabModel(/* isActive= */ false, /* isIncognito= */ true);

        Tab tab1 = createTab(tabModel);
        assertEquals(tab1, tabModel.getTabById(tab1.getId()));

        tabModel.closeTabs(TabClosureParams.closeTab(tab1).build());
        assertEquals(null, tabModel.getTabById(tab1.getId()));

        tabModel.cancelTabClosure(tab1.getId());
        assertEquals(tab1, tabModel.getTabById(tab1.getId()));

        tabModel.destroy();
        assertEquals(null, tabModel.getTabById(tab1.getId()));
    }

    @Test
    @SmallTest
    public void testGetTabsNavigatedInTimeWindow() {
        TabModelImpl tabModel =
                (TabModelImpl) createTabModel(/* isActive= */ true, /* isIncognito= */ false);
        MockTab tab1 = (MockTab) createTab(tabModel, 0, Tab.INVALID_TAB_ID);
        tab1.setLastNavigationCommittedTimestampMillis(200);

        MockTab tab2 = (MockTab) createTab(tabModel, 0, Tab.INVALID_TAB_ID);
        tab2.setLastNavigationCommittedTimestampMillis(50);

        MockTab tab3 = (MockTab) createTab(tabModel, 0, Tab.INVALID_TAB_ID);
        tab3.setLastNavigationCommittedTimestampMillis(100);

        MockTab tab4 = (MockTab) createTab(tabModel, 0, Tab.INVALID_TAB_ID);
        tab4.setLastNavigationCommittedTimestampMillis(30);
        tab4.setIsCustomTab(true);

        MockTab tab5 = (MockTab) createTab(tabModel, 0, Tab.INVALID_TAB_ID);
        tab5.setLastNavigationCommittedTimestampMillis(10);

        assertEquals(Arrays.asList(tab2, tab5), tabModel.getTabsNavigatedInTimeWindow(10, 100));
    }

    @Test
    @SmallTest
    public void testCloseTabsNavigatedInTimeWindow() {
        when(mTabModelFilterProvider.getTabModelFilter(/* isIncognito= */ false))
                .thenReturn(mTabGroupModelFilter);

        TabModelImpl tabModel =
                (TabModelImpl) createTabModel(/* isActive= */ true, /* isIncognito= */ false);

        MockTab tab1 = (MockTab) createTab(tabModel, 0, Tab.INVALID_TAB_ID);
        tab1.setLastNavigationCommittedTimestampMillis(200);
        tab1.updateAttachment(mWindowAndroid, mTabDelegateFactory);

        MockTab tab2 = (MockTab) createTab(tabModel, 0, Tab.INVALID_TAB_ID);
        tab2.setLastNavigationCommittedTimestampMillis(30);
        tab2.updateAttachment(mWindowAndroid, mTabDelegateFactory);

        MockTab tab3 = (MockTab) createTab(tabModel, 0, Tab.INVALID_TAB_ID);
        tab3.setLastNavigationCommittedTimestampMillis(20);
        tab3.updateAttachment(mWindowAndroid, mTabDelegateFactory);

        tabModel.closeTabsNavigatedInTimeWindow(20, 50);
        verify(mTabGroupModelFilter)
                .closeTabs(
                        TabClosureParams.closeTabs(Arrays.asList(tab2, tab3))
                                .allowUndo(false)
                                .saveToTabRestoreService(false)
                                .build());
    }
}