chromium/chrome/test/android/javatests/src/org/chromium/chrome/test/transit/hub/HubBaseStation.java

// Copyright 2023 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.test.transit.hub;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.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.withId;

import static org.hamcrest.CoreMatchers.allOf;

import static org.chromium.base.test.transit.Condition.whether;
import static org.chromium.base.test.transit.LogicalElement.uiThreadLogicalElement;
import static org.chromium.base.test.transit.ViewSpec.viewSpec;

import androidx.annotation.StringRes;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.NoMatchingViewException;

import org.chromium.base.supplier.Supplier;
import org.chromium.base.test.transit.Condition;
import org.chromium.base.test.transit.ConditionStatus;
import org.chromium.base.test.transit.Elements;
import org.chromium.base.test.transit.Station;
import org.chromium.base.test.transit.Transition;
import org.chromium.base.test.transit.TravelException;
import org.chromium.base.test.transit.UiThreadCondition;
import org.chromium.base.test.transit.ViewSpec;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.hub.PaneId;
import org.chromium.chrome.browser.hub.R;
import org.chromium.chrome.browser.layouts.LayoutManager;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.test.transit.page.PageStation;
import org.chromium.chrome.test.transit.tabmodel.TabModelSelectorCondition;

/** The base station for Hub, with several panes and a toolbar. */
public abstract class HubBaseStation extends Station {
    public static final ViewSpec HUB_TOOLBAR = viewSpec(withId(R.id.hub_toolbar));
    public static final ViewSpec HUB_PANE_HOST = viewSpec(withId(R.id.hub_pane_host));
    public static final ViewSpec HUB_MENU_BUTTON =
            viewSpec(
                    isDescendantOfA(withId(R.id.hub_toolbar)),
                    withId(org.chromium.chrome.R.id.menu_button));
    public static final ViewSpec HUB_PANE_SWITCHER =
            viewSpec(allOf(isDescendantOfA(withId(R.id.hub_toolbar)), withId(R.id.pane_switcher)));

    public static final ViewSpec REGULAR_TOGGLE_TAB_BUTTON =
            viewSpec(withContentDescription(R.string.accessibility_tab_switcher_standard_stack));

    public static final ViewSpec INCOGNITO_TOGGLE_TAB_BUTTON =
            viewSpec(withContentDescription(R.string.accessibility_tab_switcher_incognito_stack));

    protected Supplier<ChromeTabbedActivity> mActivitySupplier;
    protected Supplier<TabModelSelector> mTabModelSelectorSupplier;
    protected final boolean mIncognitoTabsExist;
    protected final boolean mRegularTabsExist;

    public HubBaseStation(boolean regularTabsExist, boolean incognitoTabsExist) {
        super();
        mRegularTabsExist = regularTabsExist;
        mIncognitoTabsExist = incognitoTabsExist;
    }

    /** Returns the station's {@link PaneId}. */
    public abstract @PaneId int getPaneId();

    @Override
    public void declareElements(Elements.Builder elements) {
        mActivitySupplier = elements.declareActivity(ChromeTabbedActivity.class);
        mTabModelSelectorSupplier =
                elements.declareEnterCondition(new TabModelSelectorCondition(mActivitySupplier));

        elements.declareView(HUB_TOOLBAR);
        elements.declareView(HUB_PANE_HOST);
        elements.declareView(HUB_MENU_BUTTON);

        if (mIncognitoTabsExist) {
            elements.declareView(REGULAR_TOGGLE_TAB_BUTTON);
            elements.declareView(INCOGNITO_TOGGLE_TAB_BUTTON);
        }

        elements.declareLogicalElement(
                uiThreadLogicalElement(
                        "LayoutManager is showing TAB_SWITCHER (Hub)",
                        this::isHubLayoutShowing,
                        mActivitySupplier));
        elements.declareEnterCondition(new HubLayoutNotInTransition());
    }

    /** Returns the {@link Condition} that acts as {@link Supplier<TabModelSelector>}. */
    public Supplier<TabModelSelector> getTabModelSelectorSupplier() {
        return mTabModelSelectorSupplier;
    }

