chromium/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherTabletTest.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.tasks.tab_management;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.Visibility.GONE;
import static androidx.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static androidx.test.espresso.matcher.ViewMatchers.withId;

import static org.hamcrest.Matchers.allOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import android.view.ViewGroup;
import android.view.ViewStub;

import androidx.annotation.IdRes;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;

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.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.RequiresRestart;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.compositor.layouts.Layout;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerChrome;
import org.chromium.chrome.browser.compositor.overlays.strip.StripLayoutHelperManager;
import org.chromium.chrome.browser.compositor.overlays.strip.StripLayoutTab;
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.TabLaunchType;
import org.chromium.chrome.browser.tab_ui.TabSwitcher;
import org.chromium.chrome.browser.tabmodel.TabClosureParams;
import org.chromium.chrome.browser.tabmodel.TabCreator;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
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.TabStripUtils;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.ui.test.util.UiRestriction;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

/** Tests for the {@link TabSwitcher} on tablet */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({
    ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
    "force-fieldtrials=Study/Group"
})
@Restriction({
    Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE,
    UiRestriction.RESTRICTION_TYPE_TABLET
})
@Batch(Batch.PER_CLASS)
public class TabSwitcherTabletTest {
    private static final long CALLBACK_WAIT_TIMEOUT = 15L;

    @ClassRule
    public static final ChromeTabbedActivityTestRule sActivityTestRule =
            new ChromeTabbedActivityTestRule();

    @Rule
    public final BlankCTATabInitialStateRule mInitialStateRule =
            new BlankCTATabInitialStateRule(sActivityTestRule, false);

