chromium/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/scrim/ScrimTest.java

// Copyright 2020 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.components.browser_ui.widget.scrim;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import android.app.Activity;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import androidx.annotation.ColorInt;
import androidx.test.filters.SmallTest;

import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.Callback;
import org.chromium.base.MathUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseActivityTestRule;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.PayloadCallbackHelper;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.BlankUiTestActivity;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

/** This class tests the behavior of the scrim component. */
@RunWith(BaseJUnit4ClassRunner.class)
@Batch(Batch.UNIT_TESTS)
public class ScrimTest {
    @ClassRule
    public static BaseActivityTestRule<BlankUiTestActivity> activityTestRule =
            new BaseActivityTestRule<>(BlankUiTestActivity.class);

    private static Activity sActivity;
    private static FrameLayout sParent;

    private ScrimCoordinator mScrimCoordinator;
    private View mAnchorView;

    private final PayloadCallbackHelper<Integer> mScrimColorCallbackHelper =
            new PayloadCallbackHelper<>();
    private final CallbackHelper mStatusBarCallbackHelper = new CallbackHelper();
    private final CallbackHelper mNavigationBarCallbackHelper = new CallbackHelper();
    private final ScrimCoordinator.SystemUiScrimDelegate mScrimDelegate =
            new ScrimCoordinator.SystemUiScrimDelegate() {
                @Override
                public void setScrimColor(int scrimColor) {
                    mScrimColorCallbackHelper.notifyCalled(scrimColor);
                }

                @Override
                public void setStatusBarScrimFraction(float scrimFraction) {
                    mStatusBarCallbackHelper.notifyCalled();
                }

                @Override
                public void setNavigationBarScrimFraction(float scrimFraction) {
                    mNavigationBarCallbackHelper.notifyCalled();
                }
            };

    private final CallbackHelper mScrimClickCallbackHelper = new CallbackHelper();
    private final CallbackHelper mVisibilityChangeCallbackHelper = new CallbackHelper();
    private final Runnable mClickDelegate = () -> mScrimClickCallbackHelper.notifyCalled();
    private final Callback<Boolean> mVisibilityChangeCallback =
            (v) -> mVisibilityChangeCallbackHelper.notifyCalled();

    private GestureDetector mCustomGestureDetector;
    private CallbackHelper mDelegatedEventHelper;

