chromium/chrome/android/javatests/src/org/chromium/chrome/browser/compositor/overlays/strip/TabStripGroupContextMenuTest.java

// Copyright 2024 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.compositor.overlays.strip;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
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.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.CoreMatchers.allOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;

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

import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.TabStripUtils;
import org.chromium.components.tab_groups.TabGroupColorId;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.test.util.UiRestriction;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/** Instrumentation tests for tab strip group title long-press menu popup */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
@EnableFeatures({
    ChromeFeatureList.TAB_GROUP_SYNC_ANDROID,
    ChromeFeatureList.TAB_STRIP_GROUP_CONTEXT_MENU
})
@Restriction(UiRestriction.RESTRICTION_TYPE_TABLET)
public class TabStripGroupContextMenuTest {
    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

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

    private StripLayoutHelper mStripLayoutHelper;
    private int mRootId;
    private ModalDialogManager mModalDialogManager;

    @Before
    public void setUp() throws Exception {
        mStripLayoutHelper =
                TabStripUtils.getActiveStripLayoutHelper(mActivityTestRule.getActivity());
        mModalDialogManager = mActivityTestRule.getActivity().getModalDialogManager();
    }

    @Test
    @SmallTest
    public void testOpenNewTabInGroup() {
        // Prepare standard state and show menu.
        prepareStandardState();
        showMenu();

        // Assert there are 2 grouped tabs.
        TabGroupModelFilter tabGroupModelFilter = getTabGroupModelFilter(false);
        int tabCount = tabGroupModelFilter.getRelatedTabCountForRootId(mRootId);
        assertEquals("There should be 2 tabs in group", 2, tabCount);

        // Verify and click "New tab in group".
        onView(withText(R.string.open_new_tab_in_group_context_menu_item))
                .check(matches(isDisplayed()));
        onView(withText(R.string.open_new_tab_in_group_context_menu_item)).perform(click());

        // Verify the grouped tab count is incremented.
        assertEquals(
                "There should be 3 tabs in group",
                tabCount + 1,
                tabGroupModelFilter.getRelatedTabCountForRootId(mRootId));
    }

    @Test
    @SmallTest
    public void testUngroup() {
        // Prepare standard state and show menu.
        prepareStandardState();
        showMenu();

        // Assert there are 2 grouped tabs.
        TabGroupModelFilter tabGroupModelFilter = getTabGroupModelFilter(false);
        assertEquals(
                "There should be 2 tabs in group",
                2,
                tabGroupModelFilter.getRelatedTabCountForRootId(mRootId));

        // Verify and click "Ungroup".
        onView(withText(R.string.ungroup_tab_group_menu_item)).check(matches(isDisplayed()));
        onView(withText(R.string.ungroup_tab_group_menu_item)).perform(click());

        // Verify confirmation dialog is showing and tab group is ungrouped after confirming the
        // action.
        verifyModalDialog(/* shouldShow= */ true);
        onView(withText(R.string.ungroup_tab_group_action)).perform(click());
        assertEquals(
                "Tab group should be ungrouped",
                1,
                tabGroupModelFilter.getRelatedTabCountForRootId(mRootId));

        // Verify no tab group exists.
        TabModel tabModel = mActivityTestRule.getActivity().getCurrentTabModel();
        for (int i = 0; i < tabModel.getCount(); i++) {
            assertFalse(
                    "Tab should not be grouped",
                    tabGroupModelFilter.isTabInTabGroup(tabModel.getTabAt(i)));
        }
    }

    @Test
    @SmallTest
    public void testUngroup_Incognito() {
        // Prepare incognito state and show menu.
        prepareIncognitoState();
        showMenu();

        // Verify "Delete group" option should not be showing for incognito.
        onView(withText(R.string.tab_grid_dialog_toolbar_delete_group)).check(doesNotExist());

        // Assert there are 2 grouped tabs.
        TabGroupModelFilter tabGroupModelFilter = getTabGroupModelFilter(true);
        assertEquals(
                "There should be 2 tabs in group",
                2,
                tabGroupModelFilter.getRelatedTabCountForRootId(mRootId));

        // Verify and click "Ungroup".
        onView(withText(R.string.ungroup_tab_group_menu_item)).check(matches(isDisplayed()));
        onView(withText(R.string.ungroup_tab_group_menu_item)).perform(click());

        // Verify confirmation dialog is not showing for incognito and tab group is immediately
        // ungrouped.
        verifyModalDialog(/* shouldShow= */ false);
        assertEquals(
                "Tab group should be ungrouped",
                1,
                tabGroupModelFilter.getRelatedTabCountForRootId(mRootId));

        // Verify no tab group exists.
        TabModel tabModel = mActivityTestRule.getActivity().getCurrentTabModel();
        for (int i = 0; i < tabModel.getCount(); i++) {
            assertFalse(
                    "Tab should not be grouped",
                    tabGroupModelFilter.isTabInTabGroup(tabModel.getTabAt(i)));
        }
    }

