chromium/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabBottomBarDelegateUnitTest.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.browser.customtabs;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import android.app.Activity;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageButton;
import android.widget.RemoteViews;

import androidx.browser.customtabs.CustomTabsIntent;
import androidx.test.ext.junit.rules.ActivityScenarioRule;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browser_controls.BrowserControlsSizer;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.CustomButtonParams;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabProvider;
import org.chromium.chrome.browser.night_mode.SystemNightModeMonitor;
import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.ScrollDirection;
import org.chromium.ui.base.ApplicationViewportInsetSupplier;
import org.chromium.ui.base.TestActivity;
import org.chromium.ui.base.WindowAndroid;

/** Unit test for {@link CustomTabBottomBarDelegate}. */
@RunWith(BaseRobolectricTestRunner.class)
@Batch(Batch.UNIT_TESTS)
@Config(manifest = Config.NONE)
public class CustomTabBottomBarDelegateUnitTest {
    @Rule
    public ActivityScenarioRule<TestActivity> mActivityScenarioRule =
            new ActivityScenarioRule<>(TestActivity.class);

    @Mock private WindowAndroid mWindowAndroid;
    @Mock private BrowserControlsSizer mBrowserControlsSizer;
    @Mock private CustomTabNightModeStateController mNightModeStateController;
    @Mock private SystemNightModeMonitor mSystemNightModeMonitor;
    @Mock private CustomTabActivityTabProvider mTabProvider;
    @Mock private CustomTabCompositorContentInitializer mCompositorContentInitializer;
    @Mock private CustomTabBottomBarView mBottomBarView;
    @Mock private View mShadowView;
    @Mock private RemoteViews mRemoteViews;
    @Mock private Intent mIntent;
    @Mock private PendingIntent mRemoteViewsPendingIntent;
    @Mock private ApplicationViewportInsetSupplier mViewportInsetSupplier;
    @Mock private PendingIntent mSwipeUpPendingIntent;
    @Mock private ImageButton mButtonView;