    @BeforeClass
    public static void setupSuite() {
        activityTestRule.launchActivity(null);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sActivity = activityTestRule.getActivity();
                    sParent = new FrameLayout(sActivity);
                    sActivity.setContentView(sParent);
                });
    }

    @Before
    public void setupTest() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sParent.removeAllViews();

                    mAnchorView = new View(sActivity);
                    sParent.addView(mAnchorView);

                    mScrimCoordinator =
                            new ScrimCoordinator(sActivity, mScrimDelegate, sParent, Color.RED);

                    mDelegatedEventHelper = new CallbackHelper();
                    mCustomGestureDetector =
                            new GestureDetector(
                                    new GestureDetector.SimpleOnGestureListener() {
                                        @Override
                                        public boolean onDown(MotionEvent e) {
                                            mDelegatedEventHelper.notifyCalled();
                                            return true;
                                        }
                                    });
                });
    }

    @After
    public void tearDownTest() {
        ThreadUtils.runOnUiThreadBlocking(() -> mScrimCoordinator.destroy());
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testVisibility() throws TimeoutException {
        showScrim(buildModel(true, false, true, Color.RED), false);

        assertEquals(
                "Scrim should be completely visible.",
                1.0f,
                mScrimCoordinator.getViewForTesting().getAlpha(),
                MathUtils.EPSILON);

        int callCount = mVisibilityChangeCallbackHelper.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(() -> mScrimCoordinator.hideScrim(false));
        mVisibilityChangeCallbackHelper.waitForCallback(callCount, 1);
        assertScrimVisibility(false);
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testVisibilityWithForceToFinish() throws TimeoutException {
        showScrim(buildModel(true, false, true, Color.RED), true);

        ScrimView scrimView = mScrimCoordinator.getViewForTesting();
        assertEquals(
                "Scrim should be completely visible.",
                1.0f,
                scrimView.getAlpha(),
                MathUtils.EPSILON);

        int callCount = mVisibilityChangeCallbackHelper.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mScrimCoordinator.hideScrim(true);
                    mScrimCoordinator.forceAnimationToFinish();
                    assertEquals(
                            "Scrim should be completely invisible.",
                            0.0f,
                            scrimView.getAlpha(),
                            MathUtils.EPSILON);
                });
        mVisibilityChangeCallbackHelper.waitForCallback(callCount, 1);
        assertScrimVisibility(false);
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testColor_default() throws TimeoutException {
        showScrim(buildModel(true, false, true, Color.RED), false);

        assertScrimColor(Color.RED);
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testColor_custom() throws TimeoutException {
        showScrim(buildModel(false, false, true, Color.GREEN), false);

        assertScrimColor(Color.GREEN);

        ThreadUtils.runOnUiThreadBlocking(() -> mScrimCoordinator.hideScrim(false));

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "Scrim should be null after being hidden.",
                            mScrimCoordinator.getViewForTesting(),
                            Matchers.nullValue());
                });
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testColor_mutated() throws TimeoutException {
        PropertyModel model = buildModel(false, false, true, Color.GREEN);

        showScrim(model, false);
        assertScrimColor(Color.GREEN);
        assertEquals(Color.GREEN, mScrimColorCallbackHelper.getOnlyPayloadBlocking().intValue());

        ThreadUtils.runOnUiThreadBlocking(
                () -> model.set(ScrimProperties.BACKGROUND_COLOR, Color.RED));
        assertScrimColor(Color.RED);
        assertEquals(Color.RED, mScrimColorCallbackHelper.getPayloadByIndexBlocking(1).intValue());
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testHierarchy_behindAnchor() throws TimeoutException {
        showScrim(buildModel(true, false, false, Color.RED), false);

        View scrimView = mScrimCoordinator.getViewForTesting();
        assertEquals("The parent view of the scrim is incorrect.", sParent, scrimView.getParent());
        assertTrue(
                "The scrim should be positioned behind the anchor.",
                sParent.indexOfChild(scrimView) < sParent.indexOfChild(mAnchorView));
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testHierarchy_inFrontOfAnchor() throws TimeoutException {
        showScrim(buildModel(true, false, true, Color.RED), false);

        View scrimView = mScrimCoordinator.getViewForTesting();
        assertEquals("The parent view of the scrim is incorrect.", sParent, scrimView.getParent());
        assertTrue(
                "The scrim should be positioned in front of the anchor.",
                sParent.indexOfChild(scrimView) > sParent.indexOfChild(mAnchorView));
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testObserver_clickEvent() throws ExecutionException, TimeoutException {
        showScrim(buildModel(true, false, true, Color.RED), false);

        int callCount = mScrimClickCallbackHelper.getCallCount();
        ScrimView scrimView = mScrimCoordinator.getViewForTesting();
        ThreadUtils.runOnUiThreadBlocking(() -> scrimView.callOnClick());
        mScrimClickCallbackHelper.waitForCallback(callCount, 1);
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testGestureDetector() throws ExecutionException, TimeoutException {
        ColorDrawable customDrawable = new ColorDrawable(Color.BLUE);
        PropertyModel model =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return new PropertyModel.Builder(ScrimProperties.ALL_KEYS)
                                    .with(ScrimProperties.TOP_MARGIN, 0)
                                    .with(ScrimProperties.AFFECTS_STATUS_BAR, false)
                                    .with(ScrimProperties.ANCHOR_VIEW, mAnchorView)
                                    .with(ScrimProperties.SHOW_IN_FRONT_OF_ANCHOR_VIEW, false)
                                    .with(ScrimProperties.CLICK_DELEGATE, mClickDelegate)
                                    .with(
                                            ScrimProperties.VISIBILITY_CALLBACK,
                                            mVisibilityChangeCallback)
                                    .with(ScrimProperties.BACKGROUND_COLOR, Color.RED)
                                    .with(ScrimProperties.BACKGROUND_DRAWABLE, customDrawable)
                                    .with(ScrimProperties.GESTURE_DETECTOR, mCustomGestureDetector)
                                    .build();
                        });
        showScrim(model, false);

        int gestureCallCount = mDelegatedEventHelper.getCallCount();
        ScrimView scrimView = mScrimCoordinator.getViewForTesting();
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        scrimView.dispatchTouchEvent(
                                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0)));
        mDelegatedEventHelper.waitForCallback(gestureCallCount, 1);
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testAnimation_running() throws TimeoutException {
        // The showScrim method includes checks for animation state.
        showScrim(buildModel(true, false, true, Color.RED), true);
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testAnimation_canceled() throws TimeoutException {
        showScrim(buildModel(true, false, true, Color.RED), true);

        ThreadUtils.runOnUiThreadBlocking(() -> mScrimCoordinator.setAlpha(0.5f));

        assertFalse("Animations should not be running.", mScrimCoordinator.areAnimationsRunning());
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testAffectsStatusBar_enabled() throws TimeoutException {
        int callCount = mStatusBarCallbackHelper.getCallCount();
        showScrim(buildModel(true, true, true, Color.RED), false);
        mStatusBarCallbackHelper.waitForCallback(callCount, 1);
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testAffectsStatusBar_disabled() throws TimeoutException {
        int callCount = mStatusBarCallbackHelper.getCallCount();
        showScrim(buildModel(true, false, true, Color.RED), false);

        ThreadUtils.runOnUiThreadBlocking(() -> mScrimCoordinator.setAlpha(0.5f));

        assertEquals(
                "Scrim alpha should be 0.5f.",
                0.5f,
                mScrimCoordinator.getViewForTesting().getAlpha(),
                MathUtils.EPSILON);

        assertEquals(
                "No events to the status bar delegate should have occurred",
                callCount,
                mStatusBarCallbackHelper.getCallCount());
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testAffectsNavigationBar_enabled() throws TimeoutException {
        int callCount = mNavigationBarCallbackHelper.getCallCount();
        PropertyModel model =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return new PropertyModel.Builder(ScrimProperties.ALL_KEYS)
                                    .with(ScrimProperties.TOP_MARGIN, 0)
                                    .with(ScrimProperties.AFFECTS_STATUS_BAR, false)
                                    .with(ScrimProperties.ANCHOR_VIEW, mAnchorView)
                                    .with(ScrimProperties.SHOW_IN_FRONT_OF_ANCHOR_VIEW, false)
                                    .with(ScrimProperties.CLICK_DELEGATE, mClickDelegate)
                                    .with(
                                            ScrimProperties.VISIBILITY_CALLBACK,
                                            mVisibilityChangeCallback)
                                    .with(ScrimProperties.AFFECTS_NAVIGATION_BAR, true)
                                    .build();
                        });
        showScrim(model, false);

        mNavigationBarCallbackHelper.waitForCallback(callCount, 1);
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testAffectsNavigationBar_disabled() throws TimeoutException {
        int callCount = mStatusBarCallbackHelper.getCallCount();
        PropertyModel model =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return new PropertyModel.Builder(ScrimProperties.ALL_KEYS)
                                    .with(ScrimProperties.TOP_MARGIN, 0)
                                    .with(ScrimProperties.AFFECTS_STATUS_BAR, false)
                                    .with(ScrimProperties.ANCHOR_VIEW, mAnchorView)
                                    .with(ScrimProperties.SHOW_IN_FRONT_OF_ANCHOR_VIEW, false)
                                    .with(ScrimProperties.CLICK_DELEGATE, mClickDelegate)
                                    .with(
                                            ScrimProperties.VISIBILITY_CALLBACK,
                                            mVisibilityChangeCallback)
                                    .with(ScrimProperties.AFFECTS_NAVIGATION_BAR, false)
                                    .build();
                        });
        showScrim(model, false);

        assertEquals(
                "No events to the navigation bar delegate should have occurred",
                callCount,
                mNavigationBarCallbackHelper.getCallCount());
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testCustomDrawable() throws TimeoutException {
        ColorDrawable customDrawable = new ColorDrawable(Color.BLUE);
        PropertyModel model =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return new PropertyModel.Builder(ScrimProperties.ALL_KEYS)
                                    .with(ScrimProperties.TOP_MARGIN, 0)
                                    .with(ScrimProperties.AFFECTS_STATUS_BAR, false)
                                    .with(ScrimProperties.ANCHOR_VIEW, mAnchorView)
                                    .with(ScrimProperties.SHOW_IN_FRONT_OF_ANCHOR_VIEW, false)
                                    .with(ScrimProperties.CLICK_DELEGATE, mClickDelegate)
                                    .with(
                                            ScrimProperties.VISIBILITY_CALLBACK,
                                            mVisibilityChangeCallback)
                                    .with(ScrimProperties.BACKGROUND_COLOR, Color.RED)
                                    .with(ScrimProperties.BACKGROUND_DRAWABLE, customDrawable)
                                    .with(ScrimProperties.GESTURE_DETECTOR, null)
                                    .build();
                        });

        showScrim(model, false);

        assertEquals(
                "Scrim should be using a custom background.",
                customDrawable,
                mScrimCoordinator.getViewForTesting().getBackground());

        ThreadUtils.runOnUiThreadBlocking(() -> mScrimCoordinator.hideScrim(false));

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "Scrim should be null after being hidden.",
                            mScrimCoordinator.getViewForTesting(),
                            Matchers.nullValue());
                });
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testTopMargin() throws TimeoutException {
        int topMargin = 100;
        PropertyModel model =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return new PropertyModel.Builder(ScrimProperties.REQUIRED_KEYS)
                                    .with(ScrimProperties.TOP_MARGIN, topMargin)
                                    .with(ScrimProperties.AFFECTS_STATUS_BAR, false)
                                    .with(ScrimProperties.ANCHOR_VIEW, mAnchorView)
                                    .with(ScrimProperties.SHOW_IN_FRONT_OF_ANCHOR_VIEW, false)
                                    .with(ScrimProperties.CLICK_DELEGATE, mClickDelegate)
                                    .with(
                                            ScrimProperties.VISIBILITY_CALLBACK,
                                            mVisibilityChangeCallback)
                                    .build();
                        });

        showScrim(model, false);

        View scrimView = mScrimCoordinator.getViewForTesting();
        assertEquals(
                "Scrim top margin is incorrect.",
                topMargin,
                ((ViewGroup.MarginLayoutParams) scrimView.getLayoutParams()).topMargin);
    }

    @Test
    @SmallTest
    @Feature({"Scrim"})
    public void testOldScrimHidden() throws TimeoutException {
        PropertyModel firstModel = buildModel(false, false, true, Color.RED);
        showScrim(firstModel, false);

        assertScrimVisibility(true);

        View oldScrim = mScrimCoordinator.getViewForTesting();

        showScrim(buildModel(false, false, true, Color.BLUE), false);
        assertScrimColor(Color.BLUE);

        View newScrim = mScrimCoordinator.getViewForTesting();

        assertNotEquals("The view should have changed.", oldScrim, newScrim);
        assertEquals("The old scrim should be gone.", View.GONE, oldScrim.getVisibility());

        ThreadUtils.runOnUiThreadBlocking(
                () -> firstModel.set(ScrimProperties.BACKGROUND_COLOR, Color.MAGENTA));
        assertScrimColor(Color.BLUE);

        ThreadUtils.runOnUiThreadBlocking(() -> mScrimCoordinator.hideScrim(false));
        ThreadUtils.runOnUiThreadBlocking(
                () -> firstModel.set(ScrimProperties.BACKGROUND_COLOR, Color.GREEN));
    }

    /**
     * Build a model to show the scrim with.
     *
     * @param requiredKeysOnly Whether the model should be built with only the required keys.
     * @param affectsStatusBar Whether the status bar should be affected by the scrim.
     * @param showInFrontOfAnchor Whether the scrim shows in front of the anchor view.
     * @param color The color to use for the overlay. If only using required keys, this value is
     *     ignored.
     * @return A model to pass to the scrim coordinator.
     */
    private PropertyModel buildModel(
            boolean requiredKeysOnly,
            boolean affectsStatusBar,
            boolean showInFrontOfAnchor,
            @ColorInt int color) {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    PropertyModel model =
                            new PropertyModel.Builder(
                                            requiredKeysOnly
                                                    ? ScrimProperties.REQUIRED_KEYS
                                                    : ScrimProperties.ALL_KEYS)
                                    .with(ScrimProperties.TOP_MARGIN, 0)
                                    .with(ScrimProperties.AFFECTS_STATUS_BAR, affectsStatusBar)
                                    .with(ScrimProperties.ANCHOR_VIEW, mAnchorView)
                                    .with(
                                            ScrimProperties.SHOW_IN_FRONT_OF_ANCHOR_VIEW,
                                            showInFrontOfAnchor)
                                    .with(ScrimProperties.CLICK_DELEGATE, mClickDelegate)
                                    .with(
                                            ScrimProperties.VISIBILITY_CALLBACK,
                                            mVisibilityChangeCallback)
                                    .build();

                    if (!requiredKeysOnly) {
                        model.set(ScrimProperties.BACKGROUND_COLOR, color);
                        model.set(ScrimProperties.BACKGROUND_DRAWABLE, null);
                        model.set(ScrimProperties.GESTURE_DETECTOR, null);
                    }

                    return model;
                });
    }

    /**
     * Show the scrim and wait for a visibility change.
     *
     * @param model The model to show the scrim with.
     * @param animate Whether the scrim should animate. If false, alpha is immediately set to 100%.
     */
    private void showScrim(PropertyModel model, boolean animate) throws TimeoutException {
        int callCount = mVisibilityChangeCallbackHelper.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mScrimCoordinator.showScrim(model);

                    // Animations are disabled for these types of tests, so just make sure the
                    // animation was created then continue as if we weren't running animation.
                    if (animate) {
                        assertTrue(
                                "Animations should be running.",
                                mScrimCoordinator.areAnimationsRunning());
                    }

                    mScrimCoordinator.forceAnimationToFinish();
                    assertFalse(mScrimCoordinator.areAnimationsRunning());
                    assertEquals(
                            "Scrim should be completely visible.",
                            1.0f,
                            mScrimCoordinator.getViewForTesting().getAlpha(),
                            MathUtils.EPSILON);
                });

        mVisibilityChangeCallbackHelper.waitForCallback(callCount, 1);
        assertScrimVisibility(true);
    }

    /** Assert that the scrim background is a specific color. */
    private void assertScrimColor(@ColorInt int color) {
        assertEquals(
                "Scrim color was incorrect.",
                color,
                ((ColorDrawable) mScrimCoordinator.getViewForTesting().getBackground()).getColor());
    }

    /**
     * Assert that the scrim is the desired visibility.
     *
     * @param visible Whether the scrim should be visible.
     */
    private void assertScrimVisibility(final boolean visible) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    if (visible) {
                        assertEquals(
                                "The scrim should be visible.",
                                View.VISIBLE,
                                mScrimCoordinator.getViewForTesting().getVisibility());
                    } else {
                        assertNull(
                                "The scrim should be null after being hidden.",
                                mScrimCoordinator.getViewForTesting());
                    }
                });
    }
}