chromium/chrome/android/javatests/src/org/chromium/chrome/browser/ActivityTabProviderTest.java

// 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;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;

import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.ActivityTabProvider.ActivityTabTabObserver;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.layouts.LayoutTestUtils;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.ui.test.util.UiRestriction;

import java.util.concurrent.TimeoutException;

/** Tests for {@link ChromeActivity}'s {@link ActivityTabProvider}. */
@DoNotBatch(reason = "waitForActivityCompletelyLoaded is unhappy when batched - more work needed")
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class ActivityTabProviderTest {
    /** A test observer that provides access to the tab being observed. */
    private static class TestActivityTabTabObserver extends ActivityTabTabObserver {
        /** The tab currently being observed. */
        private Tab mObservedTab;

        public TestActivityTabTabObserver(ActivityTabProvider provider) {
            super(provider);
            ThreadUtils.runOnUiThreadBlocking(() -> mObservedTab = provider.get());
        }

        @Override
        public void onObservingDifferentTab(Tab tab, boolean hint) {
            mObservedTab = tab;
        }

        @Override
        protected void updateObservedTabToCurrent() {
            ThreadUtils.runOnUiThreadBlocking(super::updateObservedTabToCurrent);
        }

        @Override
        protected void addObserverToTabSupplier() {
            ThreadUtils.runOnUiThreadBlocking(super::addObserverToTabSupplier);
        }

        @Override
        protected void removeObserverFromTabSupplier() {
            ThreadUtils.runOnUiThreadBlocking(super::removeObserverFromTabSupplier);
        }

        @Override
        public void destroy() {
            ThreadUtils.runOnUiThreadBlocking(super::destroy);
        }
    }

    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    private ChromeTabbedActivity mActivity;
    private ActivityTabProvider mProvider;
    private Tab mActivityTab;
    private CallbackHelper mActivityTabChangedHelper = new CallbackHelper();

    @Before
    public void setUp() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivity = mActivityTestRule.getActivity();
                    mProvider = mActivity.getActivityTabProvider();
                    mProvider.addObserver(
                            tab -> {
                                mActivityTab = tab;
                                mActivityTabChangedHelper.notifyCalled();
                            });
                });
        mActivityTabChangedHelper.waitForCallback(0);
        assertEquals(
                "Setup should have only triggered the event once.",
                1,
                mActivityTabChangedHelper.getCallCount());
    }

    /**
     * @return The {@link Tab} that the active model currently has selected.
     */
    private Tab getModelSelectedTab() {
        return mActivity.getTabModelSelector().getCurrentTab();
    }

    /**
     * Test that the onActivityTabChanged event is triggered when the observer is attached for only
     * that observer.
     */
    @Test
    @SmallTest
    @Feature({"ActivityTabObserver"})
    public void testTriggerOnAddObserver() throws TimeoutException {
        CallbackHelper helper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mProvider.addObserver(tab -> helper.notifyCalled());
                });
        helper.waitForCallback(0);

        assertEquals(
                "Only the added observer should have been triggered.",
                mActivityTabChangedHelper.getCallCount(),
                1);
        assertEquals(
                "The added observer should have only been triggered once.",
                1,
                helper.getCallCount());
    }

    /** Test that onActivityTabChanged is triggered when entering and exiting the tab switcher. */
    @Test
    @SmallTest
    @Feature({"ActivityTabObserver"})
    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
    public void testTriggerWithTabSwitcher() throws TimeoutException {
        assertEquals(
                "The activity tab should be the model's selected tab.",
                getModelSelectedTab(),
                mActivityTab);

        ThreadUtils.runOnUiThreadBlocking(
                () -> mActivity.getLayoutManager().showLayout(LayoutType.TAB_SWITCHER, false));
        mActivityTabChangedHelper.waitForCallback(1);
        assertEquals(
                "Entering the tab switcher should have triggered the event once.",
                2,
                mActivityTabChangedHelper.getCallCount());
        assertEquals("The activity tab should be null.", null, mActivityTab);

        LayoutTestUtils.waitForLayout(mActivity.getLayoutManager(), LayoutType.TAB_SWITCHER);

        ThreadUtils.runOnUiThreadBlocking(
                () -> mActivity.getLayoutManager().showLayout(LayoutType.BROWSING, false));
        mActivityTabChangedHelper.waitForCallback(2);
        assertEquals(
                "Exiting the tab switcher should have triggered the event once.",
                3,
                mActivityTabChangedHelper.getCallCount());
        assertEquals(
                "The activity tab should be the model's selected tab.",
                getModelSelectedTab(),
                mActivityTab);

        LayoutTestUtils.waitForLayout(mActivity.getLayoutManager(), LayoutType.BROWSING);
    }

    /**
     * Test that onActivityTabChanged is triggered when switching to a new tab without switching
     * layouts.
     */
    @Test
    @SmallTest
    @Feature({"ActivityTabObserver"})
    public void testTriggerWithTabSelection() throws TimeoutException {
        Tab startingTab = getModelSelectedTab();

        ChromeTabUtils.fullyLoadUrlInNewTab(
                InstrumentationRegistry.getInstrumentation(),
                mActivity,
                ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL,
                false);

        assertNotEquals(
                "A new tab should be in the foreground.", startingTab, getModelSelectedTab());
        assertEquals(
                "The activity tab should be the model's selected tab.",
                getModelSelectedTab(),
                mActivityTab);

        int callCount = mActivityTabChangedHelper.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Select the original tab without switching layouts.
                    mActivity
                            .getTabModelSelector()
                            .getCurrentModel()
                            .setIndex(0, TabSelectionType.FROM_USER);
                });
        mActivityTabChangedHelper.waitForCallback(callCount);

        assertEquals(
                "Switching tabs should have triggered the event once.",
                callCount + 1,
                mActivityTabChangedHelper.getCallCount());
    }

    /** Test that onActivityTabChanged is triggered when the last tab is closed. */
    @Test
    @SmallTest
    @Feature({"ActivityTabObserver"})
    public void testTriggerOnLastTabClosed() throws TimeoutException {
        // Have a tab open in incognito model. This should not be in the way getting the event
        // triggered when closing the last tab in normal mode.
        TabModelSelector selector = mActivity.getTabModelSelector();
        ThreadUtils.runOnUiThreadBlocking(() -> selector.selectModel(true));
        ChromeTabUtils.fullyLoadUrlInNewTab(
                InstrumentationRegistry.getInstrumentation(),
                mActivity,
                ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL,
                true);
        ThreadUtils.runOnUiThreadBlocking(() -> selector.selectModel(false));

        int callCount = mActivityTabChangedHelper.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    selector.closeTab(getModelSelectedTab());
                });
        mActivityTabChangedHelper.waitForCallback(callCount);

        assertEquals(
                "Closing the last tab should have triggered the event once.",
                callCount + 1,
                mActivityTabChangedHelper.getCallCount());
        assertEquals("The activity's tab should be null.", null, mActivityTab);
    }

    /**
     * Test that the correct tab is considered the activity tab when a different tab is closed on
     * phone.
     */
    @Test
    @SmallTest
    @Feature({"ActivityTabObserver"})
    public void testCorrectTabAfterTabClosed() {
        Tab startingTab = getModelSelectedTab();

        ChromeTabUtils.fullyLoadUrlInNewTab(
                InstrumentationRegistry.getInstrumentation(),
                mActivity,
                ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL,
                false);

        assertNotEquals(
                "The starting tab should not be the selected tab.",
                getModelSelectedTab(),
                startingTab);
        assertEquals(
                "The activity tab should be the model's selected tab.",
                getModelSelectedTab(),
                mActivityTab);
        Tab activityTabBefore = mActivityTab;

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivity.getTabModelSelector().closeTab(startingTab);
                });

        assertEquals("The activity tab should not have changed.", activityTabBefore, mActivityTab);
    }

    /** Test that the {@link ActivityTabTabObserver} switches between tabs as the tab changes. */
    @Test
    @SmallTest
    @Feature({"ActivityTabObserver"})
    public void testActivityTabTabObserver() throws TimeoutException {
        Tab startingTab = getModelSelectedTab();

        TestActivityTabTabObserver tabObserver = new TestActivityTabTabObserver(mProvider);

        assertEquals(
                "The observer should be attached to the starting tab.",
                startingTab,
                tabObserver.mObservedTab);

        ChromeTabUtils.fullyLoadUrlInNewTab(
                InstrumentationRegistry.getInstrumentation(),
                mActivity,
                ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL,
                false);

        assertNotEquals("The tab should have changed.", startingTab, getModelSelectedTab());
        assertEquals(
                "The observer should be attached to the new tab.",
                getModelSelectedTab(),
                tabObserver.mObservedTab);

        tabObserver.destroy();
    }
}