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

// Copyright 2019 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.closeSoftKeyboard;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.action.ViewActions.pressImeActionButton;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
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.withContentDescription;
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.core.AllOf.allOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.closeFirstTabGroupInTabSwitcher;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.closeFirstTabInTabSwitcher;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.createTabGroup;
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.getSwipeToDismissAction;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.mergeAllNormalTabsToAGroup;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.switchTabModel;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.verifyTabModelTabCount;
import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.verifyTabSwitcherCardCount;
import static org.chromium.components.embedder_support.util.UrlConstants.NTP_URL;
import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;

import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.LayerDrawable;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.NoMatchingViewException;
import androidx.test.espresso.ViewAssertion;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before;
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.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.night_mode.ChromeNightModeTestUtils;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabUtils;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tab_ui.TabThumbnailView;
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.tasks.tab_groups.TabGroupColorUtils;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.undo_tab_close_snackbar.UndoBarController;
import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.ActivityTestUtils;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.MenuUtils;
import org.chromium.components.tab_groups.TabGroupColorId;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.test.util.UiRestriction;
import org.chromium.ui.util.ColorUtils;
import org.chromium.ui.widget.ViewLookupCachingFrameLayout;

import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;

/** Tests for the {@link TabSwitcherLayout}. */
@SuppressWarnings("ConstantConditions")
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({
    ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
    "force-fieldtrials=Study/Group"
})
@DisableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
@Restriction({
    UiRestriction.RESTRICTION_TYPE_PHONE,
    Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE
})
public class TabSwitcherLayoutTest {
    private static final String TEST_URL = "/chrome/test/data/android/google.html";

    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    @SuppressWarnings("FieldCanBeLocal")
    private EmbeddedTestServer mTestServer;

    private String mUrl;
    private int mRepeat;
    private List<WeakReference<Bitmap>> mAllBitmaps = new LinkedList<>();
    private Callback<Bitmap> mBitmapListener =
            (bitmap) -> mAllBitmaps.add(new WeakReference<>(bitmap));
    private ModalDialogManager mModalDialogManager;

    @Before
    public void setUp() throws ExecutionException {
        mTestServer = mActivityTestRule.getTestServer();

        // After setUp, Chrome is launched and has one NTP.
        mActivityTestRule.startMainActivityWithURL(NTP_URL);

        ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        mUrl = mTestServer.getURL("/chrome/test/data/android/navigate/simple.html");
        mRepeat = 1;

        cta.getTabContentManager().setCaptureMinRequestTimeForTesting(0);

        CriteriaHelper.pollUiThread(cta.getTabModelSelector()::isTabStateInitialized);
        mModalDialogManager = ThreadUtils.runOnUiThreadBlocking(cta::getModalDialogManager);
    }

    @After
    public void tearDown() {
        ThreadUtils.runOnUiThreadBlocking(
                ChromeNightModeTestUtils::tearDownNightModeAfterChromeActivityDestroyed);
        ThreadUtils.runOnUiThreadBlocking(
                () -> ChromeAccessibilityUtil.get().setAccessibilityEnabledForTesting(null));
        dismissAllModalDialogs();
    }

    /**
     * Make Chrome have {@code numTabs} of regular Tabs and {@code numIncognitoTabs} of incognito
     * tabs with {@code url} loaded, and assert no bitmap fetching occurred.
     *
     * @param numTabs The number of regular tabs.
     * @param numIncognitoTabs The number of incognito tabs.
     * @param url The URL to load.
     */
    private void prepareTabs(int numTabs, int numIncognitoTabs, @Nullable String url) {
        TabUiTestHelper.prepareTabsWithThumbnail(mActivityTestRule, numTabs, numIncognitoTabs, url);
    }

