chromium/chrome/android/javatests/src/org/chromium/chrome/browser/status_indicator/StatusIndicatorTest.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.status_indicator;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.Visibility.GONE;
import static androidx.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE;
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static androidx.test.espresso.matcher.ViewMatchers.withId;

import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.view.View;
import android.view.ViewGroup;

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

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.TypeSafeMatcher;
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.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.Restriction;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabbed_mode.TabbedRootUiCoordinator;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.NewTabPageTestUtils;
import org.chromium.chrome.test.util.RecentTabsPageTestUtils;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.ui.test.util.UiRestriction;

/**
 * Integration tests for status indicator covering related code in {@link
 * StatusIndicatorCoordinator} and {@link TabbedRootUiCoordinator}.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
// TODO(crbug.com/40112282): Enable for tablets once we support them.
@Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
public class StatusIndicatorTest {

    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    private StatusIndicatorCoordinator mStatusIndicatorCoordinator;
    private StatusIndicatorSceneLayer mStatusIndicatorSceneLayer;
    private View mControlContainer;
    private BrowserControlsStateProvider mBrowserControlsStateProvider;

    @Before
    public void setUp() throws InterruptedException {
        TabbedRootUiCoordinator.setDisableTopControlsAnimationsForTesting(true);
        mActivityTestRule.startMainActivityOnBlankPage();
        mStatusIndicatorCoordinator =
                ((TabbedRootUiCoordinator)
                                mActivityTestRule.getActivity().getRootUiCoordinatorForTesting())
                        .getStatusIndicatorCoordinatorForTesting();
        mStatusIndicatorSceneLayer = mStatusIndicatorCoordinator.getSceneLayer();
        mControlContainer = mActivityTestRule.getActivity().findViewById(R.id.control_container);
        mBrowserControlsStateProvider = mActivityTestRule.getActivity().getBrowserControlsManager();
    }

    @Test
    @MediumTest
    public void testInitialState() {
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        Assert.assertNull(
                "Status indicator shouldn't be in the hierarchy initially.", getStatusIndicator());
        Assert.assertNotNull(
                "Status indicator stub should be in the hierarchy initially.",
                mActivityTestRule.getActivity().findViewById(R.id.status_indicator_stub));
        Assert.assertFalse(
                "Wrong initial composited view visibility.",
                mStatusIndicatorSceneLayer.isSceneOverlayTreeShowing());
        Assert.assertEquals(
                "Wrong initial control container top margin.",
                0,
                getTopMarginOf(mControlContainer));
    }

    @Test
    @MediumTest
    public void testShowAndHide() {
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        mStatusIndicatorCoordinator.show(
                                "Status", null, Color.BLACK, Color.WHITE, Color.WHITE));

        // Wait until the status indicator finishes animating, or becomes fully visible.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mBrowserControlsStateProvider.getTopControlsMinHeightOffset(),
                            Matchers.is(getStatusIndicator().getHeight()));
                    Criteria.checkThat(
                            getStatusIndicator().getVisibility(), Matchers.is(View.VISIBLE));
                });

        Assert.assertEquals(
                "Wrong background color.",
                Color.BLACK,
                ((ColorDrawable) getStatusIndicator().getBackground()).getColor());

        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        mStatusIndicatorCoordinator.updateContent(
                                "Exit status",
                                null,
                                Color.WHITE,
                                Color.BLACK,
                                Color.BLACK,
                                () -> {}));
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        // The Android view should be visible.
        Assert.assertEquals(
                "Wrong Android view visibility.",
                View.VISIBLE,
                getStatusIndicator().getVisibility());
        Assert.assertEquals(
                "Wrong background color.",
                Color.WHITE,
                ((ColorDrawable) getStatusIndicator().getBackground()).getColor());

        ThreadUtils.runOnUiThreadBlocking(mStatusIndicatorCoordinator::hide);
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        // Wait until the status indicator finishes animating, or becomes fully hidden.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mBrowserControlsStateProvider.getTopControlsMinHeightOffset(),
                            Matchers.is(0));
                    Criteria.checkThat(
                            getStatusIndicator().getVisibility(), Matchers.is(View.GONE));
                });

        Assert.assertFalse(
                "Composited view shouldn't be visible.",
                mStatusIndicatorSceneLayer.isSceneOverlayTreeShowing());
    }

    @Test
    @MediumTest
    public void testShowAfterHide() {
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        mStatusIndicatorCoordinator.show(
                                "Status", null, Color.BLACK, Color.WHITE, Color.WHITE));

        // Wait until the status indicator finishes animating, or becomes fully visible.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mBrowserControlsStateProvider.getTopControlsMinHeightOffset(),
                            Matchers.is(getStatusIndicator().getHeight()));
                    Criteria.checkThat(
                            getStatusIndicator().getVisibility(), Matchers.is(View.VISIBLE));
                });

        ThreadUtils.runOnUiThreadBlocking(mStatusIndicatorCoordinator::hide);

        // Wait until the status indicator finishes animating, or becomes fully hidden.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mBrowserControlsStateProvider.getTopControlsMinHeightOffset(),
                            Matchers.is(0));
                    Criteria.checkThat(
                            getStatusIndicator().getVisibility(), Matchers.is(View.GONE));
                });

        Assert.assertFalse(
                "Composited view shouldn't be visible.",
                mStatusIndicatorSceneLayer.isSceneOverlayTreeShowing());

        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        mStatusIndicatorCoordinator.show(
                                "Status", null, Color.BLACK, Color.WHITE, Color.WHITE));

        // Wait until the status indicator finishes animating, or becomes fully visible.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mBrowserControlsStateProvider.getTopControlsMinHeightOffset(),
                            Matchers.is(getStatusIndicator().getHeight()));
                    Criteria.checkThat(
                            getStatusIndicator().getVisibility(), Matchers.is(View.VISIBLE));
                });
    }

    @Test
    @MediumTest
    public void testShowAndHideOnNtp() {
        mActivityTestRule.loadUrl(UrlConstants.NTP_URL);
        Tab tab = mActivityTestRule.getActivity().getActivityTab();
        NewTabPageTestUtils.waitForNtpLoaded(tab);
        final int viewId = View.generateViewId();
        final View view = tab.getNativePage().getView();
        view.setId(viewId);

        // R.id.status_indicator won't be in the View tree until the indicator is shown for the
        // first time and the corresponding ViewStub is inflated.
        onView(withId(R.id.status_indicator)).check(doesNotExist());
        onView(withId(R.id.control_container)).check(matches(withTopMargin(0)));
        onView(withId(viewId)).check(matches(withTopMargin(0)));
        Assert.assertFalse(
                "Wrong initial composited view visibility.",
                mStatusIndicatorSceneLayer.isSceneOverlayTreeShowing());

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mStatusIndicatorCoordinator.show(
                            "Status", null, Color.BLACK, Color.WHITE, Color.WHITE);
                    mStatusIndicatorCoordinator
                            .getMediatorForTesting()
                            .finishAnimationsForTesting();
                });

        // Wait until the status indicator finishes animating.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mBrowserControlsStateProvider.getTopControlsMinHeightOffset(),
                            Matchers.is(getStatusIndicator().getHeight()));
                });

        // The status indicator will be immediately visible.
        onView(withId(R.id.status_indicator)).check(matches(withEffectiveVisibility(VISIBLE)));
        onView(withId(R.id.control_container))
                .check(matches(withTopMargin(getStatusIndicator().getHeight())));
        onView(withId(viewId)).check(matches(withTopMargin(getStatusIndicator().getHeight())));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mStatusIndicatorCoordinator.updateContent(
                            "Exit status", null, Color.WHITE, Color.BLACK, Color.BLACK, () -> {});
                    mStatusIndicatorCoordinator
                            .getMediatorForTesting()
                            .finishAnimationsForTesting();
                });

        // #updateContent shouldn't change the layout.
        onView(withId(R.id.status_indicator)).check(matches(withEffectiveVisibility(VISIBLE)));
        onView(withId(R.id.control_container))
                .check(matches(withTopMargin(getStatusIndicator().getHeight())));
        onView(withId(viewId)).check(matches(withTopMargin(getStatusIndicator().getHeight())));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mStatusIndicatorCoordinator.hide();
                    mStatusIndicatorCoordinator
                            .getMediatorForTesting()
                            .finishAnimationsForTesting();
                });

        // Wait until the status indicator finishes animating, or becomes fully hidden.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mBrowserControlsStateProvider.getTopControlsMinHeightOffset(),
                            Matchers.is(0));
                });

        onView(withId(R.id.status_indicator)).check(matches(withEffectiveVisibility(GONE)));
        onView(withId(R.id.control_container)).check(matches(withTopMargin(0)));
        onView(withId(viewId)).check(matches(withTopMargin(0)));
    }

    @Test
    @MediumTest
    public void testShowAndHideOnRecentTabsPage() {
        mActivityTestRule.loadUrl(UrlConstants.RECENT_TABS_URL);
        final Tab tab = mActivityTestRule.getActivity().getActivityTab();
        RecentTabsPageTestUtils.waitForRecentTabsPageLoaded(tab);

        // R.id.status_indicator won't be in the View tree until the indicator is shown for the
        // first time and the corresponding ViewStub is inflated.
        onView(withId(R.id.status_indicator)).check(doesNotExist());
        onView(withId(R.id.control_container)).check(matches(withTopMargin(0)));
        onView(withId(R.id.recent_tabs_root))
                .check(
                        matches(
                                withTopMargin(
                                        mBrowserControlsStateProvider.getTopControlsHeight())));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mStatusIndicatorCoordinator.show(
                            "Status", null, Color.BLACK, Color.WHITE, Color.WHITE);
                    mStatusIndicatorCoordinator
                            .getMediatorForTesting()
                            .finishAnimationsForTesting();
                });

        // Wait until the status indicator finishes animating.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mBrowserControlsStateProvider.getTopControlsMinHeightOffset(),
                            Matchers.is(getStatusIndicator().getHeight()));
                });

        onView(withId(R.id.status_indicator)).check(matches(withEffectiveVisibility(VISIBLE)));
        onView(withId(R.id.control_container))
                .check(matches(withTopMargin(getStatusIndicator().getHeight())));
        onView(withId(R.id.recent_tabs_root))
                .check(
                        matches(
                                withTopMargin(
                                        mBrowserControlsStateProvider.getTopControlsHeight())));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mStatusIndicatorCoordinator.updateContent(
                            "Exit status", null, Color.WHITE, Color.BLACK, Color.BLACK, () -> {});
                    mStatusIndicatorCoordinator
                            .getMediatorForTesting()
                            .finishAnimationsForTesting();
                });

        // #updateContent shouldn't change the layout.
        onView(withId(R.id.status_indicator)).check(matches(withEffectiveVisibility(VISIBLE)));
        onView(withId(R.id.control_container))
                .check(matches(withTopMargin(getStatusIndicator().getHeight())));
        onView(withId(R.id.recent_tabs_root))
                .check(
                        matches(
                                withTopMargin(
                                        mBrowserControlsStateProvider.getTopControlsHeight())));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mStatusIndicatorCoordinator.hide();
                    mStatusIndicatorCoordinator
                            .getMediatorForTesting()
                            .finishAnimationsForTesting();
                });

        // Wait until the status indicator finishes animating, or becomes fully hidden.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mBrowserControlsStateProvider.getTopControlsMinHeightOffset(),
                            Matchers.is(0));
                });

        onView(withId(R.id.status_indicator)).check(matches(withEffectiveVisibility(GONE)));
        onView(withId(R.id.control_container)).check(matches(withTopMargin(0)));
        onView(withId(R.id.recent_tabs_root))
                .check(
                        matches(
                                withTopMargin(
                                        mBrowserControlsStateProvider.getTopControlsHeight())));
    }

    private View getStatusIndicator() {
        return mActivityTestRule.getActivity().findViewById(R.id.status_indicator);
    }

    private static Matcher<View> withTopMargin(final int expected) {
        return new TypeSafeMatcher<View>() {
            private int mActual;

            @Override
            public boolean matchesSafely(final View view) {
                mActual = getTopMarginOf(view);
                return mActual == expected;
            }

            @Override
            public void describeTo(final Description description) {
                // TODO(sinansahin): This is a work-around because the message from
                // #describeMismatchSafely is ignored. If it is fixed one day, we can implement
                // #describeMismatchSafely.
                description
                        .appendText("View should have a topMargin of " + expected)
                        .appendText(System.lineSeparator())
                        .appendText("but actually has " + mActual);
            }
        };
    }

    private static int getTopMarginOf(View view) {
        final ViewGroup.MarginLayoutParams layoutParams =
                (ViewGroup.MarginLayoutParams) view.getLayoutParams();
        return layoutParams.topMargin;
    }
}