chromium/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/optional_button/OptionalButtonViewTest.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 androidx.test.espresso.matcher.ViewMatchers.assertThat;

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.mockito.ArgumentMatchers.any;
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 static org.robolectric.Shadows.shadowOf;

import android.app.Activity;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.transition.Transition;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.widget.ImageViewCompat;

import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.robolectric.Robolectric;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.Callback;
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.R;
import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarButtonVariant;
import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarFeatures;
import org.chromium.chrome.browser.toolbar.optional_button.OptionalButtonConstants.TransitionType;
import org.chromium.ui.listmenu.ListMenuButton;

import java.util.function.BooleanSupplier;

/** Unit tests for OptionalButtonView. */
@RunWith(BaseRobolectricTestRunner.class)
public class OptionalButtonViewTest {
    private Context mActivity;

    private OptionalButtonView mOptionalButtonView;
    private ImageButton mInnerButton;
    private TextView mActionChipLabel;
    private ImageView mButtonBackground;
    private ShadowLooper mShadowLooper;
    private BooleanSupplier mMockAnimationChecker;
    private Callback<Transition> mMockBeginDelayedTransition;
    private ViewGroup mMockTransitionRoot;
    private ListMenuButton mButton;
    private ImageView mAnimationView;

    @Before
    public void setUp() {
        mActivity =
                new ContextThemeWrapper(
                        Robolectric.setupActivity(Activity.class),
                        R.style.Theme_BrowserUI_DayNight);
        mMockAnimationChecker = Mockito.mock(BooleanSupplier.class);
        when(mMockAnimationChecker.getAsBoolean()).thenReturn(true);

        mOptionalButtonView =
                (OptionalButtonView)
                        LayoutInflater.from(mActivity)
                                .inflate(R.layout.optional_button_layout, null);
        mOptionalButtonView.setLayoutParams(
                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        mOptionalButtonView.setIsAnimationAllowedPredicate(mMockAnimationChecker);
        mOptionalButtonView.layout(10, 10, 10, 10);

        mMockBeginDelayedTransition = Mockito.mock(Callback.class);

        mMockTransitionRoot = Mockito.mock(ViewGroup.class);
        when(mMockTransitionRoot.isLaidOut()).thenReturn(true);
        mOptionalButtonView.setTransitionRoot(mMockTransitionRoot);

        mShadowLooper = shadowOf(Looper.getMainLooper());
        Handler handler = new Handler(Looper.getMainLooper());
        // This handler is used to schedule the action chip collapse.
        mOptionalButtonView.setHandlerForTesting(handler);

        // This function replaces calls to TransitionManager.beginDelayedTransition which is hard to
        // mock as it's static.
        mOptionalButtonView.setFakeBeginDelayedTransitionForTesting(mMockBeginDelayedTransition);

        mInnerButton = (ImageButton) mOptionalButtonView.getButtonView();
        mAnimationView = mOptionalButtonView.getAnimationViewForTesting();
        mActionChipLabel = mOptionalButtonView.findViewById(R.id.action_chip_label);
        mButtonBackground =
                mOptionalButtonView.findViewById(R.id.swappable_icon_secondary_background);
        mButton = mOptionalButtonView.findViewById(R.id.optional_toolbar_button);
    }

    private ButtonDataImpl getDataForStaticNewTabIconButton() {
        Drawable iconDrawable = AppCompatResources.getDrawable(mActivity, R.drawable.new_tab_icon);
        return getDataForStaticNewTabIconButton(iconDrawable);
    }

    private ButtonDataImpl getDataForStaticNewTabIconButton(Drawable iconDrawable) {
        OnClickListener clickListener = mock(OnClickListener.class);
        OnLongClickListener longClickListener = mock(OnLongClickListener.class);
        String contentDescription = mActivity.getString(R.string.button_new_tab);

        // Whether a button is static or dynamic is determined by the button variant.
        ButtonSpec buttonSpec =
                new ButtonSpec(
                        iconDrawable,
                        clickListener,
                        longClickListener,
                        contentDescription,
                        true,
                        null,
                        /* buttonVariant= */ AdaptiveToolbarButtonVariant.NEW_TAB,
                        /* actionChipLabelResId= */ Resources.ID_NULL,
                        /* tooltipTextResId= */ R.string.new_tab_title,
                        /* showHoverHighlight= */ true);
        ButtonDataImpl buttonData = new ButtonDataImpl();
        buttonData.setButtonSpec(buttonSpec);
        buttonData.setCanShow(true);
        buttonData.setEnabled(true);

        return buttonData;
    }

    private ButtonDataImpl getDataForReaderModeIconButton() {
        Drawable iconDrawable =
                AppCompatResources.getDrawable(mActivity, R.drawable.ic_mic_white_24dp);
        OnClickListener clickListener = mock(OnClickListener.class);
        OnLongClickListener longClickListener = mock(OnLongClickListener.class);
        String contentDescription = mActivity.getString(R.string.reader_view_text_alt);

        // Whether a button is static or dynamic is determined by the button variant.
        ButtonSpec buttonSpec =
                new ButtonSpec(
                        iconDrawable,
                        clickListener,
                        longClickListener,
                        contentDescription,
                        true,
                        null,
                        /* buttonVariant= */ AdaptiveToolbarButtonVariant.READER_MODE,
                        /* actionChipLabelResId= */ Resources.ID_NULL,
                        /* tooltipTextResId= */ Resources.ID_NULL,
                        /* showHoverHighlight= */ false);
        ButtonDataImpl buttonData = new ButtonDataImpl();
        buttonData.setButtonSpec(buttonSpec);
        buttonData.setCanShow(true);
        buttonData.setEnabled(true);

        return buttonData;
    }

    private ButtonDataImpl getDataForReaderModeActionChip() {
        Drawable iconDrawable = AppCompatResources.getDrawable(mActivity, R.drawable.new_tab_icon);
        OnClickListener clickListener = mock(OnClickListener.class);
        OnLongClickListener longClickListener = mock(OnLongClickListener.class);
        String contentDescription = mActivity.getString(R.string.actionbar_share);
        int actionChipLabelResId = R.string.adaptive_toolbar_button_preference_share;

        ButtonSpec buttonSpec =
                new ButtonSpec(
                        iconDrawable,
                        clickListener,
                        longClickListener,
                        contentDescription,
                        true,
                        null,
                        /* buttonVariant= */ AdaptiveToolbarButtonVariant.READER_MODE,
                        /* actionChipLabelResId= */ actionChipLabelResId,
                        /* tooltipTextResId= */ Resources.ID_NULL,
                        /* showHoverHighlight= */ false);
        ButtonDataImpl buttonData = new ButtonDataImpl();
        buttonData.setButtonSpec(buttonSpec);
        buttonData.setCanShow(true);
        buttonData.setEnabled(true);

        return buttonData;
    }

    private ButtonDataImpl getDataForTestingTooltipText(int buttonVariant, int tooltipTextIdRes) {
        Drawable iconDrawable = AppCompatResources.getDrawable(mActivity, R.drawable.new_tab_icon);
        OnClickListener clickListener = mock(OnClickListener.class);
        OnLongClickListener longClickListener = mock(OnLongClickListener.class);
        String contentDescription = mActivity.getString(R.string.actionbar_share);

        ButtonSpec buttonSpec =
                new ButtonSpec(
                        iconDrawable,
                        clickListener,
                        longClickListener,
                        contentDescription,
                        true,
                        null,
                        /* buttonVariant= */ buttonVariant,
                        0,
                        tooltipTextIdRes,
                        true);
        ButtonDataImpl buttonData = new ButtonDataImpl();
        buttonData.setButtonSpec(buttonSpec);
        buttonData.setCanShow(true);
        buttonData.setEnabled(true);

        return buttonData;
    }

    private ButtonDataImpl getDataForButtonWithoutTint(Drawable iconDrawable) {
        OnClickListener clickListener = mock(OnClickListener.class);
        OnLongClickListener longClickListener = mock(OnLongClickListener.class);
        String contentDescription = mActivity.getString(R.string.actionbar_share);

        ButtonSpec buttonSpec =
                new ButtonSpec(
                        iconDrawable,
                        clickListener,
                        longClickListener,
                        contentDescription,
                        /* supportsTinting= */ false,
                        null,
                        /* buttonVariant= */ AdaptiveToolbarButtonVariant.UNKNOWN,
                        /* actionChipLabelResId= */ Resources.ID_NULL,
                        /* tooltipTextResId= */ Resources.ID_NULL,
                        /* showHoverHighlight= */ false);
        ButtonDataImpl buttonData = new ButtonDataImpl();
        buttonData.setButtonSpec(buttonSpec);
        buttonData.setCanShow(true);
        buttonData.setEnabled(true);

        return buttonData;
    }

    @Test
    public void testHoverTooltipText() {
        // Test whether share button tooltip Text is set correctly.
        ButtonData buttonData =
                getDataForTestingTooltipText(
                        AdaptiveToolbarButtonVariant.SHARE,
                        R.string.adaptive_toolbar_button_preference_share);
        mOptionalButtonView.updateButtonWithAnimation(buttonData);
        Assert.assertEquals(
                "Tooltip text for share button is not as expected",
                mButton.getTooltipText(),
                mActivity
                        .getResources()
                        .getString(R.string.adaptive_toolbar_button_preference_share));

        // Test whether voice search button tooltip Text is set correctly.
        buttonData =
                getDataForTestingTooltipText(
                        AdaptiveToolbarButtonVariant.VOICE,
                        R.string.adaptive_toolbar_button_preference_voice_search);
        mOptionalButtonView.updateButtonWithAnimation(buttonData);
        Assert.assertEquals(
                "Tooltip text for voice search button is not as expected",
                mButton.getTooltipText(),
                mActivity
                        .getResources()
                        .getString(R.string.adaptive_toolbar_button_preference_voice_search));

        // Test whether new tab button tooltip Text is set correctly.
        buttonData =
                getDataForTestingTooltipText(
                        AdaptiveToolbarButtonVariant.NEW_TAB, R.string.button_new_tab);
        mOptionalButtonView.updateButtonWithAnimation(buttonData);
        Assert.assertEquals(
                "Tooltip text for new tab button is not as expected",
                mButton.getTooltipText(),
                mActivity.getResources().getString(R.string.button_new_tab));

        // Test whether reader mode tooltip Text is null.
        buttonData = getDataForTestingTooltipText(AdaptiveToolbarButtonVariant.READER_MODE, 0);
        mOptionalButtonView.updateButtonWithAnimation(buttonData);
        Assert.assertEquals(
                "Tooltip text for reader mode button is not as expected",
                mButton.getTooltipText(),
                null);
    }

    @Test
    public void testSetButtonEnabled() {
        ButtonDataImpl disabledButton = getDataForReaderModeIconButton();
        disabledButton.setEnabled(false);

        mOptionalButtonView.updateButtonWithAnimation(disabledButton);

        // The enabled property should be reflected immediately.
        assertFalse(mOptionalButtonView.getButtonView().isEnabled());
    }

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

        assertEquals(42, mOptionalButtonView.getPaddingStart());
    }