    @Test
    @SmallTest
    public void testCloseGroup() {
        // Prepare standard state and show menu.
        prepareStandardState();
        showMenu();

        // Assert there are 2 grouped tabs.
        TabGroupModelFilter tabGroupModelFilter = getTabGroupModelFilter(false);
        assertEquals(
                "There should be 2 tabs in group",
                2,
                tabGroupModelFilter.getRelatedTabCountForRootId(mRootId));

        // Assert last tab is an ungrouped tab.
        TabModel tabModel = mActivityTestRule.getActivity().getCurrentTabModel();
        Tab ungroupedTab = tabModel.getTabAt(2);
        assertFalse(
                "Last tab should not be grouped",
                tabGroupModelFilter.isTabInTabGroup(ungroupedTab));

        // Verify and click "Close group".
        onView(withText(R.string.tab_grid_dialog_toolbar_close_group))
                .check(matches(isDisplayed()));
        onView(withText(R.string.tab_grid_dialog_toolbar_close_group)).perform(click());

        // Assert tab group is closed and undo option showed.
        assertFalse(
                "Tab group should be closed", tabGroupModelFilter.tabGroupExistsForRootId(mRootId));
        assertEquals("Expected only one tab to be present", 1, tabModel.getCount());
        assertEquals(
                "Expected the only tab remain is the ungrouped tab",
                ungroupedTab.getId(),
                tabModel.getTabAt(0).getId());
        onView(withText("Undo")).check(matches(isDisplayed()));
    }

    @Test
    @SmallTest
    public void testCloseGroup_Incognito() {
        // Prepare incognito state and show menu.
        prepareIncognitoState();
        showMenu();

        // Verify "Delete group" option should not be showing for incognito.
        onView(withText(R.string.tab_grid_dialog_toolbar_delete_group)).check(doesNotExist());

        // Assert there are 2 grouped tabs.
        TabGroupModelFilter tabGroupModelFilter = getTabGroupModelFilter(true);
        assertEquals(
                "There should be 2 tabs in group",
                2,
                tabGroupModelFilter.getRelatedTabCountForRootId(mRootId));

        // Assert last tab is an ungrouped tab.
        TabModel tabModel = mActivityTestRule.getActivity().getCurrentTabModel();
        Tab ungroupedTab = tabModel.getTabAt(2);
        assertFalse(
                "Last tab should not be grouped",
                tabGroupModelFilter.isTabInTabGroup(ungroupedTab));

        // Verify and click "Close group".
        onView(withText(R.string.tab_grid_dialog_toolbar_close_group))
                .check(matches(isDisplayed()));
        onView(withText(R.string.tab_grid_dialog_toolbar_close_group)).perform(click());

        // Assert tab group is closed and undo option not showed.
        assertFalse(
                "Tab group should be closed", tabGroupModelFilter.tabGroupExistsForRootId(mRootId));
        assertEquals("Expected only one tab to be present", 1, tabModel.getCount());
        assertEquals(
                "Expected the only tab remain is the ungrouped tab",
                ungroupedTab.getId(),
                tabModel.getTabAt(0).getId());
        onView(withText("Undo")).check(doesNotExist());
    }

