chromium/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/optional_button/OptionalButtonCoordinatorTest.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.toolbar.optional_button;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.transition.Transition;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import org.chromium.base.Callback;
import org.chromium.base.FeatureList;
import org.chromium.base.FeatureList.TestValues;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.toolbar.ButtonData;
import org.chromium.chrome.browser.toolbar.ButtonData.ButtonSpec;
import org.chromium.chrome.browser.toolbar.ButtonDataImpl;
import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarButtonVariant;
import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarFeatures;
import org.chromium.chrome.browser.toolbar.optional_button.OptionalButtonCoordinator.TransitionType;
import org.chromium.chrome.browser.user_education.IPHCommandBuilder;
import org.chromium.chrome.browser.user_education.UserEducationHelper;
import org.chromium.components.feature_engagement.FeatureConstants;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.ui.widget.ViewRectProvider;

import java.util.function.BooleanSupplier;

/** Unit tests for OptionalButtonCoordinator. */
@RunWith(BaseRobolectricTestRunner.class)
public class OptionalButtonCoordinatorTest {
    @Mock private ViewGroup mMockRootView;
    @Mock private BooleanSupplier mMockIsAnimationAllowedDelegate;
    @Mock private OptionalButtonView mMockOptionalButtonView;
    @Mock private UserEducationHelper mMockUserEducationHelper;
    @Mock private Callback<Transition> mMockBeginDelayedTransition;
    @Mock private Tracker mMockTracker;

    @Captor ArgumentCaptor<Callback<Integer>> mCallbackArgumentCaptor;
    @Captor ArgumentCaptor<ViewRectProvider> mViewRectProviderCaptor;