    @Before
    public void setUp() throws ExecutionException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        CriteriaHelper.pollUiThread(cta.getTabModelSelectorSupplier().get()::isTabStateInitialized);
    }

    @After
    public void cleanup() throws TimeoutException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();

        LayoutManagerChrome layoutManager = cta.getLayoutManager();
        if (layoutManager.isLayoutVisible(LayoutType.TAB_SWITCHER)
                && !layoutManager.isLayoutStartingToHide(LayoutType.TAB_SWITCHER)) {
            TabModelSelector selector = cta.getTabModelSelectorSupplier().get();
            if (selector.getModel(false).getCount() == 0) {
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            TabCreator tabCreator = cta.getTabCreator(/* incognito= */ false);
                            return tabCreator.createNewTab(
                                    new LoadUrlParams("about:blank"),
                                    TabLaunchType.FROM_CHROME_UI,
                                    null,
                                    0);
                        });
                LayoutTestUtils.waitForLayout(layoutManager, LayoutType.BROWSING);
            } else {
                if (selector.getCurrentModel().isIncognito()) {
                    clickIncognitoToggleButton();
                }
                exitSwitcherWithTabClick(0);
            }
        } else {
            LayoutTestUtils.waitForLayout(layoutManager, LayoutType.BROWSING);
        }
    }

    @Test
    @MediumTest
    @RequiresRestart
    @DisabledTest(message = "Flaky, see crbug.com/327457591")
    public void testEnterAndExitTabSwitcher() throws TimeoutException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        checkHubLayout(cta, false);
        checkTabSwitcherViewHolderStub(cta, true);
        checkTabSwitcherViewHolder(cta, false);

        TabUiTestHelper.prepareTabsWithThumbnail(sActivityTestRule, 1, 0, null);
        TabUiTestHelper.enterTabSwitcher(cta);
        ensureHubLayout();

        checkHubLayout(cta, true);
        checkTabSwitcherViewHolderStub(cta, false);
        checkTabSwitcherViewHolder(cta, true);

        exitSwitcherWithTabClick(0);
        assertFalse(cta.getLayoutManager().isLayoutVisible(LayoutType.TAB_SWITCHER));
    }

    @Test
    @MediumTest
    public void testToggleIncognitoSwitcher() throws Exception {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        prepareTabs(1, 1);
        TabUiTestHelper.enterTabSwitcher(cta);

        // Start with incognito switcher.
        assertTrue("Expected to be in Incognito model", cta.getCurrentTabModel().isIncognito());

        // Toggle to normal switcher.
        clickIncognitoToggleButton();

        final Tab newTab = cta.getCurrentTabModel().getTabAt(0);
        assertFalse(newTab.isIncognito());

        exitSwitcherWithTabClick(0);
    }

    @Test
    @MediumTest
    public void testTabSwitcherScrim() throws TimeoutException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        prepareTabs(1, 1);
        TabUiTestHelper.enterTabSwitcher(cta);

        ScrimCoordinator scrimCoordinator =
                cta.getRootUiCoordinatorForTesting().getScrimCoordinator();
        assertTrue(scrimCoordinator.isShowingScrim());
        assertEquals(
                ChromeColors.getPrimaryBackgroundColor(cta, true),
                cta.getRootUiCoordinatorForTesting()
                        .getStatusBarColorController()
                        .getScrimColorForTesting());

        exitSwitcherWithTabClick(0);
        assertFalse(scrimCoordinator.isShowingScrim());
    }

    @Test
    @MediumTest
    public void testGridTabSwitcherOnNoNextTab_WithoutRestart() throws ExecutionException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        ensureHubLayout();

        checkHubLayout(cta, /* isInitialized= */ true);
        checkTabSwitcherViewHolderStub(cta, /* exists= */ false);
        checkTabSwitcherViewHolder(cta, /* exists= */ true);

        // Assert the grid tab switcher is not yet showing.
        checkTabSwitcherViewHolderVisibility(cta, false);

        // Close the only tab through the tab strip.
        closeTab(false, sActivityTestRule.getActivity().getCurrentTabModel().getTabAt(0).getId());

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

        // Assert the grid tab switcher is shown automatically, since there is no next tab.
        checkTabSwitcherViewHolderVisibility(cta, true);
    }

    @Test
    @MediumTest
    @RequiresRestart
    @DisabledTest(message = "crbug.com/342983248")
    public void testGridTabSwitcherOnNoNextTab_WithRestart() throws ExecutionException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        checkHubLayout(cta, /* isInitialized= */ false);
        checkTabSwitcherViewHolderStub(cta, /* exists= */ true);
        checkTabSwitcherViewHolder(cta, /* exists= */ false);

        // Close the only tab through the tab strip.
        closeTab(false, sActivityTestRule.getActivity().getCurrentTabModel().getTabAt(0).getId());

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

        // Assert the grid tab switcher is shown automatically, since there is no next tab.
        checkTabSwitcherViewHolderVisibility(cta, true);

        checkHubLayout(cta, /* isInitialized= */ true);
        checkTabSwitcherViewHolderStub(cta, /* exists= */ false);
        checkTabSwitcherViewHolder(cta, /* exists= */ true);
    }

    @Test
    @MediumTest
    public void testGridTabSwitcherOnCloseAllTabs_WithoutRestart() throws ExecutionException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        ensureHubLayout();

        checkHubLayout(cta, /* isInitialized= */ true);
        checkTabSwitcherViewHolderStub(cta, /* exists= */ false);
        checkTabSwitcherViewHolder(cta, /* exists= */ true);

        // Assert the grid tab switcher is not yet showing.
        checkTabSwitcherViewHolderVisibility(cta, false);

        // Close all tabs.
        ChromeTabUtils.closeAllTabs(
                InstrumentationRegistry.getInstrumentation(), cta.getTabModelSelectorSupplier());

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

        // Assert the grid tab switcher is shown automatically, since there is no next tab.
        checkTabSwitcherViewHolderVisibility(cta, true);
    }

    @Test
    @MediumTest
    public void testGridTabSwitcherToggleIncognitoWithNoRegularTab() throws ExecutionException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        TabModel regularModel = cta.getTabModelSelectorSupplier().get().getModel(false);

        // Open an incognito tab.
        prepareTabs(1, 1);

        // Close all the regular tabs.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    regularModel.closeTabs(
                            TabClosureParams.closeTab(regularModel.getTabAt(0))
                                    .allowUndo(false)
                                    .build());
                });
        assertEquals("Expected to be 0 tabs in regular model", 0, regularModel.getCount());
        assertTrue("Expected to be in Incognito model", cta.getCurrentTabModel().isIncognito());

        // Assert the grid tab switcher is not yet showing.
        checkTabSwitcherViewHolderVisibility(cta, false);

        TabUiTestHelper.enterTabSwitcher(cta);

        // Toggle to normal switcher.
        clickIncognitoToggleButton();

        checkTabSwitcherViewHolderVisibility(cta, true);
    }

    // Regression test for crbug.com/1487114.
    @Test
    @MediumTest
    @RequiresRestart
    public void testGridTabSwitcher_DeferredHubLayoutCreation() throws ExecutionException {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        prepareTabs(2, 0);
        // Verifies that the dialog visibility supplier doesn't crash when closing a Tab without the
        // grid tab switcher is inflated.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    cta.getCurrentTabModel()
                            .closeTabs(
                                    TabClosureParams.closeTab(cta.getActivityTab())
                                            .allowUndo(false)
                                            .build());
                });

        checkHubLayout(cta, /* isInitialized= */ false);
        checkTabSwitcherViewHolderStub(cta, /* exists= */ true);
        checkTabSwitcherViewHolder(cta, /* exists= */ false);

        // Click tab switcher button
        TabUiTestHelper.enterTabSwitcher(sActivityTestRule.getActivity());

        checkHubLayout(cta, /* isInitialized= */ true);
        checkTabSwitcherViewHolderStub(cta, /* exists= */ false);
        checkTabSwitcherViewHolder(cta, /* exists= */ true);
    }

    @Test
    @MediumTest
    @DisabledTest(message = "crbug.com/342983248")
    public void testEmptyStateView() throws Exception {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        prepareTabs(1, 0);
        TabUiTestHelper.enterTabSwitcher(cta);

        // Close the last tab.
        closeTab(false, cta.getCurrentTabModel().getTabAt(0).getId());

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

        // Check whether empty view show up.
        @IdRes int tabSwitcherAncestorId = TabUiTestHelper.getTabSwitcherAncestorId(cta);
        onView(
                        allOf(
                                withId(R.id.empty_state_container),
                                isDescendantOfA(withId(tabSwitcherAncestorId))))
                .check(matches(isDisplayed()));
    }

    @Test
    @MediumTest
    public void testEmptyStateView_ToggleIncognito() throws Exception {
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        prepareTabs(1, 1);
        TabUiTestHelper.enterTabSwitcher(sActivityTestRule.getActivity());

        // Close the last normal tab.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    TabModel model = cta.getTabModelSelector().getModel(false);
                    model.closeTabs(
                            TabClosureParams.closeTab(model.getTabAt(0)).allowUndo(false).build());
                });

        // Check empty view should never show up in incognito tab switcher.
        @IdRes int tabSwitcherAncestorId = TabUiTestHelper.getTabSwitcherAncestorId(cta);
        onView(
                        allOf(
                                withId(R.id.empty_state_container),
                                isDescendantOfA(withId(tabSwitcherAncestorId))))
                .check(doesNotExist());

        // Close the last incognito tab.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    TabModel model = cta.getTabModelSelector().getModel(true);
                    model.closeTabs(
                            TabClosureParams.closeTab(model.getTabAt(0)).allowUndo(false).build());
                });

        // Incognito tab switcher should exit to go to normal tab switcher and we should see empty
        // view.
        onView(
                        allOf(
                                withId(R.id.empty_state_container),
                                isDescendantOfA(withId(tabSwitcherAncestorId))))
                .check(matches(isDisplayed()));
    }

    protected void clickIncognitoToggleButton() {
        final CallbackHelper tabModelSelectedCallback = new CallbackHelper();
        Callback<TabModel> observer =
                (tabModel) -> {
                    tabModelSelectedCallback.notifyCalled();
                };
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        sActivityTestRule
                                .getActivity()
                                .getTabModelSelectorSupplier()
                                .get()
                                .getCurrentTabModelSupplier()
                                .addObserver(observer));
        StripLayoutHelperManager manager =
                TabStripUtils.getStripLayoutHelperManager(sActivityTestRule.getActivity());
        TabStripUtils.clickCompositorButton(
                manager.getModelSelectorButton(),
                InstrumentationRegistry.getInstrumentation(),
                sActivityTestRule.getActivity());
        try {
            tabModelSelectedCallback.waitForCallback(0);
        } catch (TimeoutException e) {
            Assert.fail("Tab model selected event never occurred.");
        }
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sActivityTestRule
                            .getActivity()
                            .getTabModelSelector()
                            .getCurrentTabModelSupplier()
                            .removeObserver(observer);
                });
    }

    private void prepareTabs(int numTabs, int numIncognitoTabs) {
        TabUiTestHelper.createTabs(sActivityTestRule.getActivity(), false, numTabs);
        TabUiTestHelper.createTabs(sActivityTestRule.getActivity(), true, numIncognitoTabs);
    }

    private void exitSwitcherWithTabClick(int index) throws TimeoutException {
        TabUiTestHelper.clickNthCardFromTabSwitcher(sActivityTestRule.getActivity(), index);
        LayoutTestUtils.waitForLayout(
                sActivityTestRule.getActivity().getLayoutManager(), LayoutType.BROWSING);
    }

    private void closeTab(final boolean incognito, final int id) {
        ChromeTabUtils.closeTabWithAction(
                InstrumentationRegistry.getInstrumentation(),
                sActivityTestRule.getActivity(),
                () -> {
                    StripLayoutTab tab =
                            TabStripUtils.findStripLayoutTab(
                                    sActivityTestRule.getActivity(), incognito, id);
                    TabStripUtils.clickCompositorButton(
                            tab.getCloseButton(),
                            InstrumentationRegistry.getInstrumentation(),
                            sActivityTestRule.getActivity());
                });
    }

    private void ensureHubLayout() {
        LayoutManagerChrome layoutManager = sActivityTestRule.getActivity().getLayoutManager();
        Layout tabSwitcherLayout = layoutManager.getHubLayoutForTesting();
        if (tabSwitcherLayout == null) {
            ThreadUtils.runOnUiThreadBlocking(layoutManager::initHubLayoutForTesting);
            tabSwitcherLayout = layoutManager.getHubLayoutForTesting();
        }
        assertNotNull(tabSwitcherLayout);
    }

    private void checkHubLayout(ChromeTabbedActivity cta, boolean isInitialized) {
        Layout layout = cta.getLayoutManager().getHubLayoutForTesting();
        if (isInitialized) {
            assertNotNull("HubLayout should be initialized", layout);
        } else {
            assertNull("HubLayout should not be initialized", layout);
        }
    }

    private void checkTabSwitcherViewHolderStub(ChromeTabbedActivity cta, boolean exists) {
        ViewStub tabSwitcherStub = cta.findViewById(R.id.tab_switcher_view_holder_stub);
        if (exists) {
            assertTrue(
                    "TabSwitcher view stub should not be inflated",
                    tabSwitcherStub != null && tabSwitcherStub.getParent() != null);
        } else {
            assertTrue(
                    "TabSwitcher view stub should have been inflated",
                    tabSwitcherStub == null || tabSwitcherStub.getParent() == null);
        }
    }

    private void checkTabSwitcherViewHolder(ChromeTabbedActivity cta, boolean exists) {
        ViewGroup tabSwitcherViewHolder = cta.findViewById(R.id.tab_switcher_view_holder);
        if (exists) {
            assertNotNull("TabSwitcher view should be inflated", tabSwitcherViewHolder);
        } else {
            assertNull("TabSwitcher view should not be inflated", tabSwitcherViewHolder);
        }
    }

    private void checkTabSwitcherViewHolderVisibility(ChromeTabbedActivity cta, boolean visible) {
        @IdRes int tabSwitcherId = R.id.tab_switcher_view_holder;
        if (visible) {
            onView(withId(tabSwitcherId)).check(matches(withEffectiveVisibility(VISIBLE)));
        } else {
            onView(withId(tabSwitcherId)).check(matches(withEffectiveVisibility(GONE)));
        }
    }
}