    @Test
    @SmallTest
    public void testDeleteGroup() {
        // Prepare standard state and show menu.
        prepareStandardState();
        showMenu();

        // Assert there are 2 grouped tabs.
        TabGroupModelFilter tabGroupModelFilter = getTabGroupModelFilter(false);
        assertEquals(
                "There should be 2 tabs in group",
                2,
                tabGroupModelFilter.getRelatedTabCountForRootId(mRootId));

        // Assert last tab is an ungrouped tab.
        TabModel tabModel = mActivityTestRule.getActivity().getCurrentTabModel();
        Tab ungroupedTab = tabModel.getTabAt(2);
        assertFalse(
                "Last tab should not be grouped",
                tabGroupModelFilter.isTabInTabGroup(ungroupedTab));

        // Verify and click "Delete group".
        onView(withText(R.string.tab_grid_dialog_toolbar_delete_group))
                .check(matches(isDisplayed()));
        onView(withText(R.string.tab_grid_dialog_toolbar_delete_group)).perform(click());

        // Verify confirmation dialog is showing and tab group is deleted after confirming the
        // action.
        verifyModalDialog(/* shouldShow= */ true);
        onView(withText(R.string.delete_tab_group_action)).perform(click());
        assertFalse(
                "Tab group should be deleted",
                tabGroupModelFilter.tabGroupExistsForRootId(mRootId));
        assertEquals("Expected only one tab to be present", 1, tabModel.getCount());
        assertEquals(
                "Expected the only tab remain is the ungrouped tab",
                ungroupedTab.getId(),
                tabModel.getTabAt(0).getId());
    }

    @Test
    @SmallTest
    public void testUpdateAndDeleteGroupTitle() {
        // Prepare standard state and show menu.
        prepareStandardState();
        showMenu();

        // Verify default group title.
        String title = "2 tabs";
        onView(withText(title)).check(matches(isDisplayed()));

        // Update tab group title.
        title = "newTitle";
        onView(withId(R.id.tab_group_title))
                .perform(click())
                .perform(replaceText(title))
                .perform(pressImeActionButton());

        // Wait until the keyboard is hidden to make sure the edit has taken effect.
        KeyboardVisibilityDelegate delegate =
                mActivityTestRule.getActivity().getWindowAndroid().getKeyboardDelegate();
        CriteriaHelper.pollUiThread(
                () ->
                        !delegate.isKeyboardShowing(
                                mActivityTestRule.getActivity(),
                                mActivityTestRule
                                        .getActivity()
                                        .getCompositorViewHolderForTesting()));

        // Verify the tab group title is updated.
        onView(withText(title)).check(matches(isDisplayed()));

        // Delete the group title by clearing the edit box and verify its default to "N tabs".
        title = "";
        onView(withId(R.id.tab_group_title))
                .perform(click())
                .perform(replaceText(title))
                .perform(pressImeActionButton());

        // Wait until the keyboard is hidden to make sure the edit has taken effect.
        CriteriaHelper.pollUiThread(
                () ->
                        !delegate.isKeyboardShowing(
                                mActivityTestRule.getActivity(),
                                mActivityTestRule
                                        .getActivity()
                                        .getCompositorViewHolderForTesting()));

        // Verify the tab group title is deleted and default to "N tabs.
        onView(withText("2 tabs")).check(matches(isDisplayed()));

        // Click anything to dismiss the menu.
        onView(withText(R.string.open_new_tab_in_group_context_menu_item))
                .check(matches(isDisplayed()));
        onView(withText(R.string.open_new_tab_in_group_context_menu_item)).perform(click());
    }

    @Test
    @SmallTest
    public void testUpdateGroupColor() {
        // Prepare standard state and show menu.
        prepareStandardState();
        showMenu();
        TabGroupModelFilter tabGroupModelFilter = getTabGroupModelFilter(false);

        // Verify the default grey color is selected.
        assertEquals(
                "The default grey color should be selected",
                TabGroupColorId.GREY,
                tabGroupModelFilter.getTabGroupColor(mRootId));

        // Select the blue color.
        String blueColor =
                mActivityTestRule
                        .getActivity()
                        .getString(R.string.accessibility_tab_group_color_picker_color_item_blue);
        String notSelectedStringBlue =
                mActivityTestRule
                        .getActivity()
                        .getString(
                                R.string
                                        .accessibility_tab_group_color_picker_color_item_not_selected_description,
                                blueColor);
        onView(withContentDescription(notSelectedStringBlue)).perform(click());

        // Verify the blue color is selected.
        assertEquals(
                "The blue color should be selected",
                TabGroupColorId.BLUE,
                tabGroupModelFilter.getTabGroupColor(mRootId));

        // Click anything to dismiss the menu.
        onView(withText(R.string.open_new_tab_in_group_context_menu_item))
                .check(matches(isDisplayed()));
        onView(withText(R.string.open_new_tab_in_group_context_menu_item)).perform(click());
    }

