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

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.tasks.tab_management;

import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
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.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

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

import android.content.Context;
import android.view.LayoutInflater;
import android.widget.LinearLayout;

import androidx.appcompat.content.res.AppCompatResources;
import androidx.test.filters.MediumTest;

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

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.params.ParameterAnnotations;
import org.chromium.base.test.params.ParameterSet;
import org.chromium.base.test.params.ParameterizedRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Feature;
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.TabListEditorAction.ActionDelegate;
import org.chromium.chrome.browser.tasks.tab_management.TabListEditorAction.ActionObserver;
import org.chromium.chrome.browser.tasks.tab_management.TabListEditorAction.ButtonType;
import org.chromium.chrome.browser.tasks.tab_management.TabListEditorAction.IconPosition;
import org.chromium.chrome.browser.tasks.tab_management.TabListEditorAction.ShowMode;
import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
import org.chromium.chrome.test.R;
import org.chromium.components.browser_ui.widget.NumberRollView;
import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate;
import org.chromium.ui.UiUtils;
import org.chromium.ui.listmenu.ListMenuButton;
import org.chromium.ui.modelutil.ListModelChangeProcessor;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyListModel;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.BlankUiTestActivityTestCase;
import org.chromium.ui.test.util.NightModeTestUtils;
import org.chromium.ui.test.util.RenderTestRule;
import org.chromium.ui.test.util.RenderTestRule.Component;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeoutException;

/** On-device Unit tests for the {@link TabListEditorMenu} and its related classes. */
@RunWith(ParameterizedRunner.class)
@ParameterAnnotations.UseRunnerDelegate(ChromeJUnit4RunnerDelegate.class)
@Batch(Batch.PER_CLASS)
public class TabListEditorMenuTest extends BlankUiTestActivityTestCase {
    private static final int TAB_COUNT = 3;
    private static final Integer TAB_ID_0 = 0;
    private static final Integer TAB_ID_1 = 1;
    private static final Integer TAB_ID_2 = 2;
    private static final Integer[] TAB_IDS = new Integer[] {TAB_ID_0, TAB_ID_1, TAB_ID_2};

    @ParameterAnnotations.ClassParameter
    private static List<ParameterSet> sClassParams =
            new NightModeTestUtils.NightModeParams().getParameters();

    @Rule
    public RenderTestRule mRenderTestRule =
            RenderTestRule.Builder.withPublicCorpus()
                    .setBugComponent(Component.UI_BROWSER_MOBILE_TAB_SWITCHER_GRID)
                    .setRevision(5)
                    .setDescription("New selection icons")
                    .build();

    static class FakeTabListEditorAction extends TabListEditorAction {
        private boolean mShouldEnableAction = true;

        FakeTabListEditorAction(
                Context context,
                int menuId,
                @ShowMode int showMode,
                @ButtonType int buttonType,
                @IconPosition int iconPosition,
                int title,
                Integer iconResourceId) {
            super(
                    menuId,
                    showMode,
                    buttonType,
                    iconPosition,
                    title,
                    R.plurals.accessibility_tab_selection_editor_close_tabs,
                    (iconResourceId != null)
                            ? UiUtils.getTintedDrawable(
                                    context, iconResourceId, R.color.default_icon_color_tint_list)
                            : null);
        }

        public void setShouldEnableAction(boolean shouldEnableAction) {
            mShouldEnableAction = shouldEnableAction;
        }

        @Override
        public void onSelectionStateChange(List<Integer> tabs) {
            setEnabledAndItemCount(mShouldEnableAction, tabs.size());
        }

        @Override
        public boolean performAction(List<Tab> tabs) {
            return true;
        }

        @Override
        public boolean shouldHideEditorAfterAction() {
            return false;
        }
    }

    // For R8 optimizer message
    @Mock private Tab mTabDoNotUse;

    // Real mocks.
    @Mock private TabModel mTabModel;
    @Mock private TabGroupModelFilter mTabGroupModelFilter;
    private SelectionDelegate<Integer> mSelectionDelegate;
    @Mock private ActionDelegate mDelegate;

    private List<Tab> mTabs = new ArrayList<>();

    private TabListEditorToolbar mToolbar;
    private TabListEditorMenu mTabListEditorMenu;
    private ListMenuButton mMenuButton;
    private PropertyListModel<PropertyModel, PropertyKey> mPropertyListModel;
    private ListModelChangeProcessor mChangeProcessor;