    @Test(expected = IllegalStateException.class)
    public void testSetIconDrawableWithAnimation_throwsExceptionWithNoTransitionRoot() {
        mOptionalButtonView.setTransitionRoot(null);

        // If we don't set a transitionRoot then we can't perform transitions.
        mOptionalButtonView.updateButtonWithAnimation(null);
    }

    @Test(expected = IllegalStateException.class)
    public void testSetIconDrawableWithAnimation_throwsExceptionWithNoAnimationPredicate() {
        // If we don't set an isAnimationAllowedPredicate then we can't perform transitions.
        mOptionalButtonView.setIsAnimationAllowedPredicate(null);
        mOptionalButtonView.updateButtonWithAnimation(null);
    }

    @Test
    public void testSetIconDrawableWithAnimation_fromHiddenToIcon() {
        ButtonData buttonData = getDataForReaderModeIconButton();
        String contentDescriptionString = buttonData.getButtonSpec().getContentDescription();

        mOptionalButtonView.updateButtonWithAnimation(buttonData);

        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        assertEquals(View.VISIBLE, mOptionalButtonView.getVisibility());
        assertEquals(View.VISIBLE, mInnerButton.getVisibility());
        assertEquals(buttonData.getButtonSpec().getDrawable(), mInnerButton.getDrawable());
        assertEquals(contentDescriptionString, mInnerButton.getContentDescription());
        // Dynamic buttons have a background.
        assertEquals(View.VISIBLE, mButtonBackground.getVisibility());
        assertEquals(View.GONE, mActionChipLabel.getVisibility());
    }

