// 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.tasks;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
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.ReturnToChromeUtil.FAIL_TO_SHOW_HOME_SURFACE_UI_UMA;
import static org.chromium.chrome.browser.tasks.ReturnToChromeUtil.HOME_SURFACE_RETURN_TIME_SECONDS;
import static org.chromium.chrome.browser.tasks.ReturnToChromeUtil.HOME_SURFACE_SHOWN_AT_STARTUP_UMA;
import static org.chromium.chrome.browser.tasks.ReturnToChromeUtil.HOME_SURFACE_SHOWN_UMA;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.format.DateUtils;
import androidx.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.chromium.base.BaseSwitches;
import org.chromium.base.IntentUtils;
import org.chromium.base.SysUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CommandLineFlags;
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.ChromeInactivityTracker;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.homepage.HomepageManager;
import org.chromium.chrome.browser.homepage.HomepagePolicyManager;
import org.chromium.chrome.browser.magic_stack.HomeModulesMetricsUtils;
import org.chromium.chrome.browser.ntp.NewTabPage;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabCreator;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.tasks.ReturnToChromeUtil.FailToShowHomeSurfaceReason;
import org.chromium.chrome.browser.tasks.ReturnToChromeUtilUnitTest.ShadowHomepagePolicyManager;
import org.chromium.chrome.browser.ui.native_page.FrozenNativePage;
import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.url.JUnitTestGURLs;
/** Unit tests for {@link ReturnToChromeUtil} class. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
manifest = Config.NONE,
shadows = {ShadowHomepagePolicyManager.class})
@CommandLineFlags.Add({BaseSwitches.DISABLE_LOW_END_DEVICE_MODE})
public class ReturnToChromeUtilUnitTest {
@Implements(HomepagePolicyManager.class)
static class ShadowHomepagePolicyManager {
static boolean sIsInitialized;
@Implementation
public static boolean isInitializedWithNative() {
return sIsInitialized;
}
}
private static final int ON_RETURN_THRESHOLD_SECOND = 1000;
private static final int DELTA_MS = 100;
@Rule public JniMocker mJniMocker = new JniMocker();
@Mock private Context mContext;
@Mock private TabModelSelector mTabModelSelector;
@Mock private ChromeInactivityTracker mInactivityTracker;
@Mock private Resources mResources;
@Mock private TabModel mCurrentTabModel;
@Mock private TabCreator mTabCreater;
@Mock private Tab mTab1;
@Mock private Tab mNtpTab;
@Mock private NewTabPage mNewTabPage;
@Mock private HomeSurfaceTracker mHomeSurfaceTracker;
@Mock private Bundle mSaveInstanceState;
@Captor private ArgumentCaptor<TabModelObserver> mTabModelObserverCaptor;
@Mock private HomepageManager mHomepageManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
doReturn(JUnitTestGURLs.NTP_NATIVE_URL).when(mNtpTab).getUrl();
// HomepageManager:
HomepageManager.setInstanceForTesting(mHomepageManager);
doReturn(true).when(mHomepageManager).isHomepageEnabled();
doReturn(UrlConstants.ntpGurl()).when(mHomepageManager).getHomepageGurl();
ShadowHomepagePolicyManager.sIsInitialized = true;
assertTrue(HomepagePolicyManager.isInitializedWithNative());
// Low end devices:
Assert.assertFalse(SysUtils.isLowEndDevice());
// Sets accessibility:
ChromeAccessibilityUtil.get().setAccessibilityEnabledForTesting(false);
// Sets for phones, i.e., !DeviceFormFactor.isNonMultiDisplayContextOnTablet():
doReturn(mResources).when(mContext).getResources();
doReturn(DeviceFormFactor.SCREEN_BUCKET_TABLET - 1)
.when(mResources)
.getInteger(org.chromium.ui.R.integer.min_screen_width_bucket);
Assert.assertFalse(DeviceFormFactor.isNonMultiDisplayContextOnTablet(mContext));
}
@Test
@SmallTest
public void testShouldShowTabSwitcher() {
Assert.assertEquals(
HOME_SURFACE_RETURN_TIME_SECONDS.getDefaultValue(),
HOME_SURFACE_RETURN_TIME_SECONDS.getValue());
long returnTimeMs =
HOME_SURFACE_RETURN_TIME_SECONDS.getValue() * DateUtils.SECOND_IN_MILLIS;
// When return time doesn't arrive, return false:
Assert.assertFalse(
ReturnToChromeUtil.shouldShowTabSwitcher(
System.currentTimeMillis() - returnTimeMs + DELTA_MS));
// When return time arrives, return true:
assertTrue(
ReturnToChromeUtil.shouldShowTabSwitcher(
System.currentTimeMillis() - returnTimeMs - 1));
}
@Test
@SmallTest
public void testShouldShowNtpAsHomeSurfaceAtStartup() {
// Sets main intent from launcher:
Intent intent = createMainIntentFromLauncher();
// Sets background time to make the return time arrive:
ChromeSharedPreferences.getInstance()
.addToStringSet(
ChromePreferenceKeys.TABBED_ACTIVITY_LAST_BACKGROUNDED_TIME_MS_PREF, "0");
HOME_SURFACE_RETURN_TIME_SECONDS.setForTesting(0);
assertTrue(ReturnToChromeUtil.shouldShowTabSwitcher(0));
// Tests the case when there isn't any Tab. Verifies that home surface NTP is shown.
doReturn(true).when(mTabModelSelector).isTabStateInitialized();
doReturn(0).when(mTabModelSelector).getTotalTabCount();
assertTrue(HomepagePolicyManager.isInitializedWithNative());
assertTrue(IntentUtils.isMainIntentFromLauncher(intent));
assertTrue(
ReturnToChromeUtil.shouldShowNtpAsHomeSurfaceAtStartup(
intent, null, mInactivityTracker));
// Tests the case when the total tab count > 0. Verifies that home surface NTP is shown
doReturn(1).when(mTabModelSelector).getTotalTabCount();
assertTrue(
ReturnToChromeUtil.shouldShowNtpAsHomeSurfaceAtStartup(
intent, null, mInactivityTracker));
}
@Test
@SmallTest
public void testShowNtpAsHomeSurfaceAtResumeOnTabletWithExistingNtp() {
doReturn(2).when(mCurrentTabModel).getCount();
doReturn(JUnitTestGURLs.URL_1).when(mTab1).getUrl();
doReturn(mTab1).when(mCurrentTabModel).getTabAt(0);
doReturn(true).when(mNtpTab).isNativePage();
doReturn(mNewTabPage).when(mNtpTab).getNativePage();
doReturn(mNtpTab).when(mCurrentTabModel).getTabAt(1);
// Sets the NTP is the last active Tab.
doReturn(1).when(mCurrentTabModel).index();
// Tests case of the last active NTP has home surface UI.
doReturn(true).when(mHomeSurfaceTracker).canShowHomeSurface(mNtpTab);
HistogramWatcher histogram =
HistogramWatcher.newBuilder()
.expectBooleanRecord(HOME_SURFACE_SHOWN_AT_STARTUP_UMA, true)
.expectIntRecords(HOME_SURFACE_SHOWN_UMA, 1)
.build();
ReturnToChromeUtil.setInitialOverviewStateOnResumeWithNtp(
false,
/* shouldShowNtpHomeSurfaceOnStartup= */ true,
mCurrentTabModel,
mTabCreater,
mHomeSurfaceTracker);
verify(mTabCreater, never()).createNewTab(any(), eq(TabLaunchType.FROM_STARTUP), eq(null));
verify(mCurrentTabModel, never()).setIndex(anyInt(), eq(TabSelectionType.FROM_USER));
verify(mNewTabPage, never()).showMagicStack(any());
verify(mHomeSurfaceTracker).updateHomeSurfaceAndTrackingTabs(eq(mNtpTab), eq(null));
histogram.assertExpected();
// Tests the case of the last active NTP doesn't has home surface UI.
doReturn(false).when(mHomeSurfaceTracker).canShowHomeSurface(mNtpTab);
histogram =
HistogramWatcher.newBuilder()
.expectBooleanRecord(HOME_SURFACE_SHOWN_AT_STARTUP_UMA, true)
.expectIntRecords(HOME_SURFACE_SHOWN_UMA, 1)
.build();
ReturnToChromeUtil.setInitialOverviewStateOnResumeWithNtp(
false,
/* shouldShowNtpHomeSurfaceOnStartup= */ true,
mCurrentTabModel,
mTabCreater,
mHomeSurfaceTracker);
verify(mHomeSurfaceTracker, times(2))
.updateHomeSurfaceAndTrackingTabs(eq(mNtpTab), eq(null));
histogram.assertExpected();
// Sets the last active Tab isn't a NTP.
doReturn(0).when(mCurrentTabModel).index();
// Verifies that if the NTP isn't the last active Tab, we reuse it, set index and call
// showHomeSurfaceUi() to show the single tab card module.
histogram =
HistogramWatcher.newBuilder()
.expectBooleanRecord(HOME_SURFACE_SHOWN_AT_STARTUP_UMA, true)
.expectIntRecords(HOME_SURFACE_SHOWN_UMA, 1)
.build();
ReturnToChromeUtil.setInitialOverviewStateOnResumeWithNtp(
false,
/* shouldShowNtpHomeSurfaceOnStartup= */ true,
mCurrentTabModel,
mTabCreater,
mHomeSurfaceTracker);
verify(mTabCreater, never()).createNewTab(any(), eq(TabLaunchType.FROM_STARTUP), eq(null));
verify(mCurrentTabModel).setIndex(eq(1), eq(TabSelectionType.FROM_USER));
verify(mNewTabPage).showMagicStack(eq(mTab1));
verify(mHomeSurfaceTracker).updateHomeSurfaceAndTrackingTabs(eq(mNtpTab), eq(mTab1));
histogram.assertExpected();
}
@Test
@SmallTest
public void testShowNtpAsHomeSurfaceAtResumeOnTabletWithoutAnyExistingNtp() {
doReturn(1).when(mCurrentTabModel).getCount();
doReturn(JUnitTestGURLs.URL_1).when(mTab1).getUrl();
doReturn(mTab1).when(mCurrentTabModel).getTabAt(0);
// Verifies that if the return time doesn't arrive, there isn't a new NTP is created.
ReturnToChromeUtil.setInitialOverviewStateOnResumeWithNtp(
false,
/* shouldShowNtpHomeSurfaceOnStartup= */ false,
mCurrentTabModel,
mTabCreater,
mHomeSurfaceTracker);
verify(mTabCreater, never()).createNewTab(any(), eq(TabLaunchType.FROM_STARTUP), eq(null));
verify(mHomeSurfaceTracker, never()).updateHomeSurfaceAndTrackingTabs(any(), any());
// Verifies that a new NTP is created when there isn't any existing one to reuse.
doReturn(2).when(mNtpTab).getId();
doReturn(true).when(mNtpTab).isNativePage();
doReturn(mNewTabPage).when(mNtpTab).getNativePage();
doReturn(mNtpTab)
.when(mTabCreater)
.createNewTab(any(), eq(TabLaunchType.FROM_STARTUP), eq(null));
doReturn(0).when(mCurrentTabModel).index();
HistogramWatcher histogram =
HistogramWatcher.newBuilder()
.expectBooleanRecord(HOME_SURFACE_SHOWN_AT_STARTUP_UMA, true)
.expectBooleanRecord(HOME_SURFACE_SHOWN_UMA, true)
.build();
ReturnToChromeUtil.setInitialOverviewStateOnResumeWithNtp(
false,
/* shouldShowNtpHomeSurfaceOnStartup= */ true,
mCurrentTabModel,
mTabCreater,
mHomeSurfaceTracker);
verify(mTabCreater, times(1)).createNewTab(any(), eq(TabLaunchType.FROM_STARTUP), eq(null));
verify(mNewTabPage).showMagicStack(eq(mTab1));
verify(mHomeSurfaceTracker).updateHomeSurfaceAndTrackingTabs(eq(mNtpTab), eq(mTab1));
histogram.assertExpected();
}
@Test
@SmallTest
public void testShowNtpAsHomeSurfaceAtResumeOnTabletWithMixedNtps() {
doReturn(3).when(mCurrentTabModel).getCount();
doReturn(JUnitTestGURLs.URL_1).when(mTab1).getUrl();
doReturn(mTab1).when(mCurrentTabModel).getTabAt(0);
doReturn(JUnitTestGURLs.NTP_NATIVE_URL).when(mNtpTab).getUrl();
doReturn(true).when(mNtpTab).isNativePage();
doReturn(mNewTabPage).when(mNtpTab).getNativePage();
doReturn(mNtpTab).when(mCurrentTabModel).getTabAt(1);
Tab activeNtpTab = Mockito.mock(Tab.class);
NewTabPage activeNtp = Mockito.mock(NewTabPage.class);
doReturn(JUnitTestGURLs.NTP_NATIVE_URL).when(activeNtpTab).getUrl();
doReturn(true).when(activeNtpTab).isNativePage();
doReturn(activeNtp).when(activeNtpTab).getNativePage();
doReturn(activeNtpTab).when(mCurrentTabModel).getTabAt(2);
// Set the active NTP tab as the last Tab, and has a tracking Tab.
doReturn(2).when(mCurrentTabModel).index();
doReturn(true).when(mHomeSurfaceTracker).canShowHomeSurface(activeNtpTab);
// Verifies that the first found NTP isn't the active NTP Tab.
Assert.assertEquals(
1, TabModelUtils.getTabIndexByUrl(mCurrentTabModel, UrlConstants.NTP_URL));
HistogramWatcher histogram =
HistogramWatcher.newBuilder()
.expectBooleanRecord(HOME_SURFACE_SHOWN_AT_STARTUP_UMA, true)
.expectBooleanRecord(HOME_SURFACE_SHOWN_UMA, true)
.build();
// Verifies the active NTP will be shown with its home surface UI, not the first found NTP.
ReturnToChromeUtil.setInitialOverviewStateOnResumeWithNtp(
false,
/* shouldShowNtpHomeSurfaceOnStartup= */ true,
mCurrentTabModel,
mTabCreater,
mHomeSurfaceTracker);
histogram.assertExpected();
verify(mHomeSurfaceTracker, never()).updateHomeSurfaceAndTrackingTabs(eq(mNtpTab), any());
verify(mNewTabPage, never()).showMagicStack(any());
// Set the last active NTP doesn't have a tracking Tab.
doReturn(false).when(mHomeSurfaceTracker).canShowHomeSurface(activeNtpTab);
histogram =
HistogramWatcher.newBuilder()
.expectBooleanRecord(HOME_SURFACE_SHOWN_AT_STARTUP_UMA, true)
.expectBooleanRecord(HOME_SURFACE_SHOWN_UMA, true)
.build();
// Verifies the active NTP will be shown as it is now, i.e., an empty NTP, not the first
// found NTP.
ReturnToChromeUtil.setInitialOverviewStateOnResumeWithNtp(
false,
/* shouldShowNtpHomeSurfaceOnStartup= */ true,
mCurrentTabModel,
mTabCreater,
mHomeSurfaceTracker);
histogram.assertExpected();
verify(mHomeSurfaceTracker, never()).updateHomeSurfaceAndTrackingTabs(eq(mNtpTab), any());
verify(mNewTabPage, never()).showMagicStack(any());
}
@Test
@SmallTest
public void testNoAnyTabCase() {
doReturn(0).when(mCurrentTabModel).getCount();
// Verifies that if there isn't any existing Tab, we don't create a home surface NTP.
ReturnToChromeUtil.setInitialOverviewStateOnResumeWithNtp(
false,
/* shouldShowNtpHomeSurfaceOnStartup= */ true,
mCurrentTabModel,
mTabCreater,
mHomeSurfaceTracker);
verify(mTabCreater, never()).createNewTab(any(), eq(TabLaunchType.FROM_STARTUP), eq(null));
verify(mHomeSurfaceTracker, never()).updateHomeSurfaceAndTrackingTabs(any(), any());
}
@Test
@SmallTest
@EnableFeatures({ChromeFeatureList.MAGIC_STACK_ANDROID})
public void testColdStartupWithOnlyLastActiveTabUrl_MagicStack() {
assertTrue(HomeModulesMetricsUtils.useMagicStack());
when(mTab1.getUrl()).thenReturn(JUnitTestGURLs.URL_1);
when(mNtpTab.isNativePage()).thenReturn(true);
when(mNtpTab.getNativePage()).thenReturn(mNewTabPage);
when(mTabCreater.createNewTab(any(), eq(TabLaunchType.FROM_STARTUP), eq(null)))
.thenReturn(mNtpTab);
when(mTabModelSelector.getModel(false)).thenReturn(mCurrentTabModel);
// Tests the case that a new NTP is created and waits for its tracking last active Tab being
// restored.
ReturnToChromeUtil.createNewTabAndShowHomeSurfaceUi(
mTabCreater,
mHomeSurfaceTracker,
mTabModelSelector,
JUnitTestGURLs.URL_1.getSpec(),
null);
verify(mCurrentTabModel).addObserver(mTabModelObserverCaptor.capture());
// Verifies if the added Tab matches the tracking URL, call showHomeSurfaceUi().
mTabModelObserverCaptor.getValue().willAddTab(mTab1, TabLaunchType.FROM_RESTORE);
verify(mNewTabPage).showMagicStack(eq(mTab1));
verify(mHomeSurfaceTracker).updateHomeSurfaceAndTrackingTabs(eq(mNtpTab), eq(mTab1));
}
@Test
@SmallTest
public void testShouldNotShowNtpOnRecreate() {
// Sets main intent from launcher:
Intent intent = createMainIntentFromLauncher();
// Sets background time to make the return time arrive:
ChromeSharedPreferences.getInstance()
.addToStringSet(
ChromePreferenceKeys.TABBED_ACTIVITY_LAST_BACKGROUNDED_TIME_MS_PREF, "0");
HOME_SURFACE_RETURN_TIME_SECONDS.setForTesting(0);
assertTrue(ReturnToChromeUtil.shouldShowTabSwitcher(0));
// There should always be at least 1 tab. Otherwise one will be created regardless.
doReturn(true).when(mTabModelSelector).isTabStateInitialized();
doReturn(1).when(mTabModelSelector).getTotalTabCount();
assertTrue(HomepagePolicyManager.isInitializedWithNative());
assertTrue(IntentUtils.isMainIntentFromLauncher(intent));
assertTrue(
ReturnToChromeUtil.shouldShowNtpAsHomeSurfaceAtStartup(
intent, mSaveInstanceState, mInactivityTracker));
doReturn(true)
.when(mSaveInstanceState)
.getBoolean(ChromeActivity.IS_FROM_RECREATING, false);
assertFalse(
ReturnToChromeUtil.shouldShowNtpAsHomeSurfaceAtStartup(
intent, mSaveInstanceState, mInactivityTracker));
doReturn(false)
.when(mSaveInstanceState)
.getBoolean(ChromeActivity.IS_FROM_RECREATING, false);
assertTrue(
ReturnToChromeUtil.shouldShowNtpAsHomeSurfaceAtStartup(
intent, mSaveInstanceState, mInactivityTracker));
}
@Test
@SmallTest
public void testLogFailToShowHomeSurfaceUI() {
HistogramWatcher histogram =
HistogramWatcher.newBuilder()
.expectIntRecords(
FAIL_TO_SHOW_HOME_SURFACE_UI_UMA,
FailToShowHomeSurfaceReason.NOT_A_NATIVE_PAGE)
.build();
doReturn(null).when(mNtpTab).getNativePage();
ReturnToChromeUtil.showHomeSurfaceUiOnNtp(mNtpTab, mTab1, mHomeSurfaceTracker);
histogram.assertExpected();
FrozenNativePage frozenNativePage = Mockito.mock(FrozenNativePage.class);
doReturn(true).when(frozenNativePage).isFrozen();
doReturn(frozenNativePage).when(mNtpTab).getNativePage();
histogram =
HistogramWatcher.newBuilder()
.expectIntRecords(
FAIL_TO_SHOW_HOME_SURFACE_UI_UMA,
FailToShowHomeSurfaceReason.NOT_A_NTP_NATIVE_PAGE)
.expectIntRecords(
FAIL_TO_SHOW_HOME_SURFACE_UI_UMA,
FailToShowHomeSurfaceReason.NATIVE_PAGE_IS_FROZEN)
.build();
ReturnToChromeUtil.showHomeSurfaceUiOnNtp(mNtpTab, mTab1, mHomeSurfaceTracker);
histogram.assertExpected();
}
private void setupAndVerifyTablets() {
doReturn(mResources).when(mContext).getResources();
doReturn(DeviceFormFactor.SCREEN_BUCKET_TABLET)
.when(mResources)
.getInteger(org.chromium.ui.R.integer.min_screen_width_bucket);
assertTrue(DeviceFormFactor.isNonMultiDisplayContextOnTablet(mContext));
}
private Intent createMainIntentFromLauncher() {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
assertTrue(IntentUtils.isMainIntentFromLauncher(intent));
return intent;
}
}