    public TabListEditorMenuTest(boolean nightModeEnabled) {
        NightModeTestUtils.setUpNightModeForBlankUiTestActivity(nightModeEnabled);
        mRenderTestRule.setNightModeEnabled(nightModeEnabled);
    }

    @Override
    public void setUpTest() throws Exception {
        super.setUpTest();
        MockitoAnnotations.initMocks(this);

        when(mTabGroupModelFilter.getTabModel()).thenReturn(mTabModel);
        when(mTabModel.getCount()).thenReturn(TAB_COUNT);

        for (int id = 0; id < TAB_COUNT; id++) {
            Tab tab = mock(Tab.class);
            mTabs.add(tab);
            when(tab.getId()).thenReturn(TAB_IDS[id]);
            when(mTabModel.getTabAt(id)).thenReturn(tab);
            when(mTabModel.getTabById(id)).thenReturn(tab);
        }

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mSelectionDelegate = new SelectionDelegate<>();
                    mSelectionDelegate.setSelectionModeEnabledForZeroItems(true);
                    LinearLayout layout = new LinearLayout(getActivity());
                    LinearLayout.LayoutParams layoutParams =
                            new LinearLayout.LayoutParams(
                                    LinearLayout.LayoutParams.MATCH_PARENT,
                                    LinearLayout.LayoutParams.MATCH_PARENT);
                    layout.setLayoutParams(layoutParams);

                    LayoutInflater inflater = LayoutInflater.from(getActivity());
                    mToolbar =
                            (TabListEditorToolbar)
                                    inflater.inflate(R.layout.tab_list_editor_toolbar, null);
                    layoutParams =
                            new LinearLayout.LayoutParams(
                                    LinearLayout.LayoutParams.MATCH_PARENT,
                                    getActivity()
                                            .getResources()
                                            .getDimensionPixelSize(
                                                    R.dimen.toolbar_height_no_shadow));
                    layout.addView(mToolbar, layoutParams);
                    getActivity().setContentView(layout);
                    mToolbar.initialize(mSelectionDelegate, 0, 0, 0, true);

                    mPropertyListModel = new PropertyListModel<>();
                    mTabListEditorMenu =
                            new TabListEditorMenu(getActivity(), mToolbar.getActionViewLayout());
                    mMenuButton = mToolbar.getActionViewLayout().getListMenuButtonForTesting();
                    mSelectionDelegate.addObserver(mTabListEditorMenu);
                    mChangeProcessor =
                            new ListModelChangeProcessor(
                                    mPropertyListModel,
                                    mTabListEditorMenu,
                                    new TabListEditorMenuAdapter());
                    mPropertyListModel.addObserver(mChangeProcessor);
                });
    }

    @Override
    public void tearDownTest() throws Exception {
        NightModeTestUtils.tearDownNightModeForBlankUiTestActivity();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mPropertyListModel.clear();
                });
        super.tearDownTest();
    }

    private void configureMenuWithActions(List<FakeTabListEditorAction> actions) {
        mPropertyListModel.clear();
        List<PropertyModel> models = new ArrayList<>();
        for (FakeTabListEditorAction action : actions) {
            action.getPropertyModel()
                    .set(
                            TabListEditorActionProperties.TEXT_TINT,
                            AppCompatResources.getColorStateList(
                                    getActivity(), R.color.default_text_color_list));
            action.getPropertyModel()
                    .set(
                            TabListEditorActionProperties.ICON_TINT,
                            AppCompatResources.getColorStateList(
                                    getActivity(), R.color.default_icon_color_tint_list));
            action.configure(
                    () -> mTabGroupModelFilter,
                    mSelectionDelegate,
                    mDelegate,
                    /* editorSupportsActionOnRelatedTabs= */ false);
            models.add(action.getPropertyModel());
        }
        mPropertyListModel.addAll(models, 0);
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testSingleActionView_TextAndIcon_Enabled() throws Exception {
        List<FakeTabListEditorAction> actions = new ArrayList<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_close_menu_item,
                                    ShowMode.IF_ROOM,
                                    ButtonType.ICON_AND_TEXT,
                                    IconPosition.END,
                                    R.plurals.tab_selection_editor_close_tabs,
                                    R.drawable.ic_close_tabs_24dp));
                    configureMenuWithActions(actions);
                });

        setSelectedItems(new HashSet<>(Arrays.asList(new Integer[] {TAB_ID_0, TAB_ID_2})));
        assertActionView(R.id.tab_list_editor_close_menu_item, true);

        forceFinishRollAnimation();
        mRenderTestRule.render(mToolbar, "singleActionToolbarEnabled");
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testSingleActionView_TextAndIcon_Disabled() throws Exception {
        List<FakeTabListEditorAction> actions = new ArrayList<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_close_menu_item,
                                    ShowMode.IF_ROOM,
                                    ButtonType.ICON_AND_TEXT,
                                    IconPosition.END,
                                    R.plurals.tab_selection_editor_close_tabs,
                                    R.drawable.ic_close_tabs_24dp));
                    configureMenuWithActions(actions);
                });

        ThreadUtils.runOnUiThreadBlocking(() -> actions.get(0).setShouldEnableAction(false));
        setSelectedItems(new HashSet<>(Arrays.asList(new Integer[] {TAB_ID_1})));
        assertActionView(R.id.tab_list_editor_close_menu_item, false);

        forceFinishRollAnimation();
        mRenderTestRule.render(mToolbar, "singleActionToolbarDisabled");
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testSingleActionView_IconOnly_Enabled() throws Exception {
        List<FakeTabListEditorAction> actions = new ArrayList<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_close_menu_item,
                                    ShowMode.IF_ROOM,
                                    ButtonType.ICON,
                                    IconPosition.END,
                                    R.plurals.tab_selection_editor_close_tabs,
                                    R.drawable.ic_close_tabs_24dp));
                    configureMenuWithActions(actions);
                });

        setSelectedItems(
                new HashSet<>(Arrays.asList(new Integer[] {TAB_ID_0, TAB_ID_1, TAB_ID_2})));
        assertActionView(R.id.tab_list_editor_close_menu_item, true);

        forceFinishRollAnimation();
        mRenderTestRule.render(mToolbar, "singleActionToolbarIconOnlyEnabled");
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testSingleActionView_Click() throws Exception {
        List<FakeTabListEditorAction> actions = new ArrayList<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_close_menu_item,
                                    ShowMode.IF_ROOM,
                                    ButtonType.TEXT,
                                    IconPosition.END,
                                    R.string.tab_selection_editor_select_all,
                                    R.drawable.ic_select_all_24dp));
                    configureMenuWithActions(actions);
                });

        final List<Tab> processedTabs = new ArrayList<>();
        final CallbackHelper helper = new CallbackHelper();
        final ActionObserver observer =
                new ActionObserver() {
                    @Override
                    public void preProcessSelectedTabs(List<Tab> tabs) {
                        processedTabs.clear();
                        processedTabs.addAll(tabs);
                        helper.notifyCalled();
                    }
                };
        setSelectedItems(new HashSet<>(Arrays.asList(new Integer[] {TAB_ID_0, TAB_ID_2})));
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    actions.get(0).addActionObserver(observer);
                });

        forceFinishRollAnimation();
        mRenderTestRule.render(mToolbar, "singleActionToolbarTextOnlyEnabled");
        assertActionView(R.id.tab_list_editor_close_menu_item, true);
        clickActionView(R.id.tab_list_editor_close_menu_item);

        helper.waitForCallback(0);
        assertEquals(2, processedTabs.size());
        assertEquals(mTabs.get(0), processedTabs.get(0));
        assertEquals(mTabs.get(2), processedTabs.get(1));
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testSingleMenuItem_Disabled() throws Exception {
        List<FakeTabListEditorAction> actions = new ArrayList<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_close_menu_item,
                                    ShowMode.MENU_ONLY,
                                    ButtonType.TEXT,
                                    IconPosition.START,
                                    R.string.tab_selection_editor_deselect_all,
                                    R.drawable.ic_deselect_all_24dp));
                    configureMenuWithActions(actions);
                });

        ThreadUtils.runOnUiThreadBlocking(() -> actions.get(0).setShouldEnableAction(false));
        setSelectedItems(new HashSet<>(Arrays.asList(new Integer[] {TAB_ID_0, TAB_ID_1})));

        PopupListener listener = new PopupListener();
        openMenu(listener);
        assertMenuItem("Deselect all", false);

        forceFinishRollAnimation();
        mRenderTestRule.render(
                mTabListEditorMenu.getContentView(), "singleMenuItemDisabled_Menu");
        closeMenu(listener);
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testSingleMenuItem_Click() throws Exception {
        List<FakeTabListEditorAction> actions = new ArrayList<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_close_menu_item,
                                    ShowMode.MENU_ONLY,
                                    ButtonType.TEXT,
                                    IconPosition.START,
                                    R.plurals.tab_selection_editor_close_tabs,
                                    R.drawable.ic_close_tabs_24dp));
                    configureMenuWithActions(actions);
                });

        final List<Tab> processedTabs = new ArrayList<>();
        final CallbackHelper helper = new CallbackHelper();
        final ActionObserver observer =
                new ActionObserver() {
                    @Override
                    public void preProcessSelectedTabs(List<Tab> tabs) {
                        processedTabs.clear();
                        processedTabs.addAll(tabs);
                        helper.notifyCalled();
                    }
                };

        setSelectedItems(new HashSet<>(Arrays.asList(new Integer[] {TAB_ID_2})));
        ThreadUtils.runOnUiThreadBlocking(() -> actions.get(0).addActionObserver(observer));

        PopupListener listener = new PopupListener();
        openMenu(listener);
        assertMenuItem("Close tab", true);
        clickMenuItem("Close tab");
        helper.waitForCallback(0);

        forceFinishRollAnimation();
        mRenderTestRule.render(mToolbar, "singleMenuItemEnabled_Toolbar");
        mRenderTestRule.render(
                mTabListEditorMenu.getContentView(), "singleMenuItemEnabled_Menu");
        closeMenu(listener);

        assertEquals(1, processedTabs.size());
        assertEquals(mTabs.get(2), processedTabs.get(0));
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testTwoActionView_OneActionDisabled() throws Exception {
        List<FakeTabListEditorAction> actions = new ArrayList<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_close_menu_item,
                                    ShowMode.IF_ROOM,
                                    ButtonType.ICON,
                                    IconPosition.START,
                                    R.plurals.tab_selection_editor_close_tabs,
                                    R.drawable.ic_close_tabs_24dp));
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_group_menu_item,
                                    ShowMode.IF_ROOM,
                                    ButtonType.ICON,
                                    IconPosition.END,
                                    R.plurals.tab_selection_editor_group_tabs,
                                    R.drawable.ic_widgets));
                    configureMenuWithActions(actions);
                });

        ThreadUtils.runOnUiThreadBlocking(() -> actions.get(0).setShouldEnableAction(false));
        setSelectedItems(new HashSet<>());
        assertActionView(R.id.tab_list_editor_close_menu_item, false);
        assertActionView(R.id.tab_list_editor_group_menu_item, true);

        forceFinishRollAnimation();
        mRenderTestRule.render(mToolbar, "twoActionToolbarPartlyDisabled");
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testActionViewAndMenuItem_Enabled() throws Exception {
        List<FakeTabListEditorAction> actions = new ArrayList<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_close_menu_item,
                                    ShowMode.MENU_ONLY,
                                    ButtonType.TEXT,
                                    IconPosition.START,
                                    R.plurals.tab_selection_editor_close_tabs,
                                    R.drawable.ic_close_tabs_24dp));
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_group_menu_item,
                                    ShowMode.IF_ROOM,
                                    ButtonType.ICON,
                                    IconPosition.START,
                                    R.plurals.tab_selection_editor_group_tabs,
                                    R.drawable.ic_widgets));
                    configureMenuWithActions(actions);
                });

        setSelectedItems(new HashSet<>(Arrays.asList(new Integer[] {TAB_ID_2})));

        assertActionView(R.id.tab_list_editor_group_menu_item, true);

        PopupListener listener = new PopupListener();
        openMenu(listener);
        assertMenuItem("Close tab", true);
        forceFinishRollAnimation();
        mRenderTestRule.render(mToolbar, "oneActionToolbarOneMenuItemEnabled_Toobar");
        mRenderTestRule.render(
                mTabListEditorMenu.getContentView(),
                "oneActionToolbarOneMenuItemEnabled_Menu");
        closeMenu(listener);
    }

    // Regression test for https://crbug.com/1377205.
    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testLongTextActionViewAndMenuItem() throws Exception {
        List<FakeTabListEditorAction> actions = new ArrayList<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    NumberRollView numberRoll =
                            (NumberRollView) mToolbar.getActionViewLayout().getChildAt(0);
                    numberRoll.setStringForZero(R.string.close_all_tabs_dialog_message_incognito);
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_close_menu_item,
                                    ShowMode.MENU_ONLY,
                                    ButtonType.TEXT,
                                    IconPosition.START,
                                    R.plurals.tab_selection_editor_close_tabs,
                                    R.drawable.ic_close_tabs_24dp));
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_group_menu_item,
                                    ShowMode.IF_ROOM,
                                    ButtonType.ICON,
                                    IconPosition.START,
                                    R.plurals.tab_selection_editor_group_tabs,
                                    R.drawable.ic_widgets));
                    configureMenuWithActions(actions);
                    actions.get(0).setShouldEnableAction(false);
                    actions.get(1).setShouldEnableAction(false);
                });

        setSelectedItems(new HashSet<>(Arrays.asList(new Integer[] {})));

        assertActionView(R.id.tab_list_editor_group_menu_item, false);

        forceFinishRollAnimation();
        mRenderTestRule.render(mToolbar, "longTextV2ActionAndMenu");
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testTwoMenuItems_OneMenuItemDisabled() throws Exception {
        List<FakeTabListEditorAction> actions = new ArrayList<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_close_menu_item,
                                    ShowMode.MENU_ONLY,
                                    ButtonType.TEXT,
                                    IconPosition.END,
                                    R.plurals.tab_selection_editor_close_tabs,
                                    R.drawable.ic_close_tabs_24dp));
                    actions.add(
                            new FakeTabListEditorAction(
                                    getActivity(),
                                    R.id.tab_list_editor_group_menu_item,
                                    ShowMode.MENU_ONLY,
                                    ButtonType.ICON,
                                    IconPosition.START,
                                    R.plurals.tab_selection_editor_group_tabs,
                                    R.drawable.ic_widgets));
                    configureMenuWithActions(actions);
                });

        ThreadUtils.runOnUiThreadBlocking(() -> actions.get(1).setShouldEnableAction(false));
        setSelectedItems(new HashSet<>(Arrays.asList(new Integer[] {TAB_ID_1})));

        PopupListener listener = new PopupListener();
        openMenu(listener);
        assertMenuItem("Close tab", true);
        assertMenuItem("Group tab", false);
        forceFinishRollAnimation();
        mRenderTestRule.render(
                mTabListEditorMenu.getContentView(), "twoMenuItemsPartlyDisabled_Menu");
        closeMenu(listener);
    }

    /** Helper for detecting menu shown popup events. */
    static class PopupListener implements ListMenuButton.PopupMenuShownListener {
        private CallbackHelper mShown = new CallbackHelper();
        private CallbackHelper mHidden = new CallbackHelper();

        @Override
        public void onPopupMenuShown() {
            mShown.notifyCalled();
        }

        @Override
        public void onPopupMenuDismissed() {
            mHidden.notifyCalled();
        }

        public void waitForShown() throws TimeoutException {
            mShown.waitForOnly();
        }

        public void waitForHidden() {
            mHidden.notifyCalled();
        }
    }

    private void assertActionView(int id, boolean enabled) {
        onViewWaiting(
                allOf(
                        withId(id),
                        isDescendantOfA(withId(R.id.action_view_layout)),
                        isDisplayed(),
                        enabled ? isEnabled() : not(isEnabled())));
    }

    private void assertMenuItem(String text, boolean enabled) {
        onViewWaiting(
                allOf(
                        withText(text),
                        isDescendantOfA(withId(R.id.app_menu_list)),
                        isDisplayed(),
                        enabled ? isEnabled() : not(isEnabled())));
    }

    private void openMenu(PopupListener listener) throws TimeoutException {
        ThreadUtils.runOnUiThreadBlocking(() -> mMenuButton.addPopupListener(listener));
        onViewWaiting(allOf(withId(R.id.list_menu_button), isDisplayed(), isEnabled()))
                .perform(click());
        listener.waitForShown();
    }

    private void closeMenu(PopupListener listener) {
        ThreadUtils.runOnUiThreadBlocking(() -> mMenuButton.dismiss());
        listener.waitForHidden();
        ThreadUtils.runOnUiThreadBlocking(() -> mMenuButton.removePopupListener(listener));
    }

    private void clickActionView(int id) {
        onViewWaiting(
                allOf(withId(id), isDescendantOfA(withId(R.id.action_view_layout)), isDisplayed()));
        // On Android 12 perform(click()) sometimes fails to trigger the click so force the click on
        // the view object instead.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mToolbar.findViewById(id).performClick();
                });
    }

    private void clickMenuItem(String text) {
        onViewWaiting(
                        allOf(
                                withText(text),
                                isDescendantOfA(withId(R.id.app_menu_list)),
                                isDisplayed()))
                .perform(click());
    }

    private void setSelectedItems(Set<Integer> tabIds) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mSelectionDelegate.setSelectedItems(tabIds);
                    mToolbar.invalidate();
                });
    }

    private void forceFinishRollAnimation() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    NumberRollView numberRoll =
                            (NumberRollView) mToolbar.getActionViewLayout().getChildAt(0);
                    numberRoll.endAnimationsForTesting();
                });
    }
}