// Copyright 2018 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.customtabs;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
import org.robolectric.annotation.Config;
import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.test.BaseRobolectricTestRule;
import org.chromium.base.test.util.Batch;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.WebappExtras;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityNavigationController.FinishReason;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabController;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabProvider;
import org.chromium.chrome.browser.customtabs.features.minimizedcustomtab.CustomTabMinimizationManagerHolder;
import org.chromium.chrome.browser.flags.ActivityType;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.NavigationEntry;
import org.chromium.content_public.browser.NavigationHistory;
import org.chromium.content_public.browser.WebContents;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;
import java.util.Arrays;
import java.util.Collection;
import java.util.Stack;
/** Tests for {@link CloseButtonNavigator}. */
@RunWith(ParameterizedRobolectricTestRunner.class)
@Batch(Batch.UNIT_TESTS)
@Config(manifest = Config.NONE)
public class CloseButtonNavigatorTest {
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {{true}, {false}});
}
@Rule(order = -2)
public BaseRobolectricTestRule mBaseRule = new BaseRobolectricTestRule();
@Parameter(0)
public boolean mIsWebapp;
@Mock public CustomTabActivityTabController mTabController;
@Mock public CustomTabActivityTabProvider mTabProvider;
@Mock public WebappExtras mWebappExtras;
@Mock public BrowserServicesIntentDataProvider mIntentDataProvider;
@Mock public CustomTabMinimizationManagerHolder mMinimizationManagerHolder;
private final Stack<Tab> mTabs = new Stack<>();
private CloseButtonNavigator mCloseButtonNavigator;
private Callback<@FinishReason Integer> mFinishCallback;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
if (!mIsWebapp) {
mWebappExtras = null;
}
doReturn(mWebappExtras).when(mIntentDataProvider).getWebappExtras();
doReturn(mIsWebapp ? ActivityType.WEBAPP : ActivityType.CUSTOM_TAB)
.when(mIntentDataProvider)
.getActivityType();
mFinishCallback =
reason -> {
// FinishCallback is invoked only if there is a single tab left to close.
assertTrue(mTabController.onlyOneTabRemaining());
mTabController.closeTab();
};
mCloseButtonNavigator =
new CloseButtonNavigator(
mTabController,
mTabProvider,
mIntentDataProvider,
mMinimizationManagerHolder);
// Set up our mTabs to act as the mock tab model:
// - mTabController.closeTab removes the top tab.
// - mTabController.onlyOneTabRemaining tells when the last tab is being removed.
// - mTabProvider.getTab returns the top tab.
Mockito.doAnswer(
(invocation) -> {
mTabs.pop();
return null; // Annoyingly we have to return something.
})
.when(mTabController)
.closeTab();
when(mTabController.onlyOneTabRemaining()).thenAnswer(invocation -> mTabs.size() == 1);
when(mTabProvider.getTab())
.thenAnswer(
invocation -> {
if (mTabs.empty()) return null;
return mTabs.peek();
});
}
private Tab createTabWithNavigationHistory(GURL... urls) {
NavigationHistory history = new NavigationHistory();
for (GURL url : urls) {
history.addEntry(
new NavigationEntry(
0,
url,
GURL.emptyGURL(),
GURL.emptyGURL(),
"",
null,
0,
0,
/* isInitialEntry= */ false));
}
// Point to the most recent entry in history.
history.setCurrentEntryIndex(history.getEntryCount() - 1);
Tab tab = mock(Tab.class);
WebContents webContents = mock(WebContents.class);
NavigationController navigationController = mock(NavigationController.class);
when(tab.getUrl())
.thenAnswer(
invocation ->
history.getEntryAtIndex(history.getCurrentEntryIndex()).getUrl());
when(tab.getWebContents()).thenReturn(webContents);
when(webContents.getNavigationController()).thenReturn(navigationController);
when(navigationController.getNavigationHistory()).thenReturn(history);
setParentTabId(tab, Tab.INVALID_TAB_ID);
return tab;
}
private void setParentTabId(Tab childTab, int parentTabId) {
doReturn(parentTabId).when(childTab).getParentId();
}
private NavigationController currentTabsNavigationController() {
// The navigation controller will be a mock object created in the above method.
return mTabs.peek().getWebContents().getNavigationController();
}
/** Example criteria. */
private static boolean isRed(String url) {
return url.contains("red");
}
@Test
public void noCriteria_singleTab() {
mTabs.push(createTabWithNavigationHistory(JUnitTestGURLs.BLUE_1, JUnitTestGURLs.BLUE_2));
mCloseButtonNavigator.navigateOnClose(mFinishCallback);
assertTrue(mTabs.empty());
assertOnAllTabsClosedRecorded(1);
}
@Test
public void noCriteria_multipleTabs() {
mTabs.push(createTabWithNavigationHistory(JUnitTestGURLs.BLUE_1));
mTabs.push(createTabWithNavigationHistory(JUnitTestGURLs.BLUE_2));
setParentTabId(mTabs.get(1), mTabs.get(0).getId());
mCloseButtonNavigator.navigateOnClose(mFinishCallback);
if (mIsWebapp) {
assertEquals(1, mTabs.size());
verify(currentTabsNavigationController(), never()).goToNavigationIndex(anyInt());
} else {
assertTrue(mTabs.empty());
assertOnAllTabsClosedRecorded(2);
}
}
@Test
public void noMatchingUrl_singleTab() {
mCloseButtonNavigator.setLandingPageCriteria(CloseButtonNavigatorTest::isRed);
mTabs.push(createTabWithNavigationHistory(JUnitTestGURLs.BLUE_1, JUnitTestGURLs.BLUE_2));
mCloseButtonNavigator.navigateOnClose(mFinishCallback);
assertTrue(mTabs.empty());
assertOnAllTabsClosedRecorded(1);
}
@Test
public void noMatchingUrl_multipleTabs() {
mCloseButtonNavigator.setLandingPageCriteria(CloseButtonNavigatorTest::isRed);
mTabs.push(createTabWithNavigationHistory(JUnitTestGURLs.BLUE_1));
mTabs.push(createTabWithNavigationHistory(JUnitTestGURLs.BLUE_2));
setParentTabId(mTabs.get(1), mTabs.get(0).getId());
mCloseButtonNavigator.navigateOnClose(mFinishCallback);
if (mIsWebapp) {
assertEquals(1, mTabs.size());
verify(currentTabsNavigationController(), never()).goToNavigationIndex(anyInt());
} else {
assertTrue(mTabs.empty());
assertOnAllTabsClosedRecorded(2);
}
}
@Test
public void matchingUrl_singleTab() {
mCloseButtonNavigator.setLandingPageCriteria(CloseButtonNavigatorTest::isRed);
mTabs.push(
createTabWithNavigationHistory(
JUnitTestGURLs.RED_1,
JUnitTestGURLs.RED_2,
JUnitTestGURLs.BLUE_1,
JUnitTestGURLs.BLUE_2));
mCloseButtonNavigator.navigateOnClose(mFinishCallback);
assertFalse(mTabs.isEmpty());
assertOnAllTabsClosedRecorded(0);
verify(currentTabsNavigationController()).goToNavigationIndex(eq(1));
// Ensure it was only called with that value.
verify(currentTabsNavigationController()).goToNavigationIndex(anyInt());
}
@Test
public void matchingUrl_startOfNextTab() {
mCloseButtonNavigator.setLandingPageCriteria(CloseButtonNavigatorTest::isRed);
mTabs.push(createTabWithNavigationHistory(JUnitTestGURLs.RED_1, JUnitTestGURLs.RED_2));
mTabs.push(createTabWithNavigationHistory(JUnitTestGURLs.BLUE_1, JUnitTestGURLs.BLUE_2));
setParentTabId(mTabs.get(1), mTabs.get(0).getId());
mCloseButtonNavigator.navigateOnClose(mFinishCallback);
assertEquals(1, mTabs.size());
assertOnAllTabsClosedRecorded(0);
verify(currentTabsNavigationController(), never()).goToNavigationIndex(anyInt());
}
@Test
public void matchingUrl_middleOfNextTab() {
mCloseButtonNavigator.setLandingPageCriteria(CloseButtonNavigatorTest::isRed);
mTabs.push(createTabWithNavigationHistory(JUnitTestGURLs.RED_1, JUnitTestGURLs.BLUE_1));
mTabs.push(createTabWithNavigationHistory(JUnitTestGURLs.BLUE_2, JUnitTestGURLs.BLUE_3));
setParentTabId(mTabs.get(1), mTabs.get(0).getId());
mCloseButtonNavigator.navigateOnClose(mFinishCallback);
assertEquals(1, mTabs.size());
assertOnAllTabsClosedRecorded(0);
if (mIsWebapp) {
verify(currentTabsNavigationController(), never()).goToNavigationIndex(anyInt());
} else {
verify(currentTabsNavigationController()).goToNavigationIndex(eq(0));
verify(currentTabsNavigationController()).goToNavigationIndex(anyInt());
}
}
@Test
public void middleOfHistory() {
mCloseButtonNavigator.setLandingPageCriteria(CloseButtonNavigatorTest::isRed);
mTabs.push(
createTabWithNavigationHistory(
JUnitTestGURLs.RED_1,
JUnitTestGURLs.RED_2,
JUnitTestGURLs.BLUE_1,
JUnitTestGURLs.BLUE_2,
JUnitTestGURLs.RED_3));
mTabs.peek()
.getWebContents()
.getNavigationController()
.getNavigationHistory()
.setCurrentEntryIndex(3);
mCloseButtonNavigator.navigateOnClose(mFinishCallback);
assertEquals(1, mTabs.size());
assertOnAllTabsClosedRecorded(0);
verify(currentTabsNavigationController()).goToNavigationIndex(eq(1));
verify(currentTabsNavigationController()).goToNavigationIndex(anyInt());
}
@Test
public void navigateFromLandingPage() {
mCloseButtonNavigator.setLandingPageCriteria(CloseButtonNavigatorTest::isRed);
mTabs.push(
createTabWithNavigationHistory(
JUnitTestGURLs.RED_1,
JUnitTestGURLs.RED_2,
JUnitTestGURLs.BLUE_1,
JUnitTestGURLs.BLUE_2,
JUnitTestGURLs.RED_3));
mCloseButtonNavigator.navigateOnClose(mFinishCallback);
assertEquals(1, mTabs.size());
assertOnAllTabsClosedRecorded(0);
verify(currentTabsNavigationController()).goToNavigationIndex(eq(1));
verify(currentTabsNavigationController()).goToNavigationIndex(anyInt());
}
private void assertOnAllTabsClosedRecorded(int count) {
String histogram = "CustomTabs.TabCounts.OnClosingAllTabs";
if (count > 0) {
assertEquals(
String.format("<%s> not recorded with sample <%d>.", histogram, count),
1,
RecordHistogram.getHistogramValueCountForTesting(histogram, count));
} else {
assertEquals(
String.format("<%s> should not be recorded.", histogram),
0,
RecordHistogram.getHistogramTotalCountForTesting(histogram));
}
}
}