    OptionalButtonCoordinator mOptionalButtonCoordinator;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        mOptionalButtonCoordinator =
                new OptionalButtonCoordinator(
                        mMockOptionalButtonView,
                        mMockUserEducationHelper,
                        mMockRootView,
                        mMockIsAnimationAllowedDelegate,
                        () -> mMockTracker);
    }

    @Test
    public void testSetOnBeforeHideTransitionCallback() {
        Runnable callback = () -> {};

        mOptionalButtonCoordinator.setOnBeforeHideTransitionCallback(callback);

        verify(mMockOptionalButtonView).setOnBeforeHideTransitionCallback(callback);
    }

    @Test
    public void testSetTransitionStartedCallback() {
        Callback<Integer> callback = result -> {};

        mOptionalButtonCoordinator.setTransitionStartedCallback(callback);

        verify(mMockOptionalButtonView).setTransitionStartedCallback(callback);
    }

    @Test
    public void testSetTransitionFinishedCallback() {
        // On its constructor OptionalButtonCoordinator sets its own transition finished callback.
        verify(mMockOptionalButtonView)
                .setTransitionFinishedCallback(mCallbackArgumentCaptor.capture());
        Callback<Integer> internalCallback = mCallbackArgumentCaptor.getValue();

        Callback<Integer> externalCallback = Mockito.mock(Callback.class);

        // Set a callback.
        mOptionalButtonCoordinator.setTransitionFinishedCallback(externalCallback);

        // This callback won't be passed to the view, it'll be wrapped by the Coordinator's own
        // callback.
        verify(mMockOptionalButtonView, never()).setTransitionFinishedCallback(externalCallback);

        // Check that the external callback is wrapped by the internal one.
        internalCallback.onResult(5);
        verify(externalCallback).onResult(5);
    }

    @Test
    public void testSetIconForegroundColor() {
        ColorStateList colorStateList = ColorStateList.valueOf(Color.RED);

        mOptionalButtonCoordinator.setIconForegroundColor(colorStateList);

        verify(mMockOptionalButtonView).setColorStateList(colorStateList);
    }

    @Test
    public void testSetBackgroundColorFilter() {
        mOptionalButtonCoordinator.setBackgroundColorFilter(Color.GREEN);

        verify(mMockOptionalButtonView).setBackgroundColorFilter(Color.GREEN);
    }

    @Test
    public void testSetPaddingStart() {
        mOptionalButtonCoordinator.setPaddingStart(42);

        verify(mMockOptionalButtonView).setPaddingStart(42);
    }

    @Test
    public void testCancelTransition() {
        mOptionalButtonCoordinator.cancelTransition();
        mOptionalButtonCoordinator.cancelTransition();

        verify(mMockOptionalButtonView, times(2)).cancelTransition();
    }

    @Test
    public void testGetViewVisibility() {
        when(mMockOptionalButtonView.getVisibility()).thenReturn(View.VISIBLE);

        assertEquals(View.VISIBLE, mOptionalButtonCoordinator.getViewVisibility());

        verify(mMockOptionalButtonView).getVisibility();
    }

    @Test
    public void testGetViewWidth() {
        when(mMockOptionalButtonView.getWidth()).thenReturn(100);

        assertEquals(100, mOptionalButtonCoordinator.getViewWidth());

        verify(mMockOptionalButtonView).getWidth();
    }

    @Test
    public void testGetViewForDrawing() {
        assertEquals(mMockOptionalButtonView, mOptionalButtonCoordinator.getViewForDrawing());
    }

    @Test
    public void testGetButtonView() {
        View mockView = mock(View.class);
        when(mMockOptionalButtonView.getButtonView()).thenReturn(mockView);

        assertEquals(mockView, mOptionalButtonCoordinator.getButtonViewForTesting());

        verify(mMockOptionalButtonView).getButtonView();
    }

    @Test
    public void testUpdateButton_backgroundVisible() {
        Drawable iconDrawable = mock(Drawable.class);
        OnClickListener clickListener = view -> {};
        IPHCommandBuilder mockIphCommandBuilder = mock(IPHCommandBuilder.class);
        String contentDescription = "description";
        boolean isEnabled = true;
        ButtonData buttonData =
                new ButtonDataImpl(
                        /* canShow= */ true,
                        iconDrawable,
                        clickListener,
                        contentDescription,
                        /* supportsTinting= */ true,
                        mockIphCommandBuilder,
                        /* isEnabled= */ isEnabled,
                        AdaptiveToolbarButtonVariant.UNKNOWN,
                        /* tooltipTextResId= */ Resources.ID_NULL,
                        /* showHoverHighlight= */ false);

        View backgroundView = Mockito.mock(View.class);
        doReturn(View.VISIBLE).when(backgroundView).getVisibility();
        doReturn(backgroundView).when(mMockOptionalButtonView).getBackgroundView();

        mOptionalButtonCoordinator.updateButton(buttonData);

        // IPH command builder must be populated with view specific properties.
        verify(mockIphCommandBuilder).setAnchorView(eq(backgroundView));
        verify(mockIphCommandBuilder).setViewRectProvider(mViewRectProviderCaptor.capture());
        assertEquals(backgroundView, mViewRectProviderCaptor.getValue().getViewForTesting());
        verify(mockIphCommandBuilder).setHighlightParams(any());
        verify(mockIphCommandBuilder).setOnShowCallback(any());
        verify(mockIphCommandBuilder).setOnDismissCallback(any());
        verifyNoMoreInteractions(mockIphCommandBuilder);

        verify(mMockOptionalButtonView).updateButtonWithAnimation(buttonData);
    }

    @Test
    public void testUpdateButton_backgroundGone() {
        Drawable iconDrawable = mock(Drawable.class);
        OnClickListener clickListener = view -> {};
        IPHCommandBuilder mockIphCommandBuilder = mock(IPHCommandBuilder.class);
        String contentDescription = "description";
        boolean isEnabled = true;
        ButtonData buttonData =
                new ButtonDataImpl(
                        /* canShow= */ true,
                        iconDrawable,
                        clickListener,
                        contentDescription,
                        /* supportsTinting= */ true,
                        mockIphCommandBuilder,
                        /* isEnabled= */ isEnabled,
                        AdaptiveToolbarButtonVariant.UNKNOWN,
                        /* tooltipTextResId= */ Resources.ID_NULL,
                        /* showHoverHighlight= */ false);

        View backgroundView = Mockito.mock(View.class);
        doReturn(View.GONE).when(backgroundView).getVisibility();
        doReturn(backgroundView).when(mMockOptionalButtonView).getBackgroundView();

        mOptionalButtonCoordinator.updateButton(buttonData);

        // IPH command builder must be populated with view specific properties.
        verify(mockIphCommandBuilder).setAnchorView(eq(mMockOptionalButtonView));
        verify(mockIphCommandBuilder).setViewRectProvider(mViewRectProviderCaptor.capture());
        assertEquals(
                mMockOptionalButtonView, mViewRectProviderCaptor.getValue().getViewForTesting());
        verify(mockIphCommandBuilder).setHighlightParams(any());
        verify(mockIphCommandBuilder).setOnShowCallback(any());
        verify(mockIphCommandBuilder).setOnDismissCallback(any());
        verifyNoMoreInteractions(mockIphCommandBuilder);

        verify(mMockOptionalButtonView).updateButtonWithAnimation(buttonData);
    }

    @Test
    public void testUpdateButton_showingIphChangesBackgroundAlpha() {
        Drawable iconDrawable = mock(Drawable.class);
        OnClickListener clickListener = view -> {};
        IPHCommandBuilder mockIphCommandBuilder = mock(IPHCommandBuilder.class);
        String contentDescription = "description";
        boolean isEnabled = true;
        ButtonData buttonData =
                new ButtonDataImpl(
                        /* canShow= */ true,
                        iconDrawable,
                        clickListener,
                        contentDescription,
                        /* supportsTinting= */ true,
                        mockIphCommandBuilder,
                        /* isEnabled= */ isEnabled,
                        AdaptiveToolbarButtonVariant.UNKNOWN,
                        /* tooltipTextResId= */ Resources.ID_NULL,
                        /* showHoverHighlight= */ false);

        ArgumentCaptor<Runnable> onShowCallbackCaptor = ArgumentCaptor.forClass(Runnable.class);
        ArgumentCaptor<Runnable> onDismissCallbackCaptor = ArgumentCaptor.forClass(Runnable.class);

        mOptionalButtonCoordinator.updateButton(buttonData);

        verify(mockIphCommandBuilder).setOnShowCallback(onShowCallbackCaptor.capture());
        verify(mockIphCommandBuilder).setOnDismissCallback(onDismissCallbackCaptor.capture());

        // Showing an IPH should make the background transparent to be able to see the highlight.
        onShowCallbackCaptor.getValue().run();
        verify(mMockOptionalButtonView).setBackgroundAlpha(0);

        // Dismissing the IPH should bring back the background to normal.
        onDismissCallbackCaptor.getValue().run();
        verify(mMockOptionalButtonView, atLeastOnce()).setBackgroundAlpha(255);
    }

    @Test
    public void testUpdateButton_actionChipResourceIdGetsRemovedWhenNotInVariant() {
        AdaptiveToolbarFeatures.setIsDynamicActionForTesting(
                AdaptiveToolbarButtonVariant.TEST_BUTTON, true);
        TestValues testValues = new TestValues();
        testValues.addFieldTrialParamOverride(
                AdaptiveToolbarFeatures.CONTEXTUAL_PAGE_ACTION_TEST_FEATURE_NAME,
                "action_chip",
                "false");
        FeatureList.setTestValues(testValues);

        Drawable iconDrawable = mock(Drawable.class);
        OnClickListener clickListener = view -> {};
        IPHCommandBuilder mockIphCommandBuilder = mock(IPHCommandBuilder.class);
        String contentDescription = "description";
        int actionChipResourceId = 987654;
        boolean isEnabled = true;
        ButtonData buttonData =
                new ButtonDataImpl(
                        /* canShow= */ true,
                        iconDrawable,
                        clickListener,
                        contentDescription,
                        actionChipResourceId,
                        /* supportsTinting= */ true,
                        mockIphCommandBuilder,
                        /* isEnabled= */ isEnabled,
                        AdaptiveToolbarButtonVariant.TEST_BUTTON,
                        /* tooltipTextResId= */ Resources.ID_NULL,
                        /* showHoverHighlight= */ false);

        mOptionalButtonCoordinator.updateButton(buttonData);

        verify(mMockOptionalButtonView).updateButtonWithAnimation(buttonData);
        Assert.assertEquals(
                Resources.ID_NULL, buttonData.getButtonSpec().getActionChipLabelResId());
    }

    @Test
    public void testUpdateButton_actionChipResourceIdGetsRemovedByFeatureEngagement() {
        AdaptiveToolbarFeatures.setIsDynamicActionForTesting(
                AdaptiveToolbarButtonVariant.TEST_BUTTON, true);
        TestValues testValues = new TestValues();
        testValues.addFieldTrialParamOverride(
                AdaptiveToolbarFeatures.CONTEXTUAL_PAGE_ACTION_TEST_FEATURE_NAME,
                "action_chip",
                "true");
        FeatureList.setTestValues(testValues);

        doReturn(true).when(mMockTracker).isInitialized();
        doReturn(false)
                .when(mMockTracker)
                .shouldTriggerHelpUI(FeatureConstants.CONTEXTUAL_PAGE_ACTIONS_ACTION_CHIP);

        Drawable iconDrawable = mock(Drawable.class);
        OnClickListener clickListener = view -> {};
        IPHCommandBuilder mockIphCommandBuilder = mock(IPHCommandBuilder.class);
        String contentDescription = "description";
        int actionChipResourceId = 987654;
        boolean isEnabled = true;
        ButtonData buttonData =
                new ButtonDataImpl(
                        /* canShow= */ true,
                        iconDrawable,
                        clickListener,
                        contentDescription,
                        actionChipResourceId,
                        /* supportsTinting= */ true,
                        mockIphCommandBuilder,
                        /* isEnabled= */ isEnabled,
                        AdaptiveToolbarButtonVariant.TEST_BUTTON,
                        /* tooltipTextResId= */ Resources.ID_NULL,
                        /* showHoverHighlight= */ false);

        mOptionalButtonCoordinator.updateButton(buttonData);

        verify(mMockOptionalButtonView).updateButtonWithAnimation(buttonData);
        Assert.assertEquals(
                Resources.ID_NULL, buttonData.getButtonSpec().getActionChipLabelResId());
    }

    @Test
    public void testUpdateButton_actionChipResourceIdGetsKeptByFeatureEngagement() {
        AdaptiveToolbarFeatures.setIsDynamicActionForTesting(
                AdaptiveToolbarButtonVariant.TEST_BUTTON, true);
        TestValues testValues = new TestValues();
        testValues.addFieldTrialParamOverride(
                AdaptiveToolbarFeatures.CONTEXTUAL_PAGE_ACTION_TEST_FEATURE_NAME,
                "action_chip",
                "true");
        FeatureList.setTestValues(testValues);

        doReturn(true).when(mMockTracker).isInitialized();
        doReturn(true)
                .when(mMockTracker)
                .shouldTriggerHelpUI(FeatureConstants.CONTEXTUAL_PAGE_ACTIONS_ACTION_CHIP);

        Drawable iconDrawable = mock(Drawable.class);
        OnClickListener clickListener = view -> {};
        IPHCommandBuilder mockIphCommandBuilder = mock(IPHCommandBuilder.class);
        String contentDescription = "description";
        int actionChipResourceId = 987654;
        boolean isEnabled = true;
        ButtonData buttonData =
                new ButtonDataImpl(
                        /* canShow= */ true,
                        iconDrawable,
                        clickListener,
                        contentDescription,
                        actionChipResourceId,
                        /* supportsTinting= */ true,
                        mockIphCommandBuilder,
                        /* isEnabled= */ isEnabled,
                        AdaptiveToolbarButtonVariant.TEST_BUTTON,
                        /* tooltipTextResId= */ Resources.ID_NULL,
                        /* showHoverHighlight= */ false);

        mOptionalButtonCoordinator.updateButton(buttonData);

        verify(mMockOptionalButtonView).updateButtonWithAnimation(buttonData);
        Assert.assertEquals(
                actionChipResourceId, buttonData.getButtonSpec().getActionChipLabelResId());
    }

    @Test
    public void testUpdateButton_disableButtonWithoutChanges() {
        View mockButtonView = mock(View.class);
        when(mMockOptionalButtonView.getButtonView()).thenReturn(mockButtonView);

        Drawable iconDrawable = mock(Drawable.class);
        OnClickListener clickListener = view -> {};
        String contentDescription = "description";
        ButtonDataImpl buttonData =
                new ButtonDataImpl(
                        /* canShow= */ true,
                        iconDrawable,
                        clickListener,
                        contentDescription,
                        /* supportsTinting= */ true,
                        /* iphCommandBuilder= */ null,
                        /* isEnabled= */ true,
                        AdaptiveToolbarButtonVariant.UNKNOWN,
                        /* tooltipTextResId= */ Resources.ID_NULL,
                        /* showHoverHighlight= */ false);

        // Call update button with an enabled button.
        mOptionalButtonCoordinator.updateButton(buttonData);

        buttonData.setEnabled(false);

        // Call updateButton with the same data, but with enabled = false.
        mOptionalButtonCoordinator.updateButton(buttonData);

        // Button should be disabled.
        verify(mockButtonView).setEnabled(false);
        verify(mMockOptionalButtonView, times(2)).updateButtonWithAnimation(buttonData);
    }

    @Test
    public void testShowIphAfterButtonUpdateTransition() {
        verify(mMockOptionalButtonView)
                .setTransitionFinishedCallback(mCallbackArgumentCaptor.capture());
        Callback<Integer> transitionFinishedCallback = mCallbackArgumentCaptor.getValue();

        Drawable iconDrawable = mock(Drawable.class);
        OnClickListener clickListener = view -> {};
        OnLongClickListener longClickListener =
                view -> {
                    return false;
                };
        IPHCommandBuilder mockIphCommandBuilder = mock(IPHCommandBuilder.class);
        String contentDescription = "description";
        boolean isEnabled = true;
        ButtonSpec buttonSpec =
                new ButtonSpec(
                        iconDrawable,
                        clickListener,
                        longClickListener,
                        contentDescription,
                        true,
                        mockIphCommandBuilder,
                        AdaptiveToolbarButtonVariant.UNKNOWN,
                        /* actionChipLabelResId= */ 0,
                        /* tooltipTextResId= */ Resources.ID_NULL,
                        /* showHoverHighlight= */ false);
        ButtonDataImpl buttonData = new ButtonDataImpl();
        buttonData.setButtonSpec(buttonSpec);
        buttonData.setEnabled(isEnabled);

        mOptionalButtonCoordinator.updateButton(buttonData);

        // Call the finished callback twice to ensure the IPH is only shown once.
        transitionFinishedCallback.onResult(TransitionType.SWAPPING);
        transitionFinishedCallback.onResult(TransitionType.SWAPPING);

        // IPH should have been built and shown only once.
        verify(mockIphCommandBuilder).build();
        verify(mMockUserEducationHelper).requestShowIPH(any());
    }
}