    /** Returns the {@link ChromeTabbedActivity} supplier for this station. */
    public Supplier<ChromeTabbedActivity> getActivitySupplier() {
        return mActivitySupplier;
    }

    /** Returns the {@link ChromeTabbedActivity} for this station. */
    public ChromeTabbedActivity getActivity() {
        assertSuppliersCanBeUsed();
        return mActivitySupplier.get();
    }

    /**
     * Returns to the previous tab via the back button.
     *
     * @return the {@link PageStation} that Hub returned to.
     */
    public <T extends PageStation> T leaveHubToPreviousTabViaBack(T destination) {
        return travelToSync(destination, Transition.retryOption(), () -> Espresso.pressBack());
    }

    /**
     * Selects the tab switcher pane on the Hub.
     *
     * @return the corresponding subclass of {@link HubBaseStation}.
     */
    public <T extends HubBaseStation> T selectPane(
            @PaneId int paneId, Class<T> expectedDestination) {
        recheckActiveConditions();

        if (getPaneId() == paneId) {
            return expectedDestination.cast(this);
        }

        T destinationStation =
                expectedDestination.cast(
                        HubStationUtils.createHubStation(
                                paneId, mRegularTabsExist, mIncognitoTabsExist));

        try {
            HUB_PANE_SWITCHER.onView().check(matches(isDisplayed()));
        } catch (NoMatchingViewException e) {
            throw TravelException.newTravelException(
                    "Hub pane switcher is not visible to switch to " + paneId);
        }

        @StringRes
        int contentDescriptionId = HubStationUtils.getContentDescriptionForIdPaneSelection(paneId);
        return travelToSync(
                destinationStation,
                () -> {
                    clickPaneSwitcherForPaneWithContentDescription(contentDescriptionId);
                });
    }

    /** Convenience method to select the Regular Tab Switcher pane. */
    public RegularTabSwitcherStation selectRegularTabList() {
        return selectPane(PaneId.TAB_SWITCHER, RegularTabSwitcherStation.class);
    }

    /** Convenience method to select the Incognito Tab Switcher pane. */
    public IncognitoTabSwitcherStation selectIncognitoTabList() {
        return selectPane(PaneId.INCOGNITO_TAB_SWITCHER, IncognitoTabSwitcherStation.class);
    }

    private ConditionStatus isHubLayoutShowing(ChromeTabbedActivity activity) {
        return whether(activity.getLayoutManager().isLayoutVisible(LayoutType.TAB_SWITCHER));
    }

    private void clickPaneSwitcherForPaneWithContentDescription(
            @StringRes int contentDescriptionRes) {
        // TODO(crbug.com/40287437): Content description seems reasonable for now, this might get
        // harder
        // once we use a recycler view with text based buttons.
        String contentDescription = getActivity().getString(contentDescriptionRes);
        onView(
                        allOf(
                                isDescendantOfA(HUB_PANE_SWITCHER.getViewMatcher()),
                                withContentDescription(contentDescription)))
                .perform(click());
    }

    private class HubLayoutNotInTransition extends UiThreadCondition {
        private HubLayoutNotInTransition() {
            dependOnSupplier(mActivitySupplier, "ChromeTabbedActivity");
        }

        @Override
        protected ConditionStatus checkWithSuppliers() {
            LayoutManager layoutManager = mActivitySupplier.get().getLayoutManager();
            boolean startingToShow = layoutManager.isLayoutStartingToShow(LayoutType.TAB_SWITCHER);
            boolean startingToHide = layoutManager.isLayoutStartingToHide(LayoutType.TAB_SWITCHER);
            return whether(
                    !startingToShow && !startingToHide,
                    "startingToShow=%b, startingToHide=%b",
                    startingToShow,
                    startingToHide);
        }

        @Override
        public String buildDescription() {
            return "LayoutManager is not in transition to or from TAB_SWITCHER (Hub)";
        }
    }
}