    @Test
    public void testSetIconDrawableWithAnimation_swapIcons() {
        ButtonData firstButtonData = getDataForReaderModeIconButton();
        ButtonData secondButtonData = getDataForStaticNewTabIconButton();

        // Transition from hidden to firstIcon.
        mOptionalButtonView.updateButtonWithAnimation(firstButtonData);

        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Transition from firstIcon to secondIcon.
        mOptionalButtonView.updateButtonWithAnimation(secondButtonData);

        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);

        // In the middle of the transition all handlers should be disabled.
        mInnerButton.performClick();
        mInnerButton.performLongClick();

        mOptionalButtonView.onTransitionEnd(null);

        mInnerButton.performClick();
        mInnerButton.performLongClick();

        assertEquals(View.VISIBLE, mOptionalButtonView.getVisibility());
        assertEquals(View.VISIBLE, mInnerButton.getVisibility());
        assertEquals(secondButtonData.getButtonSpec().getDrawable(), mInnerButton.getDrawable());
        // Second button is static, it has no background.
        assertEquals(View.GONE, mButtonBackground.getVisibility());
        verify(secondButtonData.getButtonSpec().getOnClickListener()).onClick(any());
        verify(secondButtonData.getButtonSpec().getOnLongClickListener()).onLongClick(any());
        verifyNoMoreInteractions(
                firstButtonData.getButtonSpec().getOnClickListener(),
                firstButtonData.getButtonSpec().getOnLongClickListener());
    }

    @Test
    public void testSetIconDrawableWithAnimation_swapIcons_withAndWithoutTint() {

        // First button has an icon that supports tinting (e.g. new tab).
        Drawable firstButtonIcon =
                AppCompatResources.getDrawable(mActivity, R.drawable.star_outline_24dp);
        ButtonData firstButtonDataWithTint = getDataForStaticNewTabIconButton(firstButtonIcon);

        // Second button has an icon that doesn't support tinting (e.g. user profile pic).
        Drawable secondButtonIcon =
                AppCompatResources.getDrawable(mActivity, R.drawable.btn_star_filled);
        ButtonData secondButtonDataWithoutTint = getDataForButtonWithoutTint(secondButtonIcon);

        // This tint color comes from the current dynamic color or set by a website.
        ColorStateList tintColor = ColorStateList.valueOf(Color.RED);
        mOptionalButtonView.setColorStateList(tintColor);

        // Transition from hidden to firstIcon.
        mOptionalButtonView.updateButtonWithAnimation(firstButtonDataWithTint);

        // Start transition, normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);

        // Record the drawable being animated and its tint color.
        Drawable buttonDrawableWhileShowing = mInnerButton.getDrawable();
        ColorStateList buttonTintWhileShowing = ImageViewCompat.getImageTintList(mInnerButton);

        // Finish transition, normally called by TransitionManager.
        mOptionalButtonView.onTransitionEnd(null);

        // Record the button's drawable and tint color after the animation.
        Drawable buttonDrawableAfterShowing = mInnerButton.getDrawable();
        ColorStateList buttonTintAfterShowing = ImageViewCompat.getImageTintList(mInnerButton);

        // Transition from the tinted button to the one that doesn't support tint.
        mOptionalButtonView.updateButtonWithAnimation(secondButtonDataWithoutTint);

        // Start transition, Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);

        // During the swap animation we show both buttons at the same time, in this case
        // mInnerButton is hiding the previous tinted icon, while mAnimationView is showing the new
        // untinted icon.
        // Record both drawables and their tints.
        Drawable animationDrawableWhileSwapping = mAnimationView.getDrawable();
        ColorStateList animationTintWhileSwapping =
                ImageViewCompat.getImageTintList(mAnimationView);
        Drawable buttonDrawableWhileSwapping = mInnerButton.getDrawable();
        ColorStateList buttonTintWhileSwapping = ImageViewCompat.getImageTintList(mInnerButton);

        // Finish transition, normally called by TransitionManager.
        mOptionalButtonView.onTransitionEnd(null);

        // After the animation only mInnerButton remains visible, it instantly changes its drawable
        // and tint to the new icon.
        Drawable buttonDrawableAfterSwapping = mInnerButton.getDrawable();
        ColorStateList buttonTintAfterSwapping = ImageViewCompat.getImageTintList(mInnerButton);

        // Verify that while the first button is appearing it is shown with tint.
        assertEquals(firstButtonIcon, buttonDrawableWhileShowing);
        assertEquals(tintColor, buttonTintWhileShowing);

        // Verify that after the first button appeared it maintained its tint.
        assertEquals(firstButtonIcon, buttonDrawableAfterShowing);
        assertEquals(tintColor, buttonTintAfterShowing);

        // Verify that while the second button replaced the first one each one had the correct tint.
        assertEquals(secondButtonIcon, animationDrawableWhileSwapping);
        assertNull(animationTintWhileSwapping);
        assertEquals(firstButtonIcon, buttonDrawableWhileSwapping);
        assertEquals(tintColor, buttonTintWhileSwapping);

        // Verify that after swapping the second button remained untinted.
        assertEquals(secondButtonIcon, buttonDrawableAfterSwapping);
        assertNull(buttonTintAfterSwapping);
    }

    @Test
    public void testSetIconDrawableWithAnimation_expandActionChipFromHidden() {
        ButtonData actionChipButtonData = getDataForReaderModeActionChip();
        String actionChipLabel =
                mActivity
                        .getResources()
                        .getString(actionChipButtonData.getButtonSpec().getActionChipLabelResId());

        // Setting an action chip label string indicates that the button should transition to an
        // action chip
        mOptionalButtonView.updateButtonWithAnimation(actionChipButtonData);

        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);
        // In the middle of the expand transition we shouldn't listen to clicks.
        mInnerButton.performClick();
        mOptionalButtonView.onTransitionEnd(null);

        // Click listener should be set when the expand transition is finished.
        mInnerButton.performClick();

        verify(actionChipButtonData.getButtonSpec().getOnClickListener(), times(1)).onClick(any());
        assertEquals(View.VISIBLE, mOptionalButtonView.getVisibility());
        assertEquals(View.VISIBLE, mInnerButton.getVisibility());
        assertEquals(View.VISIBLE, mActionChipLabel.getVisibility());
        assertEquals(actionChipLabel, mActionChipLabel.getText());
    }

    @Test
    public void testSetIconDrawableWithAnimation_expandAndCollapseActionChipFromHidden() {
        ButtonData actionChipButtonData = getDataForReaderModeActionChip();

        // Setting an action chip label string indicates that the button should transition to an
        // action chip
        mOptionalButtonView.updateButtonWithAnimation(actionChipButtonData);

        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Advance looper to begin collapse transition.
        mShadowLooper.runOneTask();

        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);
        // Click listener should remain active during this transition.
        mInnerButton.performClick();
        mOptionalButtonView.onTransitionEnd(null);

        // Click listener should remain the same after the transition
        mInnerButton.performClick();

        verify(actionChipButtonData.getButtonSpec().getOnClickListener(), times(2)).onClick(any());
        assertEquals(View.VISIBLE, mOptionalButtonView.getVisibility());
        assertEquals(View.VISIBLE, mInnerButton.getVisibility());
        assertEquals(View.GONE, mActionChipLabel.getVisibility());
    }

    @Test
    public void testSetIconDrawableWithAnimation_expandActionChipFromAnotherIcon() {
        ButtonData staticButtonData = getDataForStaticNewTabIconButton();
        ButtonData actionChipButtonData = getDataForReaderModeActionChip();

        // Transition from hidden to staticButton.
        mOptionalButtonView.updateButtonWithAnimation(staticButtonData);

        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Transition from firstIcon to an action chip with secondIcon.
        mOptionalButtonView.updateButtonWithAnimation(actionChipButtonData);

        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);
        // Click button in the middle of the action chip expansion transition, nothing should
        // happen.
        mInnerButton.performClick();
        mOptionalButtonView.onTransitionEnd(null);

        // Second click listener should be set after action chip expansion is finished.
        mInnerButton.performClick();

        verify(actionChipButtonData.getButtonSpec().getOnClickListener(), times(1)).onClick(any());
        verifyNoMoreInteractions(staticButtonData.getButtonSpec().getOnClickListener());
        assertEquals(View.VISIBLE, mOptionalButtonView.getVisibility());
        assertEquals(View.VISIBLE, mInnerButton.getVisibility());
        assertEquals(View.VISIBLE, mActionChipLabel.getVisibility());
    }

    @Test
    public void testUpdateButtonWithAnimation_actionChipWithAlternativeColor() {
        ButtonData actionChipButtonData = getDataForReaderModeActionChip();

        // Outside of tests alternative color is controlled by a field trial param for each button
        // variant.
        AdaptiveToolbarFeatures.setAlternativeColorOverrideForTesting(
                actionChipButtonData.getButtonSpec().getButtonVariant(), true);

        // Transition from hidden to action chip
        mOptionalButtonView.updateButtonWithAnimation(actionChipButtonData);

        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        ColorFilter filterAfterExpansion = mButtonBackground.getColorFilter();

        // Advance looper to begin collapse transition.
        mShadowLooper.runOneTask();
        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        ColorFilter filterAfterCollapse = mButtonBackground.getColorFilter();

        Assert.assertNotNull(filterAfterCollapse);
        Assert.assertNotNull(filterAfterExpansion);
        Assert.assertNotEquals(filterAfterCollapse, filterAfterExpansion);
    }

    @Test
    public void testSetIconDrawableWithAnimation_hideIcon() {
        ButtonData buttonData = getDataForStaticNewTabIconButton();

        // Transition from hidden to firstIcon.
        mOptionalButtonView.updateButtonWithAnimation(buttonData);

        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Setting the icon drawable to null should trigger a hiding transition.
        mOptionalButtonView.updateButtonWithAnimation(null);

        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Button should now be hidden, and its callbacks should be removed.
        mInnerButton.performClick();

        verify(buttonData.getButtonSpec().getOnClickListener(), never()).onClick(any());
        assertEquals(View.GONE, mOptionalButtonView.getVisibility());
        assertEquals(View.GONE, mInnerButton.getVisibility());
        assertEquals(View.GONE, mActionChipLabel.getVisibility());
    }

    @Test
    public void testTransitionCallbacks() {
        ButtonData firstButton = getDataForStaticNewTabIconButton();
        ButtonData secondButton = getDataForReaderModeIconButton();
        ButtonData actionChipButton = getDataForReaderModeActionChip();

        Runnable beforeHideTransitionCallback = mock(Runnable.class);
        Callback<Integer> transitionStartedCallback = mock(Callback.class);
        Callback<Integer> transitionFinishedCallback = mock(Callback.class);

        mOptionalButtonView.setTransitionStartedCallback(transitionStartedCallback);
        mOptionalButtonView.setTransitionFinishedCallback(transitionFinishedCallback);
        mOptionalButtonView.setOnBeforeHideTransitionCallback(beforeHideTransitionCallback);

        InOrder inOrder =
                Mockito.inOrder(
                        beforeHideTransitionCallback,
                        transitionStartedCallback,
                        transitionFinishedCallback);

        // Transition from hidden to firstButton.
        mOptionalButtonView.updateButtonWithAnimation(firstButton);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Transition from firstButton to secondButton
        mOptionalButtonView.updateButtonWithAnimation(secondButton);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Transition back to firstButton.
        mOptionalButtonView.updateButtonWithAnimation(firstButton);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Transition from secondButton to actionChipButton
        mOptionalButtonView.updateButtonWithAnimation(actionChipButton);
        // Run callbacks for expansion transition.
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Advance looper to begin collapse transition.
        mShadowLooper.runOneTask();
        // Run callbacks for collapse transition.
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Transition from actionChipButton to hidden.
        mOptionalButtonView.updateButtonWithAnimation(null);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Verify that callbacks are called in the expected order with the right arguments.
        inOrder.verify(transitionStartedCallback).onResult(TransitionType.SHOWING);
        inOrder.verify(transitionFinishedCallback).onResult(TransitionType.SHOWING);
        inOrder.verify(transitionStartedCallback).onResult(TransitionType.SWAPPING);
        inOrder.verify(transitionFinishedCallback).onResult(TransitionType.SWAPPING);
        inOrder.verify(transitionStartedCallback).onResult(TransitionType.SWAPPING);
        inOrder.verify(transitionFinishedCallback).onResult(TransitionType.SWAPPING);
        inOrder.verify(transitionStartedCallback).onResult(TransitionType.EXPANDING_ACTION_CHIP);
        inOrder.verify(transitionFinishedCallback).onResult(TransitionType.EXPANDING_ACTION_CHIP);
        inOrder.verify(transitionStartedCallback).onResult(TransitionType.COLLAPSING_ACTION_CHIP);
        inOrder.verify(transitionFinishedCallback).onResult(TransitionType.COLLAPSING_ACTION_CHIP);
        inOrder.verify(beforeHideTransitionCallback).run();
        inOrder.verify(transitionStartedCallback).onResult(TransitionType.HIDING);
        inOrder.verify(transitionFinishedCallback).onResult(TransitionType.HIDING);
    }

    @Test
    public void testTransitionCallbacks_withAnimationDisabled() {
        ButtonData firstButton = getDataForStaticNewTabIconButton();
        ButtonData secondButton = getDataForReaderModeIconButton();
        ButtonData actionChipButton = getDataForReaderModeActionChip();
        when(mMockAnimationChecker.getAsBoolean()).thenReturn(false);

        Runnable beforeHideTransitionCallback = mock(Runnable.class);
        Callback<Integer> transitionStartedCallback = mock(Callback.class);
        Callback<Integer> transitionFinishedCallback = mock(Callback.class);

        mOptionalButtonView.setTransitionStartedCallback(transitionStartedCallback);
        mOptionalButtonView.setTransitionFinishedCallback(transitionFinishedCallback);
        mOptionalButtonView.setOnBeforeHideTransitionCallback(beforeHideTransitionCallback);

        InOrder inOrder =
                Mockito.inOrder(
                        beforeHideTransitionCallback,
                        transitionStartedCallback,
                        transitionFinishedCallback);

        // Transition from hidden to firstButton.
        mOptionalButtonView.updateButtonWithAnimation(firstButton);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Transition from firstButton to secondButton
        mOptionalButtonView.updateButtonWithAnimation(secondButton);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Transition back to firstButton.
        mOptionalButtonView.updateButtonWithAnimation(firstButton);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Transition from secondButton to actionChipButton, when animations are disabled we don't
        // expand the action chip, as the width change would look jarring. Instead we just update
        // its icon.
        mOptionalButtonView.updateButtonWithAnimation(actionChipButton);
        // Run callbacks for update transition.
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Transition from actionChipButton to hidden.
        mOptionalButtonView.updateButtonWithAnimation(null);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Verify that callbacks are called in the expected order with the right arguments,
        // non-animated updates use either SHOWING or HIDING.
        inOrder.verify(transitionStartedCallback).onResult(TransitionType.SHOWING);
        inOrder.verify(transitionFinishedCallback).onResult(TransitionType.SHOWING);
        inOrder.verify(transitionStartedCallback).onResult(TransitionType.SHOWING);
        inOrder.verify(transitionFinishedCallback).onResult(TransitionType.SHOWING);
        inOrder.verify(transitionStartedCallback).onResult(TransitionType.SHOWING);
        inOrder.verify(transitionFinishedCallback).onResult(TransitionType.SHOWING);
        inOrder.verify(transitionStartedCallback).onResult(TransitionType.SHOWING);
        inOrder.verify(transitionFinishedCallback).onResult(TransitionType.SHOWING);
        inOrder.verify(beforeHideTransitionCallback).run();
        inOrder.verify(transitionStartedCallback).onResult(TransitionType.HIDING);
        inOrder.verify(transitionFinishedCallback).onResult(TransitionType.HIDING);
    }

    @Test
    public void testUpdateButton_earlyReturnIfNothingChanged() {
        ButtonData firstButton = getDataForStaticNewTabIconButton();

        mOptionalButtonView.updateButtonWithAnimation(firstButton);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        mOptionalButtonView.updateButtonWithAnimation(firstButton);

        // Calling updateButtonWithAnimation with the same button many times shouldn't begin
        // repeated animations.
        verify(mMockBeginDelayedTransition).onResult(any());
    }

    @Test
    public void testUpdateButton_earlyReturnIfSameVariant() {
        ButtonData readerModeButtonData = getDataForReaderModeIconButton();

        mOptionalButtonView.updateButtonWithAnimation(readerModeButtonData);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        mOptionalButtonView.updateButtonWithAnimation(readerModeButtonData);

        // Calling updateButtonWithAnimation with the same button variant many times shouldn't begin
        // repeated animations.
        verify(mMockBeginDelayedTransition).onResult(any());
    }

    @Test
    public void testUpdateButton_sameButtonWithDifferentDrawableTriggersTransition() {
        ButtonDataImpl readerModeButtonData = getDataForReaderModeIconButton();

        mOptionalButtonView.updateButtonWithAnimation(readerModeButtonData);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        Drawable newIconDrawable =
                AppCompatResources.getDrawable(mActivity, R.drawable.new_tab_icon);
        ButtonSpec originalButtonSpec = readerModeButtonData.getButtonSpec();
        // Create a copy of the original ButtonSpec with a different variant.
        readerModeButtonData.setButtonSpec(
                new ButtonSpec(
                        newIconDrawable,
                        originalButtonSpec.getOnClickListener(),
                        originalButtonSpec.getOnLongClickListener(),
                        originalButtonSpec.getContentDescription(),
                        originalButtonSpec.getSupportsTinting(),
                        originalButtonSpec.getIPHCommandBuilder(),
                        originalButtonSpec.getButtonVariant(),
                        originalButtonSpec.getActionChipLabelResId(),
                        originalButtonSpec.getHoverTooltipTextId(),
                        originalButtonSpec.getShouldShowHoverHighlight()));

        mOptionalButtonView.updateButtonWithAnimation(readerModeButtonData);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Calling updateButtonWithAnimation with the same button variant but with a different
        // drawable should begin an animation.
        verify(mMockBeginDelayedTransition, times(2)).onResult(any());
    }

    @Test
    public void testUpdateButton_sameButtonWithDifferentSpecTriggersTransition() {
        ButtonDataImpl buttonData = getDataForStaticNewTabIconButton();

        mOptionalButtonView.updateButtonWithAnimation(buttonData);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Keep the same ButtonData instance, but use a different spec.
        buttonData.setButtonSpec(getDataForReaderModeIconButton().getButtonSpec());

        mOptionalButtonView.updateButtonWithAnimation(buttonData);

        // Calling updateButtonWithAnimation with the same ButtonData instance but with a different
        // variant many times should begin a new animation.
        verify(mMockBeginDelayedTransition, times(2)).onResult(any());
    }

    @Test
    public void testUpdateButton_sameButtonWithDifferentVisibilityTriggersTransition() {
        ButtonDataImpl buttonData = getDataForStaticNewTabIconButton();

        mOptionalButtonView.updateButtonWithAnimation(buttonData);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Keep the same ButtonData instance, but set it to not show.
        buttonData.setCanShow(false);

        mOptionalButtonView.updateButtonWithAnimation(buttonData);

        // Calling updateButtonWithAnimation with the same ButtonData instance but with a different
        // visibility many times should begin a new animation.
        verify(mMockBeginDelayedTransition, times(2)).onResult(any());
    }

    @Test
    public void testUpdateButton_actionChipWidthIsRestricted() {
        ButtonDataImpl buttonData = getDataForReaderModeActionChip();
        // Set a string that's too long as an action chip label. Real code measures the number of
        // pixels this will take on screen, but robolectric just uses the character count, so use
        // any string with more than 150 characters.
        buttonData.updateActionChipResourceId(R.string.sync_encryption_create_passphrase);

        int maxActionChipWidth =
                mOptionalButtonView
                        .getResources()
                        .getDimensionPixelSize(
                                R.dimen.toolbar_phone_optional_button_action_chip_max_width);

        mOptionalButtonView.updateButtonWithAnimation(buttonData);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Button shouldn't be wider than the established maximum.
        assertThat(
                mOptionalButtonView.getLayoutParams().width,
                Matchers.lessThanOrEqualTo(maxActionChipWidth));
    }

    @Test
    public void testUpdateButton_shouldWaitUntilTransitionRootIsLaidOut() {
        ButtonDataImpl buttonData = getDataForReaderModeActionChip();

        // Set transition root to be not laid out. If this happens then TransitionManager won't run
        // any transitions.
        when(mMockTransitionRoot.isLaidOut()).thenReturn(false);

        // Try to update the button before the transition root is laid out.
        mOptionalButtonView.updateButtonWithAnimation(buttonData);

        // We shouldn't begin a transition yet.
        verify(mMockBeginDelayedTransition, never()).onResult(any());

        // Run that listener once the view is laid out.
        when(mMockTransitionRoot.isLaidOut()).thenReturn(true);
        mOptionalButtonView.getViewTreeObserver().dispatchOnGlobalLayout();

        // Now we should begin our transition.
        verify(mMockBeginDelayedTransition).onResult(any());
    }

    @Test
    public void testUpdateButton_shouldIgnoreChangesWhileWaitingForLayout() {
        ButtonDataImpl buttonData = getDataForReaderModeActionChip();

        // Set transition root to be not laid out. If this happens then TransitionManager won't run
        // any transitions.
        when(mMockTransitionRoot.isLaidOut()).thenReturn(false);

        // Try to update the button before its transition root is laid out.
        mOptionalButtonView.updateButtonWithAnimation(buttonData);

        // Change the attributes of buttonData without calling updateButtonWithAnimation again, this
        // should have no effect on the button's state after the transition.
        buttonData.setCanShow(false);

        // Run that listener once the view is laid out.
        when(mMockTransitionRoot.isLaidOut()).thenReturn(true);
        mOptionalButtonView.getViewTreeObserver().dispatchOnGlobalLayout();

        // Normally called by TransitionManager.
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Button should be visible, the property change that happened between
        // updateButtonWithAnimation and layout is ignored.
        assertEquals(View.VISIBLE, mOptionalButtonView.getVisibility());
        assertEquals(View.VISIBLE, mInnerButton.getVisibility());
        assertEquals(View.VISIBLE, mActionChipLabel.getVisibility());
    }

    @Test
    public void testUpdateButton_shouldHideActionChipLabelWhenUpdatingWithoutAnimations() {
        ButtonDataImpl actionChipButtonData = getDataForReaderModeActionChip();
        ButtonDataImpl regularButtonData = getDataForStaticNewTabIconButton();

        // Allow animations
        when(mMockAnimationChecker.getAsBoolean()).thenReturn(true);

        // Show action chip.
        mOptionalButtonView.updateButtonWithAnimation(actionChipButtonData);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Block animations.
        when(mMockAnimationChecker.getAsBoolean()).thenReturn(false);

        // Switch to regular button
        mOptionalButtonView.updateButtonWithAnimation(regularButtonData);
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Action chip shouldn't be visible
        assertEquals(View.GONE, mActionChipLabel.getVisibility());
    }

    @Test
    public void testUpdateButton_sameVariantUpdatesShouldNotBeAnimated() {
        ArgumentCaptor<Transition> transitionArgumentCaptor =
                ArgumentCaptor.forClass(Transition.class);
        // Create two ButtonData objects for the same variant (NEW_TAB) with different icons.
        ButtonDataImpl newTabButtonData =
                getDataForStaticNewTabIconButton(
                        AppCompatResources.getDrawable(mActivity, R.drawable.star_outline_24dp));
        ButtonDataImpl updatedNewTabButtonData =
                getDataForStaticNewTabIconButton(
                        AppCompatResources.getDrawable(mActivity, R.drawable.btn_star_filled));

        // First show the first icon.
        mOptionalButtonView.updateButtonWithAnimation(newTabButtonData);

        verify(mMockBeginDelayedTransition).onResult(transitionArgumentCaptor.capture());
        // Going from no button to a button should be animated.
        assertNotEquals(0, transitionArgumentCaptor.getValue().getDuration());
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Now show the second icon, with the same variant but a different drawable.
        mOptionalButtonView.updateButtonWithAnimation(updatedNewTabButtonData);

        verify(mMockBeginDelayedTransition, times(2)).onResult(transitionArgumentCaptor.capture());
        // Updating the drawable without changing variant should not be animated.
        assertEquals(0, transitionArgumentCaptor.getValue().getDuration());
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Now hide the button.
        mOptionalButtonView.updateButtonWithAnimation(null);
        verify(mMockBeginDelayedTransition, times(3)).onResult(transitionArgumentCaptor.capture());
        // Hiding the button should be animated.
        assertNotEquals(0, transitionArgumentCaptor.getValue().getDuration());
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);
    }

    @Test
    public void testUpdateButton_differentVariantUpdatesShouldBeAnimated() {
        ArgumentCaptor<Transition> transitionArgumentCaptor =
                ArgumentCaptor.forClass(Transition.class);
        // Create two ButtonData objects with different variants.
        ButtonData newTabButtonData = getDataForStaticNewTabIconButton();
        ButtonData readerModeButtonData = getDataForReaderModeIconButton();

        // First show the new tab variant.
        mOptionalButtonView.updateButtonWithAnimation(newTabButtonData);

        verify(mMockBeginDelayedTransition).onResult(transitionArgumentCaptor.capture());
        // Going from no button to a button should be animated.
        assertNotEquals(0, transitionArgumentCaptor.getValue().getDuration());
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);

        // Now show the reader mode button.
        mOptionalButtonView.updateButtonWithAnimation(readerModeButtonData);

        verify(mMockBeginDelayedTransition, times(2)).onResult(transitionArgumentCaptor.capture());
        // Changing variants should be animated.
        assertNotEquals(0, transitionArgumentCaptor.getValue().getDuration());
        mOptionalButtonView.onTransitionStart(null);
        mOptionalButtonView.onTransitionEnd(null);
    }
}