    @Test
    @MediumTest
    public void testUrlUpdatedNotCrashing_ForUndoableClosedTab() throws Exception {
        ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        cta.getSnackbarManager().disableForTesting();
        prepareTabs(2, 0, null);
        enterTabSwitcher(cta);

        Tab tab = cta.getTabModelSelector().getCurrentTab();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    cta.getTabModelSelector()
                            .getCurrentModel()
                            .closeTabs(TabClosureParams.closeTab(tab).build());
                });
        mActivityTestRule.loadUrlInTab(
                mUrl, PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR, tab);
    }

    @Test
    @MediumTest
    public void testUrlUpdatedNotCrashing_ForTabNotInCurrentModel() throws Exception {
        ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        prepareTabs(1, 1, null);
        enterTabSwitcher(cta);

        Tab tab = cta.getTabModelSelector().getCurrentTab();
        switchTabModel(cta, false);

        mActivityTestRule.loadUrlInTab(
                mUrl, PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR, tab);
    }

    private int getTabCountInCurrentTabModel() {
        return mActivityTestRule.getActivity().getTabModelSelector().getCurrentModel().getCount();
    }

    @Test
    @LargeTest
    public void testThumbnailAspectRatio_default() {
        prepareTabs(2, 0, "about:blank");
        enterTabSwitcher(mActivityTestRule.getActivity());
        onViewWaiting(tabSwitcherViewMatcher())
                .check(
                        ThumbnailAspectRatioAssertion.havingAspectRatio(
                                TabUtils.getTabThumbnailAspectRatio(
                                        mActivityTestRule.getActivity(),
                                        mActivityTestRule
                                                .getActivity()
                                                .getBrowserControlsManager())));
    }

    @Test
    @MediumTest
    public void testThumbnailFetchingResult_jpeg() throws Exception {
        // May be called when setting both grid card size and thumbnail fetcher.
        var histograms =
                HistogramWatcher.newBuilder()
                        .expectIntRecord(
                                TabContentManager.UMA_THUMBNAIL_FETCHING_RESULT,
                                TabContentManager.ThumbnailFetchingResult.GOT_JPEG)
                        .allowExtraRecords(TabContentManager.UMA_THUMBNAIL_FETCHING_RESULT)
                        .build();

        simulateJpegHasCachedWithDefaultAspectRatio();

        enterTabSwitcher(mActivityTestRule.getActivity());

        onViewWaiting(
                        allOf(
                                isDescendantOfA(
                                        withId(
                                                TabUiTestHelper.getTabSwitcherAncestorId(
                                                        mActivityTestRule.getActivity()))),
                                withId(R.id.tab_thumbnail)))
                .check(matches(isDisplayed()));

        histograms.assertExpected();
    }

    @Test
    @MediumTest
    public void testRecycling_defaultAspectRatio() {
        prepareTabs(10, 0, mUrl);
        ChromeTabUtils.switchTabInCurrentTabModel(mActivityTestRule.getActivity(), 0);
        enterTabSwitcher(mActivityTestRule.getActivity());
        onView(tabSwitcherViewMatcher()).perform(RecyclerViewActions.scrollToPosition(9));
    }

    @Test
    @MediumTest
    public void testCloseTabViaCloseButton() throws Exception {
        mActivityTestRule.getActivity().getSnackbarManager().disableForTesting();
        prepareTabs(1, 0, null);
        enterTabSwitcher(mActivityTestRule.getActivity());

        onView(
                        allOf(
                                withId(R.id.action_button),
                                withParent(withId(R.id.content_view)),
                                withEffectiveVisibility(VISIBLE)))
                .perform(click());
    }

    @Test
    @MediumTest
    @DisabledTest(message = "Flaky - https://crbug.com/1124041, crbug.com/1061178")
    public void testSwipeToDismiss_GTS() {
        ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        // Create 3 tabs and merge the first two tabs into one group.
        createTabs(cta, false, 3);
        enterTabSwitcher(cta);
        TabModel normalTabModel = cta.getTabModelSelector().getModel(false);
        List<Tab> tabGroup =
                new ArrayList<>(
                        Arrays.asList(normalTabModel.getTabAt(0), normalTabModel.getTabAt(1)));
        createTabGroup(cta, false, tabGroup);
        verifyTabSwitcherCardCount(cta, 2);
        verifyTabModelTabCount(cta, 3, 0);

        // Swipe to dismiss a single tab card.
        onView(tabSwitcherViewMatcher())
                .perform(
                        RecyclerViewActions.actionOnItemAtPosition(
                                1, getSwipeToDismissAction(false)));
        verifyTabSwitcherCardCount(cta, 1);
        verifyTabModelTabCount(cta, 2, 0);

        // Swipe to dismiss a tab group card.
        onView(tabSwitcherViewMatcher())
                .perform(
                        RecyclerViewActions.actionOnItemAtPosition(
                                0, getSwipeToDismissAction(true)));
        verifyTabSwitcherCardCount(cta, 0);
        verifyTabModelTabCount(cta, 0, 0);
    }

    @Test
    @MediumTest
    public void testCloseButtonDescription() {
        String expectedDescription = "Close New tab tab";
        ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        enterTabSwitcher(cta);

        // Test single tab.
        onView(
                        allOf(
                                withParent(withId(R.id.content_view)),
                                withId(R.id.action_button),
                                withEffectiveVisibility(VISIBLE)))
                .check(ViewContentDescription.havingDescription(expectedDescription));

        // Create 2 tabs and merge them into one group.
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        TabModel normalTabModel = cta.getTabModelSelector().getModel(false);
        List<Tab> tabGroup =
                new ArrayList<>(
                        Arrays.asList(normalTabModel.getTabAt(0), normalTabModel.getTabAt(1)));
        createTabGroup(cta, false, tabGroup);
        verifyTabSwitcherCardCount(cta, 1);

        // Test group tab.
        expectedDescription = "Close tab group with 2 tabs.";
        onView(
                        allOf(
                                withParent(withId(R.id.content_view)),
                                withId(R.id.action_button),
                                withEffectiveVisibility(VISIBLE)))
                .check(ViewContentDescription.havingDescription(expectedDescription));
    }

    private static class ViewContentDescription implements ViewAssertion {
        private String mExpectedDescription;

        public static ViewContentDescription havingDescription(String description) {
            return new ViewContentDescription(description);
        }

        public ViewContentDescription(String description) {
            mExpectedDescription = description;
        }

        @Override
        public void check(View view, NoMatchingViewException noMatchException) {
            if (noMatchException != null) throw noMatchException;

            assertEquals(mExpectedDescription, view.getContentDescription());
        }
    }

    private static class ThumbnailAspectRatioAssertion implements ViewAssertion {
        private double mExpectedRatio;

        public static ThumbnailAspectRatioAssertion havingAspectRatio(double ratio) {
            return new ThumbnailAspectRatioAssertion(ratio);
        }

        private ThumbnailAspectRatioAssertion(double expectedRatio) {
            mExpectedRatio = expectedRatio;
        }

        @Override
        public void check(View view, NoMatchingViewException noMatchException) {
            if (noMatchException != null) throw noMatchException;

            RecyclerView recyclerView = (RecyclerView) view;

            RecyclerView.Adapter adapter = recyclerView.getAdapter();
            boolean hasAtLeastOneValidViewHolder = false;
            for (int i = 0; i < adapter.getItemCount(); i++) {
                RecyclerView.ViewHolder viewHolder =
                        recyclerView.findViewHolderForAdapterPosition(i);
                if (viewHolder != null) {
                    hasAtLeastOneValidViewHolder = true;
                    ViewLookupCachingFrameLayout tabView =
                            (ViewLookupCachingFrameLayout) viewHolder.itemView;
                    TabThumbnailView thumbnail =
                            (TabThumbnailView) tabView.fastFindViewById(R.id.tab_thumbnail);

                    double thumbnailViewRatio = thumbnail.getWidth() * 1.0 / thumbnail.getHeight();
                    int pixelDelta =
                            Math.abs(
                                    (int) Math.round(thumbnail.getHeight() * mExpectedRatio)
                                            - thumbnail.getWidth());
                    assertTrue(
                            "Actual ratio: "
                                    + thumbnailViewRatio
                                    + "; Expected ratio: "
                                    + mExpectedRatio
                                    + "; Pixel delta: "
                                    + pixelDelta,
                            pixelDelta
                                    <= thumbnail.getWidth()
                                            * TabContentManager.PIXEL_TOLERANCE_PERCENT);
                }
            }
            assertTrue("should have at least one valid ViewHolder", hasAtLeastOneValidViewHolder);
        }
    }

    @Test
    @MediumTest
    public void verifyTabGroupStateAfterReparenting() throws Exception {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        assertTrue(
                cta.getTabModelSelector().getTabModelFilterProvider().getCurrentTabModelFilter()
                        instanceof TabGroupModelFilter);
        mActivityTestRule.loadUrl(mUrl);
        Tab parentTab = cta.getTabModelSelector().getCurrentTab();

        // Create a tab group.
        TabCreator tabCreator = ThreadUtils.runOnUiThreadBlocking(() -> cta.getTabCreator(false));
        LoadUrlParams loadUrlParams = new LoadUrlParams(mUrl);
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        tabCreator.createNewTab(
                                loadUrlParams,
                                TabLaunchType.FROM_LONGPRESS_BACKGROUND_IN_GROUP,
                                parentTab));
        Tab childTab = cta.getTabModelSelector().getCurrentModel().getTabAt(1);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 1);
        TabGroupModelFilter filter =
                (TabGroupModelFilter)
                        cta.getTabModelSelector()
                                .getTabModelFilterProvider()
                                .getCurrentTabModelFilter();
        ThreadUtils.runOnUiThreadBlocking(() -> filter.moveTabOutOfGroup(childTab.getId()));
        verifyTabSwitcherCardCount(cta, 2);

        ThreadUtils.runOnUiThreadBlocking(
                () -> ChromeNightModeTestUtils.setUpNightModeForChromeActivity(true));
        final ChromeTabbedActivity ctaNightMode =
                ActivityTestUtils.waitForActivity(
                        InstrumentationRegistry.getInstrumentation(), ChromeTabbedActivity.class);
        assertTrue(ColorUtils.inNightMode(ctaNightMode));
        CriteriaHelper.pollUiThread(ctaNightMode.getTabModelSelector()::isTabStateInitialized);
        enterTabSwitcher(ctaNightMode);
        verifyTabSwitcherCardCount(ctaNightMode, 2);
    }

    @Test
    @MediumTest
    public void testUndoClosure_AccessibilityMode() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> ChromeAccessibilityUtil.get().setAccessibilityEnabledForTesting(true));
        ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        SnackbarManager snackbarManager = mActivityTestRule.getActivity().getSnackbarManager();
        createTabs(cta, false, 3);

        // When grid tab switcher is enabled for accessibility mode, tab closure should still show
        // undo snack bar.
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 3);
        assertNull(snackbarManager.getCurrentSnackbarForTesting());
        closeFirstTabInTabSwitcher(cta);
        assertTrue(
                snackbarManager.getCurrentSnackbarForTesting().getController()
                        instanceof UndoBarController);
        verifyTabSwitcherCardCount(cta, 2);
        CriteriaHelper.pollInstrumentationThread(TabUiTestHelper::verifyUndoBarShowingAndClickUndo);
        verifyTabSwitcherCardCount(cta, 3);
    }

    @Test
    @MediumTest
    public void testUndoGroupClosureInTabSwitcher() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        SnackbarManager snackbarManager = mActivityTestRule.getActivity().getSnackbarManager();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        // Create a tab group.
        mergeAllNormalTabsToAGroup(cta);
        verifyTabSwitcherCardCount(cta, 1);
        assertNotNull(snackbarManager.getCurrentSnackbarForTesting());

        // Verify close this tab group and undo in tab switcher.
        closeFirstTabGroupInTabSwitcher(cta);
        assertTrue(
                snackbarManager.getCurrentSnackbarForTesting().getController()
                        instanceof UndoBarController);
        verifyTabSwitcherCardCount(cta, 0);
        CriteriaHelper.pollInstrumentationThread(TabUiTestHelper::verifyUndoBarShowingAndClickUndo);
        verifyTabSwitcherCardCount(cta, 1);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID
    })
    @DisabledTest(message = "crbug.com/353946452")
    public void testTabGroupOverflowMenuInTabSwitcher_closeGroup() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        SnackbarManager snackbarManager = cta.getSnackbarManager();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        verifyGroupVisualDataDialogOpenedAndDismiss(cta);
        verifyTabSwitcherCardCount(cta, 1);

        verifyFirstCardTitle("2 tabs");
        verifyFirstCardColor(TabGroupColorId.GREY);

        // Click the close action button to close the group
        String closeButtonText = cta.getString(R.string.close_tab_group_menu_item);
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(closeButtonText), withId(R.id.menu_item_text))).perform(click());

        // Verify the tab group was closed.
        assertTrue(
                snackbarManager.getCurrentSnackbarForTesting().getController()
                        instanceof UndoBarController);
        verifyTabSwitcherCardCount(cta, 0);
        CriteriaHelper.pollInstrumentationThread(TabUiTestHelper::verifyUndoBarShowingAndClickUndo);
        verifyTabSwitcherCardCount(cta, 1);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID
    })
    @DisabledTest(message = "crbug.com/360393681")
    public void testTabGroupOverflowMenuInTabSwitcher_renameGroupAccept() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        verifyGroupVisualDataDialogOpenedAndDismiss(cta);
        verifyTabSwitcherCardCount(cta, 1);

        verifyFirstCardTitle("2 tabs");
        verifyFirstCardColor(TabGroupColorId.GREY);

        // Click the rename action button to rename the group
        String renameButtonText = cta.getString(R.string.rename_tab_group_menu_item);
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(renameButtonText), withId(R.id.menu_item_text))).perform(click());

        // Verify that the modal dialog is now showing.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        // Verify the visual data dialog exists.
        onViewWaiting(withId(R.id.visual_data_dialog_layout), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));
        // Wait until the keyboard is showing.
        KeyboardVisibilityDelegate delegate = cta.getWindowAndroid().getKeyboardDelegate();
        CriteriaHelper.pollUiThread(
                () -> delegate.isKeyboardShowing(cta, cta.getCompositorViewHolderForTesting()));

        // Change the title.
        editGroupVisualDataDialogTitle(cta, "Test");
        // Change the color.
        String blueColor =
                cta.getString(R.string.accessibility_tab_group_color_picker_color_item_blue);
        String notSelectedStringBlue =
                cta.getString(
                        R.string
                                .accessibility_tab_group_color_picker_color_item_not_selected_description,
                        blueColor);
        onView(withContentDescription(notSelectedStringBlue)).perform(click());

        // Accept the change.
        onView(withId(R.id.positive_button)).perform(click());
        verifyModalDialogHidingAnimationCompleteInTabSwitcher();
        // Check that the title change is reflected.
        verifyFirstCardTitle("Test");
        // Verify the color icon exists.
        verifyFirstCardColor(TabGroupColorId.BLUE);
        verifyTabSwitcherCardCount(cta, 1);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID
    })
    @DisabledTest(message = "crbug.com/360393681")
    public void testTabGroupOverflowMenuInTabSwitcher_renameGroupDecline() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        verifyGroupVisualDataDialogOpenedAndDismiss(cta);
        verifyTabSwitcherCardCount(cta, 1);

        verifyFirstCardTitle("2 tabs");
        verifyFirstCardColor(TabGroupColorId.GREY);

        // Click the rename action button to rename the group
        String renameButtonText = cta.getString(R.string.rename_tab_group_menu_item);
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(renameButtonText), withId(R.id.menu_item_text))).perform(click());

        // Verify that the modal dialog is now showing.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        // Verify the visual data dialog exists.
        onViewWaiting(withId(R.id.visual_data_dialog_layout), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));
        // Wait until the keyboard is showing.
        KeyboardVisibilityDelegate delegate = cta.getWindowAndroid().getKeyboardDelegate();
        CriteriaHelper.pollUiThread(
                () -> delegate.isKeyboardShowing(cta, cta.getCompositorViewHolderForTesting()));

        // Change the title.
        editGroupVisualDataDialogTitle(cta, "Test");
        // Change the color.
        String blueColor =
                cta.getString(R.string.accessibility_tab_group_color_picker_color_item_blue);
        String notSelectedStringBlue =
                cta.getString(
                        R.string
                                .accessibility_tab_group_color_picker_color_item_not_selected_description,
                        blueColor);
        onView(withContentDescription(notSelectedStringBlue)).perform(click());

        // Decline the change.
        onView(withId(R.id.negative_button)).perform(click());
        verifyModalDialogHidingAnimationCompleteInTabSwitcher();
        // Check that the title change is reflected.
        verifyFirstCardTitle("2 tabs");
        // Verify the color icon exists.
        verifyFirstCardColor(TabGroupColorId.GREY);
        verifyTabSwitcherCardCount(cta, 1);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID,
        ChromeFeatureList.TAB_GROUP_SYNC_ANDROID,
    })
    @DisabledTest(message = "crbug.com/360393681")
    public void testTabGroupOverflowMenuInTabSwitcher_ungroupAccept() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        verifyGroupVisualDataDialogOpenedAndDismiss(cta);
        verifyTabSwitcherCardCount(cta, 1);

        verifyFirstCardTitle("2 tabs");
        verifyFirstCardColor(TabGroupColorId.GREY);

        // Click the ungroup action button to ungroup the group
        String ungroupButtonText = cta.getString(R.string.ungroup_tab_group_menu_item);
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(ungroupButtonText), withId(R.id.menu_item_text))).perform(click());

        // Verify that the modal dialog is now showing.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        // Verify the ungroup dialog exists.
        onViewWaiting(withId(R.id.stop_showing_check_box), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));
        // Confirm the action.
        onView(withId(R.id.positive_button)).perform(click());
        // Verify the tab group was ungrouped.
        verifyTabSwitcherCardCount(cta, 2);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID,
    })
    @DisabledTest(message = "crbug.com/353946452")
    public void testTabGroupOverflowMenuInTabSwitcher_ungroupDecline() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        verifyGroupVisualDataDialogOpenedAndDismiss(cta);
        verifyTabSwitcherCardCount(cta, 1);

        verifyFirstCardTitle("2 tabs");
        verifyFirstCardColor(TabGroupColorId.GREY);

        // Click the ungroup action button to ungroup the group
        String ungroupButtonText = cta.getString(R.string.ungroup_tab_group_menu_item);
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(ungroupButtonText), withId(R.id.menu_item_text))).perform(click());

        // Verify that the modal dialog is now showing.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        // Verify the ungroup dialog exists.
        onViewWaiting(withId(R.id.stop_showing_check_box), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));
        // Decline the action.
        onView(withId(R.id.negative_button)).perform(click());
        // Verify the tab group was not ungrouped.
        verifyTabSwitcherCardCount(cta, 1);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID,
    })
    @DisabledTest(message = "Flaky - crbug.com/353463207")
    public void testTabGroupOverflowMenuInTabSwitcher_ungroupDoNotShowAgain() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        verifyGroupVisualDataDialogOpenedAndDismiss(cta);
        verifyTabSwitcherCardCount(cta, 1);

        verifyFirstCardTitle("2 tabs");
        verifyFirstCardColor(TabGroupColorId.GREY);

        // Click the ungroup action button to ungroup the group
        String ungroupButtonText = cta.getString(R.string.ungroup_tab_group_menu_item);
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(ungroupButtonText), withId(R.id.menu_item_text))).perform(click());

        // Verify that the modal dialog is now showing.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        // Verify the ungroup dialog exists.
        onViewWaiting(withId(R.id.stop_showing_check_box), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));
        // Select the checkbox.
        onView(withId(R.id.stop_showing_check_box)).perform(click());
        // Confirm the action.
        onView(withId(R.id.positive_button)).perform(click());
        // Verify the tab group was ungrouped.
        verifyTabSwitcherCardCount(cta, 2);

        // Regroup the tabs.
        mergeAllNormalTabsToAGroup(cta);
        verifyTabSwitcherCardCount(cta, 1);

        // Click the ungroup action button to ungroup the group
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(ungroupButtonText), withId(R.id.menu_item_text))).perform(click());
        // Verify the ungroup dialog does not exist.
        onView(withId(R.id.stop_showing_check_box)).check(doesNotExist());
        // Verify the tab group was ungrouped.
        verifyTabSwitcherCardCount(cta, 2);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID,
        ChromeFeatureList.TAB_GROUP_SYNC_ANDROID,
    })
    @DisabledTest(message = "Flaky - crbug.com/353463207")
    public void testTabGroupOverflowMenuInTabSwitcher_deleteGroupAccept() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        verifyGroupVisualDataDialogOpenedAndDismiss(cta);
        verifyTabSwitcherCardCount(cta, 1);

        verifyFirstCardTitle("2 tabs");
        verifyFirstCardColor(TabGroupColorId.GREY);

        // Click the delete action button to close the group
        String deleteButtonText = cta.getString(R.string.delete_tab_group_menu_item);
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(deleteButtonText), withId(R.id.menu_item_text))).perform(click());

        // Verify that the modal dialog is now showing.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        // Verify the delete dialog exists.
        onViewWaiting(withId(R.id.stop_showing_check_box), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));
        // Confirm the action.
        onView(withId(R.id.positive_button)).perform(click());
        // Verify the tab group was closed.
        verifyTabSwitcherCardCount(cta, 0);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID,
        ChromeFeatureList.TAB_GROUP_SYNC_ANDROID,
    })
    @DisabledTest(message = "crbug.com/360393681")
    public void testTabGroupOverflowMenuInTabSwitcher_noDeleteIncognito() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        prepareTabs(1, 2, "about:blank");
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        verifyGroupVisualDataDialogOpenedAndDismiss(cta);
        verifyTabSwitcherCardCount(cta, 1);

        verifyFirstCardTitle("2 tabs");
        // Ignore color because incognito's color state list is different.

        // Click the delete action button to close the group
        String deleteButtonText = cta.getString(R.string.delete_tab_group_menu_item);
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(deleteButtonText), withId(R.id.menu_item_text)))
                .check(doesNotExist());

        String closeButtonText = cta.getString(R.string.close_tab_group_menu_item);
        onView(allOf(withText(closeButtonText), withId(R.id.menu_item_text))).perform(click());

        // Closing the last incognito tabs will select the regular tab model.
        CriteriaHelper.pollUiThread(
                () -> {
                    return !cta.getTabModelSelectorSupplier()
                            .get()
                            .isIncognitoBrandedModelSelected();
                });

        // Verify the regular tab model still has a tab.
        verifyTabSwitcherCardCount(cta, 1);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID,
        ChromeFeatureList.TAB_GROUP_SYNC_ANDROID,
    })
    @DisabledTest(message = "crbug.com/353463207")
    public void testTabGroupOverflowMenuInTabSwitcher_deleteGroupDecline() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        verifyGroupVisualDataDialogOpenedAndDismiss(cta);
        verifyTabSwitcherCardCount(cta, 1);

        verifyFirstCardTitle("2 tabs");
        verifyFirstCardColor(TabGroupColorId.GREY);

        // Click the delete action button to close the group
        String deleteButtonText = cta.getString(R.string.delete_tab_group_menu_item);
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(deleteButtonText), withId(R.id.menu_item_text))).perform(click());

        // Verify that the modal dialog is now showing.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        // Verify the delete dialog exists.
        onViewWaiting(withId(R.id.stop_showing_check_box), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));
        // Decline the action.
        Espresso.pressBack();
        // Verify the tab group was not closed.
        verifyTabSwitcherCardCount(cta, 1);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID,
        ChromeFeatureList.TAB_GROUP_SYNC_ANDROID,
    })
    @DisabledTest(message = "crbug.com/360393681")
    public void testTabGroupOverflowMenuInTabSwitcher_deleteGroupDoNotShowAgain() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        String expectedDescription = "Open the tab group action menu for tab group Test";
        SnackbarManager snackbarManager = cta.getSnackbarManager();
        createTabs(cta, false, 4);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 4);

        // Merge last two tabs into a group.
        TabListEditorTestingRobot robot = new TabListEditorTestingRobot();
        enterTabListEditor(cta);
        robot.resultRobot.verifyTabListEditorIsVisible();

        robot.actionRobot.clickItemAtAdapterPosition(2);
        robot.actionRobot.clickItemAtAdapterPosition(3);
        robot.actionRobot.clickToolbarMenuButton().clickToolbarMenuItem("Group tabs");
        robot.resultRobot.verifyTabListEditorIsHidden();

        // Verify the visual data dialog exists.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        onViewWaiting(withId(R.id.visual_data_dialog_layout), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));

        // Change the title.
        editGroupVisualDataDialogTitle(cta, "Test");
        // Accept the change.
        onView(withId(R.id.positive_button)).perform(click());
        verifyModalDialogHidingAnimationCompleteInTabSwitcher();
        verifyTabSwitcherCardCount(cta, 3);

        // Merge first two tabs into a group.
        robot = new TabListEditorTestingRobot();
        enterTabListEditor(cta);
        robot.resultRobot.verifyTabListEditorIsVisible();

        robot.actionRobot.clickItemAtAdapterPosition(0);
        robot.actionRobot.clickItemAtAdapterPosition(1);
        robot.actionRobot.clickToolbarMenuButton().clickToolbarMenuItem("Group tabs");
        robot.resultRobot.verifyTabListEditorIsHidden();

        verifyGroupVisualDataDialogOpenedAndDismiss(cta);
        verifyTabSwitcherCardCount(cta, 2);

        // Click the delete action button to close the group "Test"
        String deleteButtonText = cta.getString(R.string.delete_tab_group_menu_item);
        onView(allOf(withContentDescription(expectedDescription), withId(R.id.action_button)))
                .perform(click());
        onView(allOf(withText(deleteButtonText), withId(R.id.menu_item_text))).perform(click());

        // Verify that the modal dialog is now showing.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        // Verify the delete dialog exists.
        onViewWaiting(withId(R.id.stop_showing_check_box), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));
        // Select the checkbox.
        onView(withId(R.id.stop_showing_check_box)).perform(click());
        // Confirm the action.
        onView(withId(R.id.positive_button)).perform(click());
        // Verify the tab group was closed.
        verifyTabSwitcherCardCount(cta, 1);

        // Click the delete action button to delete the group
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(deleteButtonText), withId(R.id.menu_item_text))).perform(click());
        // Verify the delete dialog does not exist.
        onView(withId(R.id.stop_showing_check_box)).check(doesNotExist());
        // Verify the tab group was closed.
        assertTrue(
                snackbarManager.getCurrentSnackbarForTesting().getController()
                        instanceof UndoBarController);
        verifyTabSwitcherCardCount(cta, 0);
        CriteriaHelper.pollInstrumentationThread(TabUiTestHelper::verifyUndoBarShowingAndClickUndo);
        verifyTabSwitcherCardCount(cta, 1);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID,
        ChromeFeatureList.TAB_GROUP_CREATION_DIALOG_ANDROID,
    })
    @DisableFeatures({
        ChromeFeatureList.TAB_GROUP_SYNC_ANDROID,
    })
    public void testTabGroupDialogSingleTab() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 1);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 1);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 1);
        verifyGroupVisualDataDialogOpenedAndDismiss(cta);
        verifyTabSwitcherCardCount(cta, 1);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID,
    })
    @DisableFeatures({
        ChromeFeatureList.TAB_GROUP_SYNC_ANDROID,
        ChromeFeatureList.TAB_GROUP_CREATION_DIALOG_ANDROID,
    })
    public void testNoTabGroupDialogSingleTab() {
        TabGroupModelFilter.SKIP_TAB_GROUP_CREATION_DIALOG.setForTesting(true);

        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        SnackbarManager snackbarManager = mActivityTestRule.getActivity().getSnackbarManager();
        createTabs(cta, false, 1);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 1);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 1);
        // Verify the undo group merge snackbar is showing.
        assertTrue(
                snackbarManager.getCurrentSnackbarForTesting().getController()
                        instanceof UndoGroupSnackbarController);
        // Verify that no modal dialog was shown.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mModalDialogManager.isShowing(), Matchers.is(false));
                });
        verifyTabSwitcherCardCount(cta, 1);

        TabGroupModelFilter.SKIP_TAB_GROUP_CREATION_DIALOG.setForTesting(false);
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID,
    })
    @DisableFeatures({
        ChromeFeatureList.TAB_GROUP_SYNC_ANDROID,
    })
    @DisabledTest(message = "crbug.com/360393681")
    public void testTabGroupOverflowMenuInTabSwitcher_deleteGroupNoShowSyncDisabled() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        verifyGroupVisualDataDialogOpenedAndDismiss(cta);
        verifyTabSwitcherCardCount(cta, 1);

        // Verify the delete action button does not exist
        String deleteButtonText = cta.getString(R.string.delete_tab_group_menu_item);
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(deleteButtonText), withId(R.id.menu_item_text)))
                .check(doesNotExist());
    }

    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
    @DisabledTest(message = "crbug.com/360393681")
    public void testTabGroupColorInTabSwitcher() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);

        // Expect that the the dialog is dismissed via another action.
        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.TabGroupParity.TabGroupCreationDialogResultAction", 2);

        verifyGroupVisualDataDialogOpenedAndDismiss(cta);

        // Verify the color icon exists.
        onView(allOf(withId(R.id.tab_favicon), withParent(withId(R.id.card_view))))
                .check(matches(isDisplayed()));
        watcher.assertExpected();
    }

    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
    @DisabledTest(message = "crbug.com/360393681")
    public void testTabGroupCreation_acceptInputValues() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);

        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        // Verify the visual data dialog exists.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        onViewWaiting(withId(R.id.visual_data_dialog_layout), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));

        // Change the title.
        editGroupVisualDataDialogTitle(cta, "Test");
        // Change the color.
        String blueColor =
                cta.getString(R.string.accessibility_tab_group_color_picker_color_item_blue);
        String notSelectedStringBlue =
                cta.getString(
                        R.string
                                .accessibility_tab_group_color_picker_color_item_not_selected_description,
                        blueColor);
        onView(withContentDescription(notSelectedStringBlue)).perform(click());

        // Expect a changed color and title selection to be recorded and an acceptance action.
        var histograms =
                HistogramWatcher.newBuilder()
                        .expectIntRecord(
                                "Android.TabGroupParity.TabGroupCreationFinalSelections", 3)
                        .expectIntRecord(
                                "Android.TabGroupParity.TabGroupCreationDialogResultAction", 0)
                        .build();

        // Accept the change.
        onView(withId(R.id.positive_button)).perform(click());
        verifyModalDialogHidingAnimationCompleteInTabSwitcher();
        // Check that the title change is reflected.
        verifyFirstCardTitle("Test");
        // Verify the color icon exists.
        verifyFirstCardColor(TabGroupColorId.BLUE);
        histograms.assertExpected();
    }

    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
    @DisabledTest(message = "crbug.com/360393681")
    public void testTabGroupCreation_acceptNullTitle() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);

        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        // Verify the visual data dialog exists.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        onViewWaiting(withId(R.id.visual_data_dialog_layout), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));

        // Close the soft keyboard that appears when the dialog is shown.
        closeSoftKeyboard();

        // Expect changed color and title selection to be recorded.
        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.TabGroupParity.TabGroupCreationFinalSelections", 0);

        // Accept without changing the title.
        onView(withId(R.id.positive_button)).perform(click());
        verifyModalDialogHidingAnimationCompleteInTabSwitcher();

        // Check that the title change is reflected.
        verifyFirstCardTitle("2 tabs");
        verifyFirstCardColor(TabGroupColorId.GREY);
        watcher.assertExpected();
    }

    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
    @DisabledTest(message = "crbug.com/360393681")
    public void testTabGroupCreation_dismissEmptyTitle() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);

        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        // Verify the visual data dialog exists.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        onViewWaiting(withId(R.id.visual_data_dialog_layout), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));

        // Close the soft keyboard that appears when the dialog is shown.
        closeSoftKeyboard();

        // Change the title.
        editGroupVisualDataDialogTitle(cta, "");
        // Change the color.
        String blueColor =
                cta.getString(R.string.accessibility_tab_group_color_picker_color_item_blue);
        String notSelectedStringBlue =
                cta.getString(
                        R.string
                                .accessibility_tab_group_color_picker_color_item_not_selected_description,
                        blueColor);
        onView(withContentDescription(notSelectedStringBlue)).perform(click());

        // Enact a backpress to dismiss the dialog.
        Espresso.pressBack();
        verifyModalDialogHidingAnimationCompleteInTabSwitcher();
        // Check that the title change has reverted to the default null state.
        verifyFirstCardTitle("2 tabs");
    }

    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
    @DisabledTest(message = "Flaky - crbug.com/353463207")
    public void testTabGroupCreation_rejectInvalidTitle() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);

        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        // Verify the visual data dialog exists.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        onViewWaiting(withId(R.id.visual_data_dialog_layout), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));

        // Change the title and accept.
        editGroupVisualDataDialogTitle(cta, "");
        onView(withId(R.id.positive_button)).perform(click());

        // Verify that the change was rejected and the dialog is still showing.
        onViewWaiting(withId(R.id.visual_data_dialog_layout), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));

        // Exit the dialog.
        Espresso.pressBack();
        verifyModalDialogHidingAnimationCompleteInTabSwitcher();
    }

    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
    @DisabledTest(message = "Flaky - crbug.com/353463207")
    public void testTabGroupCreation_dismissSavesState() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);

        // Create a tab group.
        mergeNormalTabsToAGroupWithDialog(cta, 2);
        // Verify the visual data dialog exists.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        onViewWaiting(withId(R.id.visual_data_dialog_layout), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));

        // Change the title.
        editGroupVisualDataDialogTitle(cta, "Test");
        // Change the color.
        String blueColor =
                cta.getString(R.string.accessibility_tab_group_color_picker_color_item_blue);
        String notSelectedStringBlue =
                cta.getString(
                        R.string
                                .accessibility_tab_group_color_picker_color_item_not_selected_description,
                        blueColor);
        onView(withContentDescription(notSelectedStringBlue)).perform(click());

        // Expect that the dismiss action is recorded.
        HistogramWatcher watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.TabGroupParity.TabGroupCreationDialogResultAction", 1);

        // Enact a backpress to dismiss the dialog.
        Espresso.pressBack();
        verifyModalDialogHidingAnimationCompleteInTabSwitcher();
        // Check that the title change is reflected.
        verifyFirstCardTitle("Test");
        // Verify the color icon exists.
        verifyFirstCardColor(TabGroupColorId.BLUE);
        watcher.assertExpected();
    }

    @Test
    @MediumTest
    public void testLongPressTab_entryInTabSwitcher_verifyNoSelectionOccurs() {
        TabUiFeatureUtilities.setTabListEditorLongPressEntryEnabledForTesting(true);

        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);

        // LongPress entry to TabListEditor.
        onView(tabSwitcherViewMatcher())
                .perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));

        TabListEditorTestingRobot mSelectionEditorRobot = new TabListEditorTestingRobot();
        mSelectionEditorRobot.resultRobot.verifyTabListEditorIsVisible();

        // Verify no selection action occurred to switch the selected tab in the tab model
        Criteria.checkThat(
                mActivityTestRule.getActivity().getCurrentTabModel().index(), Matchers.is(1));
    }

    @Test
    @MediumTest
    public void testLongPressTabGroup_entryInTabSwitcher() {
        TabUiFeatureUtilities.setTabListEditorLongPressEntryEnabledForTesting(true);

        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);

        // Create a tab group.
        mergeAllNormalTabsToAGroup(cta);
        verifyTabSwitcherCardCount(cta, 1);

        // LongPress entry to TabListEditor.
        onView(tabSwitcherViewMatcher())
                .perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));

        TabListEditorTestingRobot mSelectionEditorRobot = new TabListEditorTestingRobot();
        mSelectionEditorRobot.resultRobot.verifyTabListEditorIsVisible();
    }

    @Test
    @MediumTest
    public void testLongPressTab_verifyPostLongPressClickNoSelectionEditor() {
        TabUiFeatureUtilities.setTabListEditorLongPressEntryEnabledForTesting(true);

        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 2);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);

        // LongPress entry to TabListEditor.
        onView(tabSwitcherViewMatcher())
                .perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));

        TabListEditorTestingRobot mSelectionEditorRobot = new TabListEditorTestingRobot();
        mSelectionEditorRobot.resultRobot.verifyTabListEditorIsVisible();
        mSelectionEditorRobot.actionRobot.clickItemAtAdapterPosition(0);
        mSelectionEditorRobot.resultRobot.verifyItemSelectedAtAdapterPosition(0);
        Espresso.pressBack();

        onView(tabSwitcherViewMatcher())
                .perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
        // Check the selected tab in the tab model switches from the second tab to the first to
        // verify clicking the tab worked.
        Criteria.checkThat(
                mActivityTestRule.getActivity().getCurrentTabModel().index(), Matchers.is(0));
    }

    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
    public void testUndoGroupMergeInTabSwitcher_TabToGroupAdjacent() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        SnackbarManager snackbarManager = mActivityTestRule.getActivity().getSnackbarManager();
        createTabs(cta, false, 3);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 3);

        // Get the next suggested color id.
        TabGroupModelFilter filter = getTabGroupModelFilter();
        int nextSuggestedColorId = TabGroupColorUtils.getNextSuggestedColorId(filter);

        // Merge first two tabs into a group.
        TabModel normalTabModel = cta.getTabModelSelector().getModel(false);
        List<Tab> tabGroup =
                new ArrayList<>(
                        Arrays.asList(normalTabModel.getTabAt(0), normalTabModel.getTabAt(1)));
        createTabGroup(cta, false, tabGroup);
        ThreadUtils.runOnUiThreadBlocking(
                () -> filter.setTabGroupTitle(normalTabModel.getTabAt(0).getRootId(), "Foo"));
        verifyTabSwitcherCardCount(cta, 2);

        // Assert default color was set properly.
        assertEquals(
                nextSuggestedColorId,
                filter.getTabGroupColor(normalTabModel.getTabAt(0).getRootId()));

        // Merge tab group of 2 at first index with the 3rd tab.
        mergeAllNormalTabsToAGroup(cta);
        assertTrue(
                snackbarManager.getCurrentSnackbarForTesting().getController()
                        instanceof UndoGroupSnackbarController);

        // Assert the default color is still the tab group color
        assertEquals(
                nextSuggestedColorId,
                filter.getTabGroupColor(normalTabModel.getTabAt(0).getRootId()));

        // Undo merge in tab switcher.
        verifyTabSwitcherCardCount(cta, 1);
        assertEquals("3", snackbarManager.getCurrentSnackbarForTesting().getTextForTesting());
        CriteriaHelper.pollInstrumentationThread(TabUiTestHelper::verifyUndoBarShowingAndClickUndo);
        verifyTabSwitcherCardCount(cta, 2);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    assertEquals(
                            "Foo", filter.getTabGroupTitle(normalTabModel.getTabAt(1).getRootId()));
                    assertNull(filter.getTabGroupTitle(normalTabModel.getTabAt(2).getRootId()));
                    assertEquals(
                            nextSuggestedColorId,
                            filter.getTabGroupColor(normalTabModel.getTabAt(1).getRootId()));
                    assertEquals(
                            TabGroupColorUtils.INVALID_COLOR_ID,
                            filter.getTabGroupColor(normalTabModel.getTabAt(2).getRootId()));
                });
    }

    @Test
    @MediumTest
    @EnableFeatures({
        ChromeFeatureList.TAB_GROUP_PARITY_ANDROID,
        ChromeFeatureList.TAB_GROUP_PANE_ANDROID
    })
    public void testGroupMerge_UndoBarGoneAfterManualUngroup() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        createTabs(cta, false, 3);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 3);

        // Merge first two tabs into a group.
        TabModel normalTabModel = cta.getTabModelSelector().getModel(false);
        List<Tab> tabGroup =
                new ArrayList<>(
                        Arrays.asList(normalTabModel.getTabAt(0), normalTabModel.getTabAt(1)));
        createTabGroup(cta, false, tabGroup);

        // Merge tab group of 2 at first index with the 3rd tab.
        mergeAllNormalTabsToAGroup(cta);

        onViewWaiting(withId(R.id.snackbar_button)).check(matches(isCompletelyDisplayed()));

        String ungroupButtonText = cta.getString(R.string.ungroup_tab_group_menu_item);
        onView(withId(R.id.action_button)).perform(click());
        onView(allOf(withText(ungroupButtonText), withId(R.id.menu_item_text))).perform(click());

        onView(withId(R.id.snackbar_button)).check(doesNotExist());
    }

    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
    public void testUndoGroupMergeInTabSwitcher_GroupToGroupNonAdjacent() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        SnackbarManager snackbarManager = mActivityTestRule.getActivity().getSnackbarManager();
        createTabs(cta, false, 5);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 5);

        // Get the next suggested color id.
        TabGroupModelFilter filter =
                (TabGroupModelFilter)
                        cta.getTabModelSelectorSupplier()
                                .get()
                                .getTabModelFilterProvider()
                                .getCurrentTabModelFilter();
        int nextSuggestedColorId1 = TabGroupColorUtils.getNextSuggestedColorId(filter);

        // Merge last two tabs into a group.
        TabModel normalTabModel = cta.getTabModelSelector().getModel(false);
        List<Tab> tabGroup =
                new ArrayList<>(
                        Arrays.asList(normalTabModel.getTabAt(3), normalTabModel.getTabAt(4)));
        createTabGroup(cta, false, tabGroup);
        verifyTabSwitcherCardCount(cta, 4);

        // Assert default color 1 was set properly.
        assertEquals(
                nextSuggestedColorId1,
                filter.getTabGroupColor(normalTabModel.getTabAt(4).getRootId()));

        // Get the next suggested color id.
        int nextSuggestedColorId2 =
                TabGroupColorUtils.getNextSuggestedColorId(
                        (TabGroupModelFilter)
                                cta.getTabModelSelectorSupplier()
                                        .get()
                                        .getTabModelFilterProvider()
                                        .getCurrentTabModelFilter());

        // Merge first two tabs into a group.
        List<Tab> tabGroup2 =
                new ArrayList<>(
                        Arrays.asList(normalTabModel.getTabAt(0), normalTabModel.getTabAt(1)));
        createTabGroup(cta, false, tabGroup2);
        verifyTabSwitcherCardCount(cta, 3);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    filter.setTabGroupTitle(normalTabModel.getTabAt(3).getRootId(), "Foo");
                    filter.setTabGroupTitle(normalTabModel.getTabAt(1).getRootId(), "Bar");
                });

        // Assert default color 2 was set properly.
        assertEquals(
                nextSuggestedColorId2,
                filter.getTabGroupColor(normalTabModel.getTabAt(1).getRootId()));

        // Merge the two tab groups into a group.
        List<Tab> tabGroup3 =
                new ArrayList<>(
                        Arrays.asList(normalTabModel.getTabAt(0), normalTabModel.getTabAt(3)));
        createTabGroup(cta, false, tabGroup3);
        assertTrue(
                snackbarManager.getCurrentSnackbarForTesting().getController()
                        instanceof UndoGroupSnackbarController);

        // Assert default color 2 was set as the overall merged group color.
        assertEquals(
                nextSuggestedColorId2,
                filter.getTabGroupColor(normalTabModel.getTabAt(3).getRootId()));

        // Undo merge in tab switcher.
        verifyTabSwitcherCardCount(cta, 2);
        assertEquals("4", snackbarManager.getCurrentSnackbarForTesting().getTextForTesting());
        CriteriaHelper.pollInstrumentationThread(TabUiTestHelper::verifyUndoBarShowingAndClickUndo);
        verifyTabSwitcherCardCount(cta, 3);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    assertEquals(
                            "Foo", filter.getTabGroupTitle(normalTabModel.getTabAt(4).getRootId()));
                    assertEquals(
                            "Bar", filter.getTabGroupTitle(normalTabModel.getTabAt(0).getRootId()));
                    assertEquals(
                            nextSuggestedColorId1,
                            filter.getTabGroupColor(normalTabModel.getTabAt(4).getRootId()));
                    assertEquals(
                            nextSuggestedColorId2,
                            filter.getTabGroupColor(normalTabModel.getTabAt(0).getRootId()));
                });
    }

    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
    public void testUndoGroupMergeInTabSwitcher_PostMergeGroupTitleCommit() {
        final ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        SnackbarManager snackbarManager = mActivityTestRule.getActivity().getSnackbarManager();
        createTabs(cta, false, 3);
        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 3);

        // Get the next suggested color id.
        TabGroupModelFilter filter =
                (TabGroupModelFilter)
                        cta.getTabModelSelectorSupplier()
                                .get()
                                .getTabModelFilterProvider()
                                .getCurrentTabModelFilter();
        int nextSuggestedColorId = TabGroupColorUtils.getNextSuggestedColorId(filter);

        // Merge first two tabs into a group.
        TabModel normalTabModel = cta.getTabModelSelector().getModel(false);
        List<Tab> tabGroup =
                new ArrayList<>(
                        Arrays.asList(normalTabModel.getTabAt(0), normalTabModel.getTabAt(1)));
        createTabGroup(cta, false, tabGroup);
        int[] ungroupedRootId = new int[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    filter.setTabGroupTitle(normalTabModel.getTabAt(0).getRootId(), "Foo");
                    ungroupedRootId[0] = normalTabModel.getTabAt(2).getRootId();
                    filter.setTabGroupTitle(ungroupedRootId[0], "Bar");
                });
        verifyTabSwitcherCardCount(cta, 2);

        // Assert default color was set properly.
        assertEquals(
                nextSuggestedColorId,
                filter.getTabGroupColor(normalTabModel.getTabAt(1).getRootId()));

        // Merge tab group of 2 at first index with the 3rd tab.
        mergeAllNormalTabsToAGroup(cta);
        assertTrue(
                snackbarManager.getCurrentSnackbarForTesting().getController()
                        instanceof UndoGroupSnackbarController);

        // Assert default color was set properly for the overall merged group.
        assertEquals(
                nextSuggestedColorId,
                filter.getTabGroupColor(normalTabModel.getTabAt(2).getRootId()));

        // Check that the old group title was handed over when the group merge is committed
        // and no longer exists.
        ThreadUtils.runOnUiThreadBlocking(() -> snackbarManager.dismissAllSnackbars());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    assertNull(filter.getTabGroupTitle(ungroupedRootId[0]));
                    assertEquals(
                            "Foo", filter.getTabGroupTitle(normalTabModel.getTabAt(0).getRootId()));
                });

        // Assert color still exists post snackbar dismissal.
        assertEquals(
                nextSuggestedColorId,
                filter.getTabGroupColor(normalTabModel.getTabAt(1).getRootId()));
    }

    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
    public void testUndoClosure_UndoGroupClosure() {
        ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        SnackbarManager snackbarManager = mActivityTestRule.getActivity().getSnackbarManager();
        TabGroupModelFilter filter = getTabGroupModelFilter();
        createTabs(cta, false, 2);

        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        assertNull(snackbarManager.getCurrentSnackbarForTesting());

        // Get the next suggested color id.
        int nextSuggestedColorId =
                TabGroupColorUtils.getNextSuggestedColorId(
                        (TabGroupModelFilter)
                                cta.getTabModelSelectorSupplier()
                                        .get()
                                        .getTabModelFilterProvider()
                                        .getCurrentTabModelFilter());

        // Merge first two tabs into a group.
        TabModel normalTabModel = cta.getTabModelSelector().getModel(false);
        List<Tab> tabGroup =
                new ArrayList<>(
                        Arrays.asList(normalTabModel.getTabAt(0), normalTabModel.getTabAt(1)));
        createTabGroup(cta, false, tabGroup);
        verifyTabSwitcherCardCount(cta, 1);

        // Assert default color was set properly.
        assertEquals(
                nextSuggestedColorId,
                filter.getTabGroupColor(normalTabModel.getTabAt(1).getRootId()));
        ThreadUtils.runOnUiThreadBlocking(() -> snackbarManager.dismissAllSnackbars());

        // Temporarily save the tab to get the rootId later.
        Tab tab2 = normalTabModel.getTabAt(1);

        closeFirstTabGroupInTabSwitcher(cta);
        assertTrue(
                snackbarManager.getCurrentSnackbarForTesting().getController()
                        instanceof UndoBarController);
        verifyTabSwitcherCardCount(cta, 0);

        // Default color should still persist, though the root id might change.
        assertEquals(nextSuggestedColorId, filter.getTabGroupColor(tab2.getRootId()));

        CriteriaHelper.pollInstrumentationThread(TabUiTestHelper::verifyUndoBarShowingAndClickUndo);
        verifyTabSwitcherCardCount(cta, 1);

        // Assert default color still persists.
        assertEquals(
                nextSuggestedColorId,
                filter.getTabGroupColor(normalTabModel.getTabAt(1).getRootId()));
    }

    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_PARITY_ANDROID})
    public void testUndoClosure_AcceptGroupClosure() {
        ChromeTabbedActivity cta = mActivityTestRule.getActivity();
        SnackbarManager snackbarManager = mActivityTestRule.getActivity().getSnackbarManager();
        TabGroupModelFilter filter = getTabGroupModelFilter();
        createTabs(cta, false, 2);

        enterTabSwitcher(cta);
        verifyTabSwitcherCardCount(cta, 2);
        assertNull(snackbarManager.getCurrentSnackbarForTesting());

        // Get the next suggested color id.
        int nextSuggestedColorId =
                TabGroupColorUtils.getNextSuggestedColorId(
                        (TabGroupModelFilter)
                                cta.getTabModelSelectorSupplier()
                                        .get()
                                        .getTabModelFilterProvider()
                                        .getCurrentTabModelFilter());

        // Merge first two tabs into a group.
        TabModel normalTabModel = cta.getTabModelSelector().getModel(false);
        List<Tab> tabGroup =
                new ArrayList<>(
                        Arrays.asList(normalTabModel.getTabAt(0), normalTabModel.getTabAt(1)));
        createTabGroup(cta, false, tabGroup);
        verifyTabSwitcherCardCount(cta, 1);

        // Assert default color was set properly.
        assertEquals(
                nextSuggestedColorId,
                filter.getTabGroupColor(normalTabModel.getTabAt(1).getRootId()));
        ThreadUtils.runOnUiThreadBlocking(() -> snackbarManager.dismissAllSnackbars());

        // Temporarily save the rootID to check during closure.
        Tab tab2 = normalTabModel.getTabAt(1);
        int groupRootId = tab2.getRootId();

        closeFirstTabGroupInTabSwitcher(cta);
        assertTrue(
                snackbarManager.getCurrentSnackbarForTesting().getController()
                        instanceof UndoBarController);
        verifyTabSwitcherCardCount(cta, 0);

        // Default color should still persist, though the root id might change.
        assertEquals(nextSuggestedColorId, filter.getTabGroupColor(tab2.getRootId()));

        ThreadUtils.runOnUiThreadBlocking(() -> snackbarManager.dismissAllSnackbars());

        // Assert default color is cleared.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    assertEquals(
                            TabGroupColorUtils.INVALID_COLOR_ID,
                            filter.getTabGroupColor(groupRootId));
                    assertEquals(
                            TabGroupColorUtils.INVALID_COLOR_ID,
                            filter.getTabGroupColor(tab2.getRootId()));
                });
    }

    private void enterTabListEditor(ChromeTabbedActivity cta) {
        MenuUtils.invokeCustomMenuActionSync(
                InstrumentationRegistry.getInstrumentation(), cta, R.id.menu_select_tabs);
    }

    private void simulateJpegHasCachedWithAspectRatio(double aspectRatio) throws IOException {
        TabModel currentModel = mActivityTestRule.getActivity().getCurrentTabModel();
        int jpegWidth = 125;
        int jpegHeight = (int) (jpegWidth * 1.0 / aspectRatio);
        for (int i = 0; i < currentModel.getCount(); i++) {
            Tab tab = currentModel.getTabAt(i);
            Bitmap bitmap = Bitmap.createBitmap(jpegWidth, jpegHeight, Config.ARGB_8888);
            encodeJpeg(tab, bitmap);
        }
    }

    private void simulateJpegHasCachedWithDefaultAspectRatio() throws IOException {
        simulateJpegHasCachedWithAspectRatio(
                TabUtils.getTabThumbnailAspectRatio(
                        mActivityTestRule.getActivity(),
                        mActivityTestRule.getActivity().getBrowserControlsManager()));
    }

    private void encodeJpeg(Tab tab, Bitmap bitmap) throws IOException {
        FileOutputStream outputStream =
                new FileOutputStream(TabContentManager.getTabThumbnailFileJpeg(tab.getId()));
        bitmap.compress(Bitmap.CompressFormat.JPEG, 50, outputStream);
        outputStream.close();
    }

    private Matcher<View> tabSwitcherViewMatcher() {
        return allOf(
                isDescendantOfA(
                        withId(
                                TabUiTestHelper.getTabSwitcherAncestorId(
                                        mActivityTestRule.getActivity()))),
                withId(R.id.tab_list_recycler_view));
    }

    private void mergeNormalTabsToAGroupWithDialog(ChromeTabbedActivity cta, int tabCount) {
        TabListEditorTestingRobot robot = new TabListEditorTestingRobot();
        enterTabListEditor(cta);
        robot.resultRobot.verifyTabListEditorIsVisible();

        for (int i = 0; i < tabCount; i++) {
            robot.actionRobot.clickItemAtAdapterPosition(i);
        }
        robot.actionRobot
                .clickToolbarMenuButton()
                .clickToolbarMenuItem(tabCount == 1 ? "Group tab" : "Group tabs");
        robot.resultRobot.verifyTabListEditorIsHidden();
    }

    private void verifyGroupVisualDataDialogOpenedAndDismiss(ChromeTabbedActivity cta) {
        // Verify that the modal dialog is now showing.
        verifyModalDialogShowingAnimationCompleteInTabSwitcher();
        // Verify the visual data dialog exists.
        onViewWaiting(withId(R.id.visual_data_dialog_layout), /* checkRootDialog= */ true)
                .check(matches(isDisplayed()));
        // Wait until the keyboard is showing.
        KeyboardVisibilityDelegate delegate = cta.getWindowAndroid().getKeyboardDelegate();
        CriteriaHelper.pollUiThread(
                () -> delegate.isKeyboardShowing(cta, cta.getCompositorViewHolderForTesting()));
        // Dismiss the tab group visual data dialog.
        dismissAllModalDialogs();
        // Verify that the modal dialog is now hidden.
        verifyModalDialogHidingAnimationCompleteInTabSwitcher();
    }

    private void editGroupVisualDataDialogTitle(ChromeTabbedActivity cta, String title) {
        onView(withId(R.id.title_input_text))
                .perform(click())
                .perform(replaceText(title))
                .perform(pressImeActionButton());
        // Wait until the keyboard is hidden to make sure the edit has taken effect.
        KeyboardVisibilityDelegate delegate = cta.getWindowAndroid().getKeyboardDelegate();
        CriteriaHelper.pollUiThread(
                () -> !delegate.isKeyboardShowing(cta, cta.getCompositorViewHolderForTesting()));
    }

    private void verifyFirstCardTitle(String title) {
        onView(
                        allOf(
                                isDescendantOfA(
                                        withId(
                                                TabUiTestHelper.getTabSwitcherAncestorId(
                                                        mActivityTestRule.getActivity()))),
                                withId(R.id.tab_list_recycler_view)))
                .check(
                        (v, noMatchException) -> {
                            if (noMatchException != null) throw noMatchException;

                            RecyclerView recyclerView = (RecyclerView) v;
                            TextView firstCardTitleTextView =
                                    recyclerView
                                            .findViewHolderForAdapterPosition(0)
                                            .itemView
                                            .findViewById(R.id.tab_title);
                            assertEquals(title, firstCardTitleTextView.getText().toString());
                        });
    }

    private void verifyFirstCardColor(@TabGroupColorId int color) {
        onView(allOf(withId(R.id.tab_favicon), withParent(withId(R.id.card_view))))
                .check(
                        (v, noMatchException) -> {
                            if (noMatchException != null) throw noMatchException;

                            ImageView imageView = (ImageView) v;
                            LayerDrawable layerDrawable = (LayerDrawable) imageView.getDrawable();
                            GradientDrawable drawable =
                                    (GradientDrawable) layerDrawable.getDrawable(1);

                            assertEquals(
                                    ColorStateList.valueOf(
                                            ColorPickerUtils.getTabGroupColorPickerItemColor(
                                                    mActivityTestRule.getActivity(), color, false)),
                                    drawable.getColor());
                        });
    }

    private void verifyModalDialogShowingAnimationCompleteInTabSwitcher() {
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mModalDialogManager.isShowing(), Matchers.is(true));
                });
    }

    private void verifyModalDialogHidingAnimationCompleteInTabSwitcher() {
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mModalDialogManager.isShowing(), Matchers.is(false));
                });
    }

    private void dismissAllModalDialogs() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mModalDialogManager.dismissAllDialogs(DialogDismissalCause.UNKNOWN);
                });
    }

    private TabGroupModelFilter getTabGroupModelFilter() {
        return (TabGroupModelFilter)
                mActivityTestRule
                        .getActivity()
                        .getTabModelSelectorSupplier()
                        .get()
                        .getTabModelFilterProvider()
                        .getCurrentTabModelFilter();
    }
}