chromium/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiTest.java

// Copyright 2020 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.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.RootMatchers.withDecorView;
import static androidx.test.espresso.matcher.ViewMatchers.Visibility.INVISIBLE;
import static androidx.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE;
import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
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 androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import static org.chromium.base.ThreadUtils.runOnUiThreadBlocking;
import static org.chromium.chrome.browser.ntp.HomeSurfaceTestUtils.createTabStatesAndMetadataFile;
import static org.chromium.chrome.browser.ntp.HomeSurfaceTestUtils.createThumbnailBitmapAndWriteToFile;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.clickFirstCardFromTabSwitcher;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.clickFirstTabInDialog;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.clickNthTabInDialog;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.createTabs;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.enterTabSwitcher;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.finishActivity;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.mergeAllNormalTabsToAGroup;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.verifyTabStripFaviconCount;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.verifyTabSwitcherCardCount;

import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

import androidx.recyclerview.widget.RecyclerView;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;

import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Batch;
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.Feature;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
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.tabmodel.TabModel;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.toolbar.bottom.BottomControlsCoordinator;
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.ChromeRenderTestRule;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.SheetState;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetTestSupport;
import org.chromium.components.browser_ui.bottomsheet.TestBottomSheetContent;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.ui.test.util.UiRestriction;
import org.chromium.ui.test.util.ViewUtils;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

