// Copyright 2015 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;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE;
import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;
import android.content.pm.ActivityInfo;
import android.graphics.Point;
import android.os.SystemClock;
import android.util.DisplayMetrics;
import android.view.View;
import androidx.test.espresso.Espresso;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.ApplicationTestUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisableIf;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Restriction;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.hub.HubFieldTrial;
import org.chromium.chrome.browser.layouts.LayoutTestUtils;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.layouts.animation.CompositorAnimationHandler;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabClosureParams;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorImpl;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.tabpersistence.TabStateDirectory;
import org.chromium.chrome.browser.tabpersistence.TabStateFileManager;
import org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.NewTabPageTestUtils;
import org.chromium.components.javascript_dialogs.JavascriptTabModalDialog;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.content_public.browser.test.util.DOMUtils;
import org.chromium.content_public.browser.test.util.JavaScriptUtils;
import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.content_public.browser.test.util.UiUtils;
import org.chromium.content_public.common.ContentSwitches;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.UiDisableIf;
import org.chromium.ui.test.util.UiRestriction;
import java.io.File;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
/** General Tab tests. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@DoNotBatch(
reason =
"https://crbug.com/1347598: Side effects are causing flakes in CI and failures"
+ " locally. Unbatched to isolate flakes before batching again.")
public class TabsTest {
@ClassRule
public static ChromeTabbedActivityTestRule sActivityTestRule =
new ChromeTabbedActivityTestRule();
@Rule
public BlankCTATabInitialStateRule mBlankCTATabInitialStateRule =
new BlankCTATabInitialStateRule(sActivityTestRule, false);
private static final String TEST_FILE_PATH =
"/chrome/test/data/android/tabstest/tabs_test.html";
private static final String TEST_PAGE_FILE_PATH = "/chrome/test/data/google/google.html";
private boolean mNotifyChangedCalled;
private static final long WAIT_RESIZE_TIMEOUT_MS = 3000;
private static final String INITIAL_SIZE_TEST_URL =
UrlUtils.encodeHtmlDataUri(
"<html><head><meta name=\"viewport\" content=\"width=device-width\">"
+ "<script>"
+ " document.writeln(window.innerWidth + ',' + window.innerHeight);"
+ "</script></head>"
+ "<body>"
+ "</body></html>");
private static final String RESIZE_TEST_URL =
UrlUtils.encodeHtmlDataUri(
"<html><head><script>"
+ " var resizeHappened = false;"
+ " function onResize() {"
+ " resizeHappened = true;"
+ " document.getElementById('test').textContent ="
+ " window.innerWidth + 'x' + window.innerHeight;"
+ " }"
+ "</script></head>"
+ "<body onresize=\"onResize()\">"
+ " <div id=\"test\">No resize event has been received yet.</div>"
+ "</body></html>");
@Before
public void setUp() throws InterruptedException {
CompositorAnimationHandler.setTestingMode(true);
}
@After
public void tearDown() {
sActivityTestRule
.getActivity()
.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
private String getUrl(String filePath) {
return sActivityTestRule.getTestServer().getURL(filePath);
}
/** Verify that spawning a popup from a background tab in a different model works properly. */
@Test
@LargeTest
@Feature({"Navigation"})
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
@CommandLineFlags.Add(ContentSwitches.DISABLE_POPUP_BLOCKING)
public void testSpawnPopupOnBackgroundTab() {
sActivityTestRule.loadUrl(getUrl(TEST_FILE_PATH));
final Tab tab = sActivityTestRule.getActivity().getActivityTab();
sActivityTestRule.newIncognitoTabFromMenu();
ThreadUtils.runOnUiThreadBlocking(
() ->
tab.getWebContents()
.evaluateJavaScriptForTests(
"(function() {"
+ " window.open('www.google.com');"
+ "})()",
null));
CriteriaHelper.pollUiThread(
() -> {
int tabCount =
sActivityTestRule
.getActivity()
.getTabModelSelector()
.getModel(false)
.getCount();
Criteria.checkThat(tabCount, Matchers.is(2));
});
}
@Test
@MediumTest
public void testAlertDialogDoesNotChangeActiveModel() {
sActivityTestRule.newIncognitoTabFromMenu();
sActivityTestRule.loadUrl(getUrl(TEST_FILE_PATH));
final Tab tab = sActivityTestRule.getActivity().getActivityTab();
ThreadUtils.runOnUiThreadBlocking(
() ->
tab.getWebContents()
.evaluateJavaScriptForTests(
"(function() {" + " alert('hi');" + "})()", null));
final AtomicReference<JavascriptTabModalDialog> dialog = new AtomicReference<>();
CriteriaHelper.pollInstrumentationThread(
() -> {
dialog.set(getCurrentAlertDialog());
Criteria.checkThat(dialog.get(), Matchers.notNullValue());
});
onView(withId(R.id.positive_button)).perform(click());
dialog.set(null);
CriteriaHelper.pollInstrumentationThread(
() -> Criteria.checkThat(getCurrentAlertDialog(), Matchers.nullValue()));
Assert.assertTrue(
"Incognito model was not selected",
sActivityTestRule.getActivity().getTabModelSelector().isIncognitoSelected());
}
/** Verify New Tab Open and Close Event not from the context menu. */
@Test
@LargeTest
@Feature({"Android-TabSwitcher"})
@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
public void testOpenAndCloseNewTabButton() {
sActivityTestRule.loadUrl(getUrl(TEST_FILE_PATH));
Tab tab0 =
ThreadUtils.runOnUiThreadBlocking(
() -> {
return sActivityTestRule.getActivity().getCurrentTabModel().getTabAt(0);
});
Assert.assertEquals("Data file for TabsTest", ChromeTabUtils.getTitleOnUiThread(tab0));
final int originalTabCount =
ThreadUtils.runOnUiThreadBlocking(
() -> {
return sActivityTestRule.getActivity().getCurrentTabModel().getCount();
});
onViewWaiting(withId(R.id.tab_switcher_button))
.check(matches(isDisplayed()))
.perform(click());
LayoutTestUtils.waitForLayout(
sActivityTestRule.getActivity().getLayoutManager(), LayoutType.TAB_SWITCHER);
int newTabButtonId =
HubFieldTrial.usesFloatActionButton()
? R.id.host_action_button
: R.id.toolbar_action_button;
onViewWaiting(withId(newTabButtonId)).check(matches(isDisplayed())).perform(click());
LayoutTestUtils.waitForLayout(
sActivityTestRule.getActivity().getLayoutManager(), LayoutType.BROWSING);
int currentTabCount =
ThreadUtils.runOnUiThreadBlocking(
() -> {
return sActivityTestRule.getActivity().getCurrentTabModel().getCount();
});
Assert.assertEquals(
"The tab count should increase by one", originalTabCount + 1, currentTabCount);
CriteriaHelper.pollUiThread(
() -> {
Tab tab1 = sActivityTestRule.getActivity().getCurrentTabModel().getTabAt(1);
String title = tab1.getTitle().toLowerCase(Locale.US);
String expectedTitle = "new tab";
Criteria.checkThat(title, Matchers.startsWith(expectedTitle));
});
ChromeTabUtils.closeCurrentTab(
InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
currentTabCount =
ThreadUtils.runOnUiThreadBlocking(
() -> {
return sActivityTestRule.getActivity().getCurrentTabModel().getCount();
});
Assert.assertEquals(
"The tab count should be same as original", originalTabCount, currentTabCount);
}
private void assertWaitForKeyboardStatus(final boolean show) {
CriteriaHelper.pollUiThread(
() -> {
boolean isKeyboardShowing =
sActivityTestRule
.getKeyboardDelegate()
.isKeyboardShowing(
sActivityTestRule.getActivity(),
sActivityTestRule.getActivity().getTabsView());
Criteria.checkThat(isKeyboardShowing, Matchers.is(show));
});
}
/**
* Verify that opening a new tab, switching to an existing tab and closing current tab hide
* keyboard.
*/
@Test
@LargeTest
@Restriction(UiRestriction.RESTRICTION_TYPE_TABLET)
@Feature({"Android-TabSwitcher"})
@DisableIf.Device(type = {UiDisableIf.TABLET}) // crbug.com/353910783
public void testHideKeyboard() throws Exception {
// Open a new tab(The 1st tab) and click node.
sActivityTestRule.loadUrlInNewTab(getUrl(TEST_FILE_PATH), false);
Assert.assertEquals(
"Failed to click node.",
true,
DOMUtils.clickNode(sActivityTestRule.getWebContents(), "input_text"));
assertWaitForKeyboardStatus(true);
// Open a new tab(the 2nd tab).
sActivityTestRule.loadUrlInNewTab(getUrl(TEST_FILE_PATH), false);
assertWaitForKeyboardStatus(false);
// Click node in the 2nd tab.
DOMUtils.clickNode(sActivityTestRule.getWebContents(), "input_text");
assertWaitForKeyboardStatus(true);
// Switch to the 1st tab.
ChromeTabUtils.switchTabInCurrentTabModel(sActivityTestRule.getActivity(), 1);
assertWaitForKeyboardStatus(false);
// Click node in the 1st tab.
DOMUtils.clickNode(sActivityTestRule.getWebContents(), "input_text");
assertWaitForKeyboardStatus(true);
// Close current tab(the 1st tab).
ChromeTabUtils.closeCurrentTab(
InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
assertWaitForKeyboardStatus(false);
}
/** Verify that opening a new window hides keyboard. */
@Test
@MediumTest
@Feature({"Android-TabSwitcher"})
@DisabledTest(message = "https://crbug.com/329064612")
public void testHideKeyboardWhenOpeningWindow() throws Exception {
// Open a new tab and click an editable node.
ChromeTabUtils.fullyLoadUrlInNewTab(
InstrumentationRegistry.getInstrumentation(),
sActivityTestRule.getActivity(),
getUrl(TEST_FILE_PATH),
false);
Assert.assertEquals(
"Failed to click textarea.",
true,
DOMUtils.clickNode(sActivityTestRule.getWebContents(), "textarea"));
assertWaitForKeyboardStatus(true);
// Click the button to open a new window.
Assert.assertEquals(
"Failed to click button.",
true,
DOMUtils.clickNode(sActivityTestRule.getWebContents(), "button"));
assertWaitForKeyboardStatus(false);
}
private void assertWaitForSelectedText(final String text) {
CriteriaHelper.pollUiThread(
() -> {
WebContents webContents = sActivityTestRule.getWebContents();
SelectionPopupController controller =
SelectionPopupController.fromWebContents(webContents);
final String actualText = controller.getSelectedText();
Criteria.checkThat(actualText, Matchers.is(text));
});
}
/**
* Generate a fling sequence from the given start/end X,Y percentages, for the given steps.
* Works in either landscape or portrait orientation.
*/
private void fling(float startX, float startY, float endX, float endY, int stepCount) {
Point size = new Point();
sActivityTestRule.getActivity().getWindowManager().getDefaultDisplay().getSize(size);
float dragStartX = size.x * startX;
float dragEndX = size.x * endX;
float dragStartY = size.y * startY;
float dragEndY = size.y * endY;
TouchCommon.performDrag(
sActivityTestRule.getActivity(),
dragStartX,
dragEndX,
dragStartY,
dragEndY,
stepCount,
250);
}
private void scrollDown() {
fling(0.f, 0.9f, 0.f, 0.1f, 100);
}
/**
* Verify that the selection is collapsed when switching to the tab-switcher mode then switching
* back. https://crbug.com/697756
*/
@Test
@MediumTest
@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
@Feature({"Android-TabSwitcher"})
public void testTabSwitcherCollapseSelection() throws Exception {
sActivityTestRule.loadUrlInNewTab(getUrl(TEST_FILE_PATH), false);
DOMUtils.longPressNode(sActivityTestRule.getWebContents(), "textarea");
assertWaitForSelectedText("helloworld");
// Switch to tab-switcher mode, switch back, and scroll page.
showOverviewWithNoAnimation();
hideOverviewWithNoAnimation();
scrollDown();
assertWaitForSelectedText("");
}
/**
* Verify that opening a new tab and navigating immediately sets a size on the newly created
* renderer. https://crbug.com/434477.
*
* @throws TimeoutException
*/
@Test
@SmallTest
public void testNewTabSetsContentViewSize() throws TimeoutException {
ChromeTabUtils.newTabFromMenu(
InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
// Make sure we're on the NTP
Tab tab = sActivityTestRule.getActivity().getActivityTab();
NewTabPageTestUtils.waitForNtpLoaded(tab);
sActivityTestRule.loadUrl(INITIAL_SIZE_TEST_URL);
final WebContents webContents = tab.getWebContents();
String innerText =
JavaScriptUtils.executeJavaScriptAndWaitForResult(
webContents, "document.body.innerText")
.replace("\"", "");
DisplayMetrics metrics = sActivityTestRule.getActivity().getResources().getDisplayMetrics();
// For non-integer pixel ratios like the N7v1 (1.333...), the layout system will actually
// ceil the width.
int expectedWidth = (int) Math.ceil(metrics.widthPixels / metrics.density);
String[] nums = innerText.split(",");
Assert.assertTrue(nums.length == 2);
int innerWidth = Integer.parseInt(nums[0]);
int innerHeight = Integer.parseInt(nums[1]);
Assert.assertEquals(expectedWidth, innerWidth);
// Height can be affected by browser controls so just make sure it's non-0.
Assert.assertTrue("innerHeight was not set by page load time", innerHeight > 0);
}
/** Enters the tab switcher without animation. */
private void showOverviewWithNoAnimation() {
LayoutTestUtils.startShowingAndWaitForLayout(
sActivityTestRule.getActivity().getLayoutManager(), LayoutType.TAB_SWITCHER, false);
}
/** Exits the tab switcher without animation. */
private void hideOverviewWithNoAnimation() {
LayoutTestUtils.startShowingAndWaitForLayout(
sActivityTestRule.getActivity().getLayoutManager(), LayoutType.BROWSING, false);
}
/** Test that we can safely close a tab during a fling (http://b/issue?id=5364043) */
@Test
@SmallTest
@Feature({"Android-TabSwitcher"})
public void testCloseTabDuringFling() {
sActivityTestRule.loadUrlInNewTab(
getUrl("/chrome/test/data/android/tabstest/text_page.html"));
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
WebContents webContents = sActivityTestRule.getWebContents();
webContents
.getEventForwarder()
.startFling(SystemClock.uptimeMillis(), 0, -2000, false, true);
});
ChromeTabUtils.closeCurrentTab(
InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
}
@Test
@MediumTest
@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
@DisabledTest(message = "https://crbug.com/1347598")
public void testQuickSwitchBetweenTabAndSwitcherMode() {
final String[] urls = {
getUrl("/chrome/test/data/android/navigate/one.html"),
getUrl("/chrome/test/data/android/navigate/two.html"),
getUrl("/chrome/test/data/android/navigate/three.html")
};
for (String url : urls) {
sActivityTestRule.loadUrlInNewTab(url, false);
}
final int lastUrlIndex = urls.length - 1;
ChromeTabbedActivity cta = sActivityTestRule.getActivity();
View button = sActivityTestRule.getActivity().findViewById(R.id.tab_switcher_button);
Assert.assertNotNull("Could not find 'tab_switcher_button'", button);
for (int i = 0; i < 15; i++) {
// Wait for UI to show so the back press will apply to the switcher not the tab.
TabUiTestHelper.enterTabSwitcher(cta);
// Switch back to the tab view from the tab-switcher mode.
Espresso.pressBack();
Assert.assertEquals(
"URL mismatch after switching back to the tab from tab-switch mode",
urls[lastUrlIndex],
ChromeTabUtils.getUrlStringOnUiThread(
sActivityTestRule.getActivity().getActivityTab()));
}
}
/** Open an incognito tab from menu and verify its property. */
@Test
@MediumTest
@Feature({"Android-TabSwitcher"})
public void testOpenIncognitoTab() {
sActivityTestRule.newIncognitoTabFromMenu();
Assert.assertTrue(
"Current Tab should be an incognito tab.",
sActivityTestRule.getActivity().getActivityTab().isIncognito());
}
/** Test that orientation changes cause the live tab reflow. */
@Test
@MediumTest
@Feature({"Android-TabSwitcher"})
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testOrientationChangeCausesLiveTabReflowInNormalView()
throws InterruptedException, TimeoutException {
sActivityTestRule
.getActivity()
.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
ChromeTabUtils.newTabFromMenu(
InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
sActivityTestRule.loadUrl(RESIZE_TEST_URL);
final WebContents webContents = sActivityTestRule.getWebContents();
JavaScriptUtils.executeJavaScriptAndWaitForResult(
sActivityTestRule.getWebContents(), "resizeHappened = false;");
sActivityTestRule
.getActivity()
.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
UiUtils.settleDownUI(InstrumentationRegistry.getInstrumentation());
Assert.assertEquals(
"onresize event wasn't received by the tab (normal view)",
"true",
JavaScriptUtils.executeJavaScriptAndWaitForResult(
webContents,
"resizeHappened",
WAIT_RESIZE_TIMEOUT_MS,
TimeUnit.MILLISECONDS));
}
@Test
@MediumTest
@Feature({"Android-TabSwitcher"})
public void testLastClosedUndoableTabGetsHidden() {
final TabModel model =
sActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
final Tab tab = TabModelUtils.getCurrentTab(model);
Assert.assertEquals("Too many tabs at startup", 1, model.getCount());
ThreadUtils.runOnUiThreadBlocking(
(Runnable) () -> model.closeTabs(TabClosureParams.closeTab(tab).build()));
ThreadUtils.runOnUiThreadBlocking(
() -> {
Assert.assertTrue(
"Tab close is not undoable", model.isClosurePending(tab.getId()));
Assert.assertTrue("Tab was not hidden", tab.isHidden());
});
}
private static class FocusListener implements View.OnFocusChangeListener {
private View mView;
private int mTimesFocused;
private int mTimesUnfocused;
FocusListener(View view) {
mView = view;
}
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (v != mView) return;
if (hasFocus) {
mTimesFocused++;
} else {
mTimesUnfocused++;
}
}
int getTimesFocused() {
return mTimesFocused;
}
int getTimesUnfocused() {
return mTimesUnfocused;
}
boolean hasFocus() {
return ThreadUtils.runOnUiThreadBlocking(
() -> {
return mView.hasFocus();
});
}
}
// Regression test for https://crbug.com/1394372.
@Test
@MediumTest
@Restriction({UiRestriction.RESTRICTION_TYPE_PHONE, RESTRICTION_TYPE_NON_LOW_END_DEVICE})
@Feature({"Android-TabSwitcher"})
public void testRequestFocusOnCloseTab() throws Exception {
final View urlBar = sActivityTestRule.getActivity().findViewById(R.id.url_bar);
final TabModel model =
sActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
final Tab oldTab = TabModelUtils.getCurrentTab(model);
Assert.assertNotNull("Tab should have a view", oldTab.getView());
final FocusListener focusListener = new FocusListener(oldTab.getView());
ThreadUtils.runOnUiThreadBlocking(
() -> {
oldTab.getView().setOnFocusChangeListener(focusListener);
});
Assert.assertEquals(
"oldTab should not have been focused.", 0, focusListener.getTimesFocused());
Assert.assertEquals(
"oldTab should not have been unfocused.", 0, focusListener.getTimesUnfocused());
Assert.assertTrue("oldTab should have focus.", focusListener.hasFocus());
final Tab newTab =
ChromeTabUtils.fullyLoadUrlInNewTab(
InstrumentationRegistry.getInstrumentation(),
sActivityTestRule.getActivity(),
"about:blank",
false);
Assert.assertEquals(
"oldTab should not have been focused.", 0, focusListener.getTimesFocused());
Assert.assertEquals(
"oldTab should have been unfocused.", 1, focusListener.getTimesUnfocused());
Assert.assertFalse("oldTab should not have focus", focusListener.hasFocus());
ThreadUtils.runOnUiThreadBlocking(
() -> {
model.closeTabs(TabClosureParams.closeTab(newTab).build());
});
Assert.assertEquals("oldTab should have been focused.", 1, focusListener.getTimesFocused());
Assert.assertEquals(
"oldTab should not have been unfocused again.",
1,
focusListener.getTimesUnfocused());
Assert.assertTrue("oldTab should have focus.", focusListener.hasFocus());
// Focus on the URL bar.
UiUtils.settleDownUI(InstrumentationRegistry.getInstrumentation());
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> urlBar.requestFocus());
UiUtils.settleDownUI(InstrumentationRegistry.getInstrumentation());
Assert.assertEquals(
"oldTab should not have been focused again.", 1, focusListener.getTimesFocused());
Assert.assertEquals(
"oldTab should have been unfocused by url bar.",
2,
focusListener.getTimesUnfocused());
Assert.assertFalse("oldTab should not have focus.", focusListener.hasFocus());
Assert.assertTrue(
"Keyboard should show",
sActivityTestRule
.getKeyboardDelegate()
.isKeyboardShowing(sActivityTestRule.getActivity(), urlBar));
// Check refocus doesn't happen again on the closure being finalized.
ThreadUtils.runOnUiThreadBlocking(() -> model.commitAllTabClosures());
Assert.assertEquals(
"oldTab should not have been focused again after committing tab closures.",
1,
focusListener.getTimesFocused());
Assert.assertEquals(
"oldTab should not have been unfocused again after committing tab closures.",
2,
focusListener.getTimesUnfocused());
Assert.assertFalse("oldTab should remain unfocused.", focusListener.hasFocus());
Assert.assertTrue(
"Keyboard should show",
sActivityTestRule
.getKeyboardDelegate()
.isKeyboardShowing(sActivityTestRule.getActivity(), urlBar));
// Ensure the keyboard is hidden so we are in a clean-slate for next test.
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> urlBar.clearFocus());
UiUtils.settleDownUI(InstrumentationRegistry.getInstrumentation());
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(() -> oldTab.getView().requestFocus());
UiUtils.settleDownUI(InstrumentationRegistry.getInstrumentation());
Assert.assertFalse(
"Keyboard should no longer show",
sActivityTestRule
.getKeyboardDelegate()
.isKeyboardShowing(sActivityTestRule.getActivity(), urlBar));
}
@Test
@MediumTest
@Restriction({UiRestriction.RESTRICTION_TYPE_PHONE, RESTRICTION_TYPE_NON_LOW_END_DEVICE})
@Feature({"Android-TabSwitcher"})
public void testRequestFocusOnSwitchTab() {
final TabModel model =
sActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
final Tab oldTab = TabModelUtils.getCurrentTab(model);
Assert.assertNotNull("Tab should have a view", oldTab.getView());
final FocusListener oldTabFocusListener = new FocusListener(oldTab.getView());
ThreadUtils.runOnUiThreadBlocking(
() -> {
oldTab.getView().setOnFocusChangeListener(oldTabFocusListener);
});
Assert.assertEquals(
"oldTab should not have been focused.", 0, oldTabFocusListener.getTimesFocused());
Assert.assertEquals(
"oldTab should not have been unfocused.",
0,
oldTabFocusListener.getTimesUnfocused());
Assert.assertTrue("oldTab should have focus.", oldTabFocusListener.hasFocus());
final Tab newTab =
ChromeTabUtils.fullyLoadUrlInNewTab(
InstrumentationRegistry.getInstrumentation(),
sActivityTestRule.getActivity(),
"about:blank",
false);
final FocusListener newTabFocusListener = new FocusListener(newTab.getView());
ThreadUtils.runOnUiThreadBlocking(
() -> {
newTab.getView().setOnFocusChangeListener(newTabFocusListener);
});
Assert.assertEquals(
"newTab should not have been focused.", 0, newTabFocusListener.getTimesFocused());
Assert.assertEquals(
"newTab should not have been unfocused.",
0,
newTabFocusListener.getTimesUnfocused());
Assert.assertTrue("newTab should have focus.", newTabFocusListener.hasFocus());
Assert.assertEquals(
"oldTab should not have been focused.", 0, oldTabFocusListener.getTimesFocused());
Assert.assertEquals(
"oldTab should have been unfocused.", 1, oldTabFocusListener.getTimesUnfocused());
Assert.assertFalse("oldTab should not have focus.", oldTabFocusListener.hasFocus());
ThreadUtils.runOnUiThreadBlocking(
() -> {
model.setIndex(model.indexOf(oldTab), TabSelectionType.FROM_USER);
});
Assert.assertEquals(
"newTab should not have been focused.", 0, newTabFocusListener.getTimesFocused());
Assert.assertEquals(
"newTab should have been unfocused.", 1, newTabFocusListener.getTimesUnfocused());
Assert.assertFalse("newTab should not have focus.", newTabFocusListener.hasFocus());
Assert.assertEquals(
"oldTab should have been focused.", 1, oldTabFocusListener.getTimesFocused());
Assert.assertEquals(
"oldTab should not have been unfocused again.",
1,
oldTabFocusListener.getTimesUnfocused());
Assert.assertTrue("oldTab should have focus.", oldTabFocusListener.hasFocus());
}
@Test
@MediumTest
@Feature({"Android-TabSwitcher"})
public void testLastClosedTabTriggersNotifyChangedCall() {
final TabModel model =
sActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
final Tab tab = TabModelUtils.getCurrentTab(model);
final TabModelSelector selector = sActivityTestRule.getActivity().getTabModelSelector();
mNotifyChangedCalled = false;
ThreadUtils.runOnUiThreadBlocking(
() -> {
selector.addObserver(
new TabModelSelectorObserver() {
@Override
public void onChange() {
mNotifyChangedCalled = true;
}
});
});
Assert.assertEquals("Too many tabs at startup", 1, model.getCount());
ThreadUtils.runOnUiThreadBlocking(
(Runnable) () -> model.closeTabs(TabClosureParams.closeTab(tab).build()));
Assert.assertTrue("notifyChanged() was not called", mNotifyChangedCalled);
}
@Test
@MediumTest
@Feature({"Android-TabSwitcher"})
public void testTabsAreDestroyedOnModelDestruction() throws Exception {
final TabModelSelectorImpl selector =
(TabModelSelectorImpl) sActivityTestRule.getActivity().getTabModelSelector();
final Tab tab = sActivityTestRule.getActivity().getActivityTab();
final CallbackHelper webContentsDestroyed = new CallbackHelper();
ThreadUtils.runOnUiThreadBlocking(
() -> {
@SuppressWarnings("unused") // Avoid GC of observer
WebContentsObserver observer =
new WebContentsObserver(tab.getWebContents()) {
@Override
public void destroy() {
super.destroy();
webContentsDestroyed.notifyCalled();
}
};
Assert.assertNotNull("No initial tab at startup", tab);
Assert.assertNotNull("Tab does not have a web contents", tab.getWebContents());
Assert.assertTrue("Tab is destroyed", tab.isInitialized());
});
ApplicationTestUtils.finishActivity(sActivityTestRule.getActivity());
ThreadUtils.runOnUiThreadBlocking(
() -> {
Assert.assertNull("Tab still has a web contents", tab.getWebContents());
Assert.assertFalse("Tab was not destroyed", tab.isInitialized());
});
webContentsDestroyed.waitForOnly();
}
@Test
@MediumTest
@Feature({"Android-TabSwitcher"})
@DisableFeatures(ChromeFeatureList.ANDROID_TAB_DECLUTTER)
@DisabledTest(message = "https://crbug.com/361535551")
public void testIncognitoTabsNotRestoredAfterSwipe() throws Exception {
sActivityTestRule.loadUrl(getUrl(TEST_PAGE_FILE_PATH));
sActivityTestRule.newIncognitoTabFromMenu();
// Tab states are not saved for empty NTP tabs, so navigate to any page to trigger a file
// to be saved.
sActivityTestRule.loadUrl(getUrl(TEST_PAGE_FILE_PATH));
File tabStateDir = TabStateDirectory.getOrCreateTabbedModeStateDirectory();
TabModel normalModel =
sActivityTestRule.getActivity().getTabModelSelector().getModel(false);
TabModel incognitoModel =
sActivityTestRule.getActivity().getTabModelSelector().getModel(true);
File normalTabFile =
new File(
tabStateDir,
TabStateFileManager.getTabStateFilename(
normalModel.getTabAt(normalModel.getCount() - 1).getId(),
false,
false));
File incognitoTabFile =
new File(
tabStateDir,
TabStateFileManager.getTabStateFilename(
incognitoModel.getTabAt(0).getId(), true, false
/** isFlatBuffer= */
));
assertFileExists(normalTabFile, true);
assertFileExists(incognitoTabFile, true);
// Although we're destroying the activity, the Application will still live on since its in
// the same process as this test.
ApplicationTestUtils.finishActivity(sActivityTestRule.getActivity());
// Activity will be started without a savedInstanceState.
sActivityTestRule.startMainActivityOnBlankPage();
assertFileExists(normalTabFile, true);
assertFileExists(incognitoTabFile, false);
}
@Test
@MediumTest
public void testTabModelSelectorCloseTabInUndoableState() {
ChromeTabUtils.newTabFromMenu(
InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
TabModelSelectorImpl selector =
(TabModelSelectorImpl) sActivityTestRule.getActivity().getTabModelSelector();
Tab tab = sActivityTestRule.getActivity().getActivityTab();
// Start undoable tab closure.
ThreadUtils.runOnUiThreadBlocking(
() -> {
Assert.assertFalse(tab.isClosing());
Assert.assertFalse(tab.isDestroyed());
selector.getModel(/* incognito= */ false)
.closeTabs(TabClosureParams.closeTab(tab).allowUndo(true).build());
Assert.assertTrue(tab.isClosing());
Assert.assertFalse(tab.isDestroyed());
});
// Later something calls `TabModelSelector#closeTab`.
ThreadUtils.runOnUiThreadBlocking(
() -> {
Assert.assertTrue(tab.isClosing());
Assert.assertFalse(tab.isDestroyed());
// Prior to fixing crbug.com/40067160 this would assert as the tab could not be
// found in
// any model as it was in the undoable tab closure state.
selector.closeTab(tab);
Assert.assertTrue(tab.isClosing());
Assert.assertTrue(tab.isDestroyed());
});
}
private void assertFileExists(final File fileToCheck, final boolean expected) {
CriteriaHelper.pollInstrumentationThread(
() -> Criteria.checkThat(fileToCheck.exists(), Matchers.is(expected)));
}
private JavascriptTabModalDialog getCurrentAlertDialog() {
return (JavascriptTabModalDialog)
ThreadUtils.runOnUiThreadBlocking(
() -> {
PropertyModel dialogModel =
sActivityTestRule
.getActivity()
.getModalDialogManager()
.getCurrentDialogForTest();
return dialogModel != null
? dialogModel.get(ModalDialogProperties.CONTROLLER)
: null;
});
}
}