    private void prepareStandardState() {
        // 1. Create 2 more normal new tabs, for a total 3 normal tabs.
        ChromeTabUtils.newTabFromMenu(
                InstrumentationRegistry.getInstrumentation(), mActivityTestRule.getActivity());
        ChromeTabUtils.newTabFromMenu(
                InstrumentationRegistry.getInstrumentation(), mActivityTestRule.getActivity());

        // 2. Assert the normal tab strip is selected and there are 3 normal tabs in total.
        assertFalse(
                "Expected normal strip to be selected",
                mActivityTestRule.getActivity().getTabModelSelector().isIncognitoSelected());
        assertEquals(
                "There should be three tabs present",
                3,
                mActivityTestRule.getActivity().getCurrentTabModel().getCount());

        // 3. Create tab group with 2 tabs.
        groupFirstTwoTabs(/* isIncognito= */ false);
    }

    private void prepareIncognitoState() {
        // 1. create 3 incognito tabs
        ChromeTabUtils.newTabFromMenu(
                InstrumentationRegistry.getInstrumentation(),
                mActivityTestRule.getActivity(),
                /* incognito= */ true,
                /* waitForNtpLoad= */ true);
        ChromeTabUtils.newTabFromMenu(
                InstrumentationRegistry.getInstrumentation(),
                mActivityTestRule.getActivity(),
                /* incognito= */ true,
                /* waitForNtpLoad= */ true);
        ChromeTabUtils.newTabFromMenu(
                InstrumentationRegistry.getInstrumentation(),
                mActivityTestRule.getActivity(),
                /* incognito= */ true,
                /* waitForNtpLoad= */ true);

        // 2. Assert the incognito tab strip is selected and there are 3 incognito tabs in total.
        Assert.assertTrue(
                "Expected incognito strip to be selected",
                mActivityTestRule.getActivity().getTabModelSelector().isIncognitoSelected());
        assertEquals(
                "There are 3 incognito tabs present",
                3,
                mActivityTestRule.getActivity().getCurrentTabModel().getCount());

        // 3. Create an incognito tab group with 2 tabs.
        groupFirstTwoTabs(/* isIncognito= */ true);
    }

    private TabGroupModelFilter getTabGroupModelFilter(boolean isIncognito) {
        assert mActivityTestRule
                        .getActivity()
                        .getTabModelSelector()
                        .getTabModelFilterProvider()
                        .getTabModelFilter(isIncognito)
                instanceof TabGroupModelFilter;
        return (TabGroupModelFilter)
                mActivityTestRule
                        .getActivity()
                        .getTabModelSelector()
                        .getTabModelFilterProvider()
                        .getTabModelFilter(isIncognito);
    }

    private void verifyModalDialog(boolean shouldShow) {
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mModalDialogManager.isShowing(), Matchers.is(shouldShow));
                });
    }

    private void groupFirstTwoTabs(boolean isIncognito) {
        // Assert the correct strip is selected.
        Assert.assertEquals(
                "The wrong strip is selected",
                isIncognito,
                mActivityTestRule.getActivity().getTabModelSelector().isIncognitoSelected());

        // Group the first two tabs.
        List<Tab> tabGroup =
                new ArrayList<>(
                        Arrays.asList(
                                mActivityTestRule.getActivity().getCurrentTabModel().getTabAt(0),
                                mActivityTestRule.getActivity().getCurrentTabModel().getTabAt(1)));
        TabUiTestHelper.createTabGroup(mActivityTestRule.getActivity(), isIncognito, tabGroup);
        mStripLayoutHelper =
                TabStripUtils.getActiveStripLayoutHelper(mActivityTestRule.getActivity());
        StripLayoutView[] views = mStripLayoutHelper.getStripLayoutViewsForTesting();
        assertTrue(
                "First view should be a group title.", views[0] instanceof StripLayoutGroupTitle);
    }

    private void showMenu() {
        StripLayoutView[] views = mStripLayoutHelper.getStripLayoutViewsForTesting();
        assertTrue(
                "First view should be a group title.", views[0] instanceof StripLayoutGroupTitle);
        float x = ((StripLayoutGroupTitle) views[0]).getPaddedX();
        float y = ((StripLayoutGroupTitle) views[0]).getPaddedY();
        mRootId = ((StripLayoutGroupTitle) views[0]).getRootId();

        final StripLayoutHelperManager manager =
                mActivityTestRule.getActivity().getLayoutManager().getStripLayoutHelperManager();
        InstrumentationRegistry.getInstrumentation()
                .runOnMainSync(
                        new Runnable() {
                            @Override
                            public void run() {
                                manager.simulateLongPress(x, y);
                            }
                        });
        onViewWaiting(allOf(withId(R.id.tab_group_action_menu_list), isDisplayed()));
    }
}