    private Activity mActivity;
    private BrowserServicesIntentDataProvider mIntentDataProvider;
    private CustomTabBottomBarDelegate mBottomBarDelegate;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mActivityScenarioRule.getScenario().onActivity(activity -> mActivity = activity);
        when(mIntent.getParcelableExtra(CustomTabsIntent.EXTRA_REMOTEVIEWS))
                .thenReturn(mRemoteViews);
        when(mIntent.getIntArrayExtra(CustomTabsIntent.EXTRA_REMOTEVIEWS_VIEW_IDS))
                .thenReturn(new int[] {1});
        when(mIntent.getParcelableExtra(CustomTabsIntent.EXTRA_REMOTEVIEWS_PENDINGINTENT))
                .thenReturn(mRemoteViewsPendingIntent);
        when(mIntent.getParcelableExtra(
                        CustomTabIntentDataProvider.EXTRA_SECONDARY_TOOLBAR_SWIPE_UP_ACTION))
                .thenReturn(mSwipeUpPendingIntent);
        when(mWindowAndroid.getApplicationBottomInsetSupplier()).thenReturn(mViewportInsetSupplier);
        mIntentDataProvider =
                new CustomTabIntentDataProvider(
                        mIntent, mActivity, CustomTabsIntent.COLOR_SCHEME_LIGHT);
        mBottomBarDelegate =
                new CustomTabBottomBarDelegate(
                        mActivity,
                        mWindowAndroid,
                        mIntentDataProvider,
                        mBrowserControlsSizer,
                        mNightModeStateController,
                        mSystemNightModeMonitor,
                        mTabProvider,
                        mCompositorContentInitializer);
        when(mBottomBarView.findViewById(eq(R.id.bottombar_shadow))).thenReturn(mShadowView);
        mBottomBarDelegate.setBottomBarViewForTesting(mBottomBarView);
    }

    @Test
    public void testIsSwipeEnabled() {
        // Swipe should only be enabled when the bottom bar is visible and the direction is up.
        when(mBottomBarView.getVisibility()).thenReturn(View.VISIBLE);
        assertTrue(mBottomBarDelegate.isSwipeEnabled(ScrollDirection.UP));
        assertFalse(mBottomBarDelegate.isSwipeEnabled(ScrollDirection.DOWN));

        when(mBottomBarView.getVisibility()).thenReturn(View.INVISIBLE);
        assertFalse(mBottomBarDelegate.isSwipeEnabled(ScrollDirection.UP));
    }

    @Test
    public void testSendsSwipeIntent() throws CanceledException {
        mBottomBarDelegate.showBottomBarIfNecessary();
        assertEquals(
                mSwipeUpPendingIntent,
                mIntentDataProvider.getSecondaryToolbarSwipeUpPendingIntent());
        // Simulate a swipe up gesture.
        mBottomBarDelegate.onSwipeStarted(
                ScrollDirection.UP, MotionEvent.obtain(0, 10, MotionEvent.ACTION_MOVE, 0f, 10f, 0));
        // Verify the intent is sent.
        verify(mSwipeUpPendingIntent)
                .send(eq(mActivity), anyInt(), any(), any(), any(), any(), any());
    }

    @Test
    public void testSwipeIntentAfterUpdate() throws CanceledException {
        mBottomBarDelegate.showBottomBarIfNecessary();
        var pendingIntent = Mockito.mock(PendingIntent.class);
        mBottomBarDelegate.updateSwipeUpPendingIntent(pendingIntent);
        // Simulate a swipe up gesture.
        mBottomBarDelegate.onSwipeStarted(
                ScrollDirection.UP, MotionEvent.obtain(0, 10, MotionEvent.ACTION_MOVE, 0f, 10f, 0));
        // Verify the intent is sent.
        verify(pendingIntent).send(eq(mActivity), anyInt(), any(), any(), any(), any(), any());
    }

    @Test
    public void testSwipeIntentAfterUpdateToNull() {
        mBottomBarDelegate.showBottomBarIfNecessary();
        mBottomBarDelegate.updateSwipeUpPendingIntent(null);
        // Simulate a swipe up gesture.
        mBottomBarDelegate.onSwipeStarted(
                ScrollDirection.UP, MotionEvent.obtain(0, 10, MotionEvent.ACTION_MOVE, 0f, 10f, 0));
        // No exception should be thrown.
    }

    @Test
    public void testUpdateBottomBarButtons() {
        when(mBottomBarView.findViewById(1)).thenReturn(mButtonView);
        Drawable icon = Mockito.mock(Drawable.class);
        var description = "description";
        CustomButtonParams customButtonParams = Mockito.mock(CustomButtonParams.class);
        when(customButtonParams.getId()).thenReturn(1);
        when(customButtonParams.getIcon(any())).thenReturn(icon);
        when(customButtonParams.getDescription()).thenReturn(description);

        mBottomBarDelegate.updateBottomBarButtons(customButtonParams);

        verify(mButtonView).setImageDrawable(any());
        verify(mButtonView).setContentDescription(eq(description));
    }

    @Test
    public void testUpdateBottomBarButtons_updaterSet_noInteractionsWithButtonView() {
        when(mBottomBarView.findViewById(1)).thenReturn(mButtonView);
        Drawable icon = Mockito.mock(Drawable.class);
        var description = "description";
        CustomButtonParams customButtonParams = Mockito.mock(CustomButtonParams.class);
        when(customButtonParams.getId()).thenReturn(1);
        when(customButtonParams.getIcon(any())).thenReturn(icon);
        when(customButtonParams.getDescription()).thenReturn(description);
        CustomTabBottomBarDelegate.CustomButtonsUpdater updater =
                Mockito.mock(CustomTabBottomBarDelegate.CustomButtonsUpdater.class);
        when(updater.updateBottomBarButton(customButtonParams)).thenReturn(true);
        mBottomBarDelegate.setCustomButtonsUpdater(updater);

        mBottomBarDelegate.updateBottomBarButtons(customButtonParams);

        verify(updater).updateBottomBarButton(eq(customButtonParams));
        verifyNoInteractions(mButtonView);
    }

    @Test
    public void testOnControlsOffsetChanged() {
        when(mBrowserControlsSizer.getBottomControlsMinHeight()).thenReturn(100);

        mBottomBarDelegate.onControlsOffsetChanged(
                /* topOffset= */ 0,
                /* topControlsMinHeightOffset= */ 0,
                /* bottomOffset= */ 0,
                /* bottomControlsMinHeightOffset= */ 0,
                /* needsAnimate= */ false,
                /* isVisibilityForced= */ false);
        verify(mBottomBarView).setTranslationY(-100);
    }

    @Test
    public void testOnBottomControlsHeightChanged() {
        when(mBrowserControlsSizer.getBottomControlsMinHeightOffset()).thenReturn(100);
        when(mBrowserControlsSizer.getBrowserControlHiddenRatio()).thenReturn(1f);
        mBottomBarDelegate.onBottomControlsHeightChanged(
                /* bottomControlsHeight= */ 50, /* bottomControlsMinHeight= */ 0);

        verify(mBottomBarView).setTranslationY(-50); // 1.0 * 50 - 100
    }
}