/** End-to-end tests for TabGroupUi component. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@DisableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
@Batch(Batch.PER_CLASS)
public class TabGroupUiTest {

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

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

    @Rule
    public ChromeRenderTestRule mRenderTestRule =
            ChromeRenderTestRule.Builder.withPublicCorpus()
                    .setBugComponent(ChromeRenderTestRule.Component.UI_BROWSER_MOBILE_TAB_GROUPS)
                    .setRevision(2)
                    .build();

    @Mock private BrowserControlsStateProvider mBrowserControlsStateProvider;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        sActivityTestRule.loadUrl(UrlConstants.NTP_URL);
        TabUiTestHelper.verifyTabSwitcherLayoutType(sActivityTestRule.getActivity());
        CriteriaHelper.pollUiThread(
                sActivityTestRule.getActivity().getTabModelSelector()::isTabStateInitialized);
    }

    @Test
    @MediumTest
    public void testStripShownOnGroupTabPage() {
        final ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        mergeAllNormalTabsToAGroup(cta);
        verifyTabSwitcherCardCount(cta, 1);

        // Select the 1st tab in group.
        clickFirstCardFromTabSwitcher(cta);
        clickFirstTabInDialog(cta);
        assertFalse(cta.getLayoutManager().isLayoutVisible(LayoutType.TAB_SWITCHER));
        ViewUtils.waitForVisibleView(
                allOf(
                        withId(R.id.tab_list_recycler_view),
                        isDescendantOfA(withId(R.id.bottom_controls)),
                        isCompletelyDisplayed()));
        verifyTabStripFaviconCount(cta, 2);
    }

    @Test
    @LargeTest
    @Feature({"RenderTest"})
    @DisableIf.Build(supported_abis_includes = "x86")
    public void testRenderStrip_Select5thTabIn10Tabs() throws IOException {
        final ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        AtomicReference<RecyclerView> recyclerViewReference = new AtomicReference<>();
        TabUiTestHelper.addBlankTabs(cta, false, 9);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 10);
        mergeAllNormalTabsToAGroup(cta);
        verifyTabSwitcherCardCount(cta, 1);

        // Select the 5th tab in group.
        clickFirstCardFromTabSwitcher(cta);
        clickNthTabInDialog(cta, 4);

        ViewUtils.waitForVisibleView(
                allOf(
                        withId(R.id.tab_list_recycler_view),
                        isDescendantOfA(withId(R.id.bottom_controls)),
                        isCompletelyDisplayed()));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ViewGroup bottomToolbar = cta.findViewById(R.id.bottom_controls);
                    RecyclerView stripRecyclerView =
                            bottomToolbar.findViewById(R.id.tab_list_recycler_view);
                    recyclerViewReference.set(stripRecyclerView);
                });
        mRenderTestRule.render(recyclerViewReference.get(), "5th_tab_selected");
    }

    @Test
    @LargeTest
    @Feature({"RenderTest"})
    @DisabledTest(message = "crbug.com/359640997")
    public void testRenderStrip_Select10thTabIn10Tabs() throws IOException {
        final ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        AtomicReference<RecyclerView> recyclerViewReference = new AtomicReference<>();
        TabUiTestHelper.addBlankTabs(cta, false, 9);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 10);
        mergeAllNormalTabsToAGroup(cta);
        verifyTabSwitcherCardCount(cta, 1);

        // Select the 10th tab in group.
        clickFirstCardFromTabSwitcher(cta);
        clickNthTabInDialog(cta, 9);

        ViewUtils.waitForVisibleView(
                allOf(
                        withId(R.id.tab_list_recycler_view),
                        isDescendantOfA(withId(R.id.bottom_controls)),
                        isCompletelyDisplayed()));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ViewGroup bottomToolbar = cta.findViewById(R.id.bottom_controls);
                    RecyclerView stripRecyclerView =
                            bottomToolbar.findViewById(R.id.tab_list_recycler_view);
                    recyclerViewReference.set(stripRecyclerView);
                });
        mRenderTestRule.render(recyclerViewReference.get(), "10th_tab_selected");
    }

    @Test
    @LargeTest
    @Feature({"RenderTest"})
    @EnableFeatures(ChromeFeatureList.DATA_SHARING)
    public void testRenderStrip_toggleNotificationBubble() throws IOException {
        final ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        AtomicReference<ViewGroup> controlsReference = new AtomicReference<>();
        TabUiTestHelper.addBlankTabs(cta, false, 1);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        mergeAllNormalTabsToAGroup(cta);
        verifyTabSwitcherCardCount(cta, 1);

        // Select the 2nd tab in group.
        clickFirstCardFromTabSwitcher(cta);
        clickNthTabInDialog(cta, 1);

        ViewUtils.waitForVisibleView(
                allOf(
                        withId(R.id.tab_list_recycler_view),
                        isDescendantOfA(withId(R.id.bottom_controls)),
                        isCompletelyDisplayed()));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ViewGroup bottomToolbar = cta.findViewById(R.id.bottom_controls);
                    RecyclerView stripRecyclerView =
                            bottomToolbar.findViewById(R.id.tab_list_recycler_view);

                    ImageView notificationView =
                            stripRecyclerView.findViewById(R.id.tab_strip_notification_bubble);
                    notificationView.setVisibility(View.VISIBLE);
                    controlsReference.set(bottomToolbar);
                });
        mRenderTestRule.render(
                controlsReference.get(), "bottom_controls_tab_strip_notification_bubble_on");

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ViewGroup bottomToolbar = cta.findViewById(R.id.bottom_controls);
                    RecyclerView stripRecyclerView =
                            bottomToolbar.findViewById(R.id.tab_list_recycler_view);

                    ImageView notificationView =
                            stripRecyclerView.findViewById(R.id.tab_strip_notification_bubble);
                    notificationView.setVisibility(View.GONE);
                    controlsReference.set(bottomToolbar);
                });
        mRenderTestRule.render(
                controlsReference.get(), "bottom_controls_tab_strip_notification_bubble_off");
    }

    @Test
    @LargeTest
    @Feature({"RenderTest"})
    public void testRenderStrip_AddTab() throws IOException {
        final ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        AtomicReference<RecyclerView> recyclerViewReference = new AtomicReference<>();
        TabUiTestHelper.addBlankTabs(cta, false, 9);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 10);
        mergeAllNormalTabsToAGroup(cta);
        verifyTabSwitcherCardCount(cta, 1);

        // Select the first tab in group and add one new tab to group.
        clickFirstCardFromTabSwitcher(cta);
        clickNthTabInDialog(cta, 0);
        ViewUtils.waitForVisibleView(
                allOf(
                        withId(R.id.tab_list_recycler_view),
                        isDescendantOfA(withId(R.id.bottom_controls)),
                        isCompletelyDisplayed()));
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ViewGroup bottomToolbar = cta.findViewById(R.id.bottom_controls);
                    RecyclerView stripRecyclerView =
                            bottomToolbar.findViewById(R.id.tab_list_recycler_view);
                    recyclerViewReference.set(stripRecyclerView);
                    // Disable animation to reduce flakiness.
                    stripRecyclerView.setItemAnimator(null);
                });
        onView(
                        allOf(
                                withId(R.id.toolbar_new_tab_button),
                                withParent(withId(R.id.main_content)),
                                withEffectiveVisibility(VISIBLE)))
                .perform(click());
        mRenderTestRule.render(recyclerViewReference.get(), "11th_tab_selected");
    }

    @Test
    @LargeTest
    @Feature({"RenderTest"})
    public void testRenderStrip_BackgroundAddTab() throws IOException {
        final ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        AtomicReference<RecyclerView> recyclerViewReference = new AtomicReference<>();
        TabUiTestHelper.addBlankTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 3);
        mergeAllNormalTabsToAGroup(cta);
        verifyTabSwitcherCardCount(cta, 1);

        // Select the first tab in group and add one new tab to group.
        clickFirstCardFromTabSwitcher(cta);
        clickNthTabInDialog(cta, 0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Tab tab =
                            cta.getCurrentTabCreator()
                                    .createNewTab(
                                            new LoadUrlParams("about:blank"),
                                            "About Test",
                                            TabLaunchType.FROM_SYNC_BACKGROUND,
                                            null,
                                            TabModel.INVALID_TAB_INDEX);
                    TabGroupModelFilter filter =
                            (TabGroupModelFilter)
                                    cta.getTabModelSelector()
                                            .getTabModelFilterProvider()
                                            .getTabModelFilter(false);
                    filter.mergeListOfTabsToGroup(
                            List.of(tab), filter.getTabAt(0), /* notify= */ false);
                });
        ViewUtils.waitForVisibleView(
                allOf(
                        withId(R.id.tab_list_recycler_view),
                        isDescendantOfA(withId(R.id.bottom_controls)),
                        isCompletelyDisplayed()));
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ViewGroup bottomToolbar = cta.findViewById(R.id.bottom_controls);
                    RecyclerView stripRecyclerView =
                            bottomToolbar.findViewById(R.id.tab_list_recycler_view);
                    recyclerViewReference.set(stripRecyclerView);
                    // Disable animation to reduce flakiness.
                    stripRecyclerView.setItemAnimator(null);
                });
        mRenderTestRule.render(recyclerViewReference.get(), "3rd_tab_selected");
    }

    @Test
    @MediumTest
    @DisabledTest(message = "crbug.com/363049835")
    public void testVisibilityChangeWithOmnibox() throws Exception {

        // Create a tab group with 2 tabs.
        finishActivity(sActivityTestRule.getActivity());
        createThumbnailBitmapAndWriteToFile(0, mBrowserControlsStateProvider);
        createThumbnailBitmapAndWriteToFile(1, mBrowserControlsStateProvider);
        createTabStatesAndMetadataFile(new int[] {0, 1}, new int[] {0, 0});

        // Restart Chrome and make sure tab strip is showing.
        sActivityTestRule.startMainActivityFromLauncher();
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        CriteriaHelper.pollUiThread(cta.getTabModelSelector()::isTabStateInitialized);
        ViewUtils.waitForVisibleView(
                allOf(
                        withId(R.id.tab_list_recycler_view),
                        isDescendantOfA(withId(R.id.bottom_controls)),
                        isCompletelyDisplayed()));

        // The strip should be hidden when omnibox is focused.
        onView(withId(R.id.url_bar)).perform(click());
        onView(
                        allOf(
                                withId(R.id.tab_list_recycler_view),
                                isDescendantOfA(withId(R.id.bottom_controls))))
                .check(matches(withEffectiveVisibility((INVISIBLE))));
    }

    @Test
    @MediumTest
    @DisabledTest(message = "crbug.com/326049916")
    @CommandLineFlags.Add({
        "enable-features=IPH_TabGroupsTapToSeeAnotherTab<TabGroupsTapToSeeAnotherTab",
        "force-fieldtrials=TabGroupsTapToSeeAnotherTab/Enabled/",
        "force-fieldtrial-params=TabGroupsTapToSeeAnotherTab.Enabled:availability/any/"
                + "event_trigger/"
                + "name%3Aiph_tabgroups_strip;comparator%3A==0;window%3A30;storage%3A365/"
                + "event_trigger2/"
                + "name%3Aiph_tabgroups_strip;comparator%3A<2;window%3A90;storage%3A365/"
                + "event_used/"
                + "name%3Aiph_tabgroups_strip;comparator%3A==0;window%3A365;storage%3A365/"
                + "session_rate/<1"
    })
    public void testIphBottomSheetSuppression() throws Exception {

        // Create a tab group with 2 tabs, and turn on enable_launch_bug_fix variation.
        finishActivity(sActivityTestRule.getActivity());
        createThumbnailBitmapAndWriteToFile(0, mBrowserControlsStateProvider);
        createThumbnailBitmapAndWriteToFile(1, mBrowserControlsStateProvider);
        createTabStatesAndMetadataFile(new int[] {0, 1}, new int[] {0, 0});

        // Restart Chrome and make sure both tab strip and IPH text bubble are showing.
        sActivityTestRule.startMainActivityFromLauncher();
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        CriteriaHelper.pollUiThread(cta.getTabModelSelector()::isTabStateInitialized);
        ViewUtils.waitForVisibleView(
                allOf(
                        withId(R.id.tab_list_recycler_view),
                        isDescendantOfA(withId(R.id.bottom_controls)),
                        isCompletelyDisplayed()));
        assertTrue(isTabStripIphShowing(cta));

        // Show a bottom sheet, and the IPH should be hidden.
        final BottomSheetController bottomSheetController =
                cta.getRootUiCoordinatorForTesting().getBottomSheetController();
        final BottomSheetTestSupport bottomSheetTestSupport =
                new BottomSheetTestSupport(bottomSheetController);
        runOnUiThreadBlocking(
                () -> {
                    TestBottomSheetContent bottomSheetContent =
                            new TestBottomSheetContent(
                                    cta, BottomSheetContent.ContentPriority.HIGH, false);
                    bottomSheetController.requestShowContent(bottomSheetContent, false);
                });
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            bottomSheetController.getSheetState(), not(is(SheetState.HIDDEN)));
                });
        assertFalse(isTabStripIphShowing(cta));

        // Hide the bottom sheet, and the IPH should reshow.
        runOnUiThreadBlocking(() -> bottomSheetTestSupport.setSheetState(SheetState.HIDDEN, false));
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            bottomSheetController.getSheetState(), is(SheetState.HIDDEN));
                });
        assertTrue(isTabStripIphShowing(cta));

        // When the IPH is clicked and dismissed, opening bottom sheet should never reshow it.
        onView(withText(cta.getString(R.string.iph_tab_groups_tap_to_see_another_tab_text)))
                .inRoot(withDecorView(not(cta.getWindow().getDecorView())))
                .perform(click());
        assertFalse(isTabStripIphShowing(cta));
        runOnUiThreadBlocking(
                () -> {
                    TestBottomSheetContent bottomSheetContent =
                            new TestBottomSheetContent(
                                    cta, BottomSheetContent.ContentPriority.HIGH, false);
                    bottomSheetController.requestShowContent(bottomSheetContent, false);
                });
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            bottomSheetController.getSheetState(), not(is(SheetState.HIDDEN)));
                });
        assertFalse(isTabStripIphShowing(cta));
    }

    @Test
    @MediumTest
    public void testStripShownOnGroupTabPage_EdgeToEdge() throws Exception {
        // Create a tab group with 2 tabs.
        finishActivity(sActivityTestRule.getActivity());
        createThumbnailBitmapAndWriteToFile(0, mBrowserControlsStateProvider);
        createThumbnailBitmapAndWriteToFile(1, mBrowserControlsStateProvider);
        createTabStatesAndMetadataFile(new int[] {0, 1}, new int[] {0, 0});

        // Restart Chrome and make sure tab strip is showing.
        sActivityTestRule.startMainActivityFromLauncher();
        ChromeTabbedActivity cta = sActivityTestRule.getActivity();
        CriteriaHelper.pollUiThread(cta.getTabModelSelector()::isTabStateInitialized);
        ViewUtils.waitForVisibleView(
                allOf(
                        withId(R.id.tab_list_recycler_view),
                        isDescendantOfA(withId(R.id.bottom_controls)),
                        isCompletelyDisplayed()));

        BottomControlsCoordinator coordinator =
                sActivityTestRule
                        .getActivity()
                        .getRootUiCoordinatorForTesting()
                        .getToolbarManager()
                        .getBottomControlsCoordinatorForTesting();

        assertTrue(
                "Scene overlay should be visible",
                coordinator.getSceneLayerForTesting().isSceneOverlayTreeShowing());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    coordinator.simulateEdgeToEdgeChangeForTesting(
                            100, /* isDrawingToEdge= */ true, /* isPageOptInToEdge= */ true);
                });

        assertFalse(
                "Scene overlay should be hidden.",
                coordinator.getSceneLayerForTesting().isSceneOverlayTreeShowing());

        // Force a bitmap capture.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    coordinator.getResourceAdapterForTesting().triggerBitmapCapture();
                });

        assertTrue(
                "Scene overlay should visible after bitmap capture.",
                coordinator.getSceneLayerForTesting().isSceneOverlayTreeShowing());
    }

    private boolean isTabStripIphShowing(ChromeTabbedActivity cta) {
        String iphText = cta.getString(R.string.iph_tab_groups_tap_to_see_another_tab_text);
        boolean isShowing = true;
        try {
            onView(withText(iphText))
                    .inRoot(withDecorView(not(cta.getWindow().getDecorView())))
                    .check(matches(isDisplayed()));
        } catch (Exception e) {
            isShowing = false;
        }
        return isShowing;
    }
}