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

import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;

import android.app.Activity;
import android.content.res.Resources;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.graphics.drawable.VectorDrawable;
import android.os.Looper;
import android.view.View;
import android.widget.ImageButton;
import android.widget.LinearLayout;

import androidx.annotation.DrawableRes;
import androidx.test.ext.junit.rules.ActivityScenarioRule;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
import org.robolectric.shadows.ShadowDrawable;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.R;
import org.chromium.ui.base.TestActivity;

import java.util.concurrent.TimeUnit;

/** Unit test for {@link CustomTabToolbarAnimationDelegate}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(shadows = {ShadowLooper.class, ShadowDrawable.class})
@LooperMode(Mode.PAUSED)
public class BrandingSecurityButtonAnimationDelegateUnitTest {
    private static final @DrawableRes int ICON_16_DP = R.drawable.ic_group_icon_16dp;
    private static final @DrawableRes int ICON_24_DP = R.drawable.ic_globe_24dp;

    @Rule
    public ActivityScenarioRule<TestActivity> mActivityScenario =
            new ActivityScenarioRule<>(TestActivity.class);

    @Spy ImageButton mImageButton;
    Activity mActivity;

    private BrandingSecurityButtonAnimationDelegate mAnimationDelegate;

    @Before
    public void setup() {
        mActivityScenario.getScenario().onActivity((activity -> mActivity = activity));

        LinearLayout content = new LinearLayout(mActivity, null);
        content.setOrientation(LinearLayout.HORIZONTAL);
        mActivity.setContentView(content);

        mImageButton = Mockito.spy(new ImageButton(mActivity, null));
        content.addView(mImageButton);

        mAnimationDelegate = new BrandingSecurityButtonAnimationDelegate(mImageButton);
    }

    @Test
    public void testTransition_Invisible() {
        // There's no great way to test the animation. This test case focuses on checking the
        // drawable when transition start -> ongoing -> finish.
        setupInitialImageButtonState();

        mImageButton.setVisibility(View.INVISIBLE);
        mAnimationDelegate.updateDrawableResource(ICON_24_DP);
        assertDrawableResource(ICON_24_DP, mImageButton.getDrawable());
    }

    @Test
    public void testTransition_Rotation() {
        // There's no great way to test the animation. This test case focuses on checking the
        // drawable when transition start -> ongoing -> finish.
        setupInitialImageButtonState();

        mAnimationDelegate.updateDrawableResource(ICON_24_DP);
        Assert.assertTrue(
                "Drawable should be a TransitionDrawable during animation.",
                mImageButton.getDrawable() instanceof TransitionDrawable);

        // Advance looper so the animation finishes.
        advanceLooper(BrandingSecurityButtonAnimationDelegate.ICON_ANIMATION_DURATION_MS);
        assertDrawableResource(ICON_24_DP, mImageButton.getDrawable());
        verify(mImageButton, atLeastOnce()).setRotation(anyFloat());
        Assert.assertEquals("Rotation should be reset.", 0, mImageButton.getRotation(), 0.01f);
    }

    @Test
    public void testTransition_Rotation_UpdateAgainDuringTransition() {
        // There's no great way to test the animation. This test case focuses on checking the
        // drawable when transition start -> ongoing -> finish.
        setupInitialImageButtonState();

        mAnimationDelegate.updateDrawableResource(ICON_24_DP);
        Assert.assertTrue(
                "Drawable should be a TransitionDrawable during animation.",
                mImageButton.getDrawable() instanceof TransitionDrawable);

        // Advance half way through the animation.
        advanceLooper(BrandingSecurityButtonAnimationDelegate.ICON_ANIMATION_DURATION_MS / 2);

        // Start another transition back to the original icon.
        mAnimationDelegate.updateDrawableResource(ICON_16_DP);
        advanceLooper(BrandingSecurityButtonAnimationDelegate.ICON_ANIMATION_DURATION_MS);
        assertDrawableResource(ICON_16_DP, mImageButton.getDrawable());
        verify(mImageButton, atLeastOnce()).setRotation(anyFloat());
        Assert.assertEquals("Rotation should be reset.", 0, mImageButton.getRotation(), 0.01f);
    }

    @Test
    public void testResizeToBitmapDrawable() {
        Resources resources = mActivity.getResources();
        Drawable drawable = ApiCompatibilityUtils.getDrawable(resources, ICON_24_DP);
        Assert.assertTrue(drawable instanceof VectorDrawable);

        int width = drawable.getIntrinsicWidth();
        int height = drawable.getIntrinsicHeight();

        BitmapDrawable d1 =
                BrandingSecurityButtonAnimationDelegate.resizeToBitmapDrawable(
                        resources, drawable, width, height);
        Assert.assertEquals(
                "Width of the resized drawable is different.", width, d1.getIntrinsicWidth());
        Assert.assertEquals(
                "Height of the resized drawable is different.", height, d1.getIntrinsicHeight());

        BitmapDrawable d2 =
                BrandingSecurityButtonAnimationDelegate.resizeToBitmapDrawable(
                        resources, drawable, width * 2, height * 2);
        Assert.assertEquals(
                "Width of the resized drawable is different.", width * 2, d2.getIntrinsicWidth());
        Assert.assertEquals(
                "Height of the resized drawable is different.",
                height * 2,
                d2.getIntrinsicHeight());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testResizeDrawable_InvalidInput() {
        Resources resources = mActivity.getResources();
        Drawable drawable = ApiCompatibilityUtils.getDrawable(resources, ICON_24_DP);
        Assert.assertTrue(drawable instanceof VectorDrawable);

        int width = drawable.getIntrinsicWidth();
        int height = drawable.getIntrinsicHeight();

        BitmapDrawable d3 =
                BrandingSecurityButtonAnimationDelegate.resizeToBitmapDrawable(
                        resources, drawable, width - 1, height - 1);
    }

    private void setupInitialImageButtonState() {
        mAnimationDelegate.updateDrawableResource(ICON_16_DP);
        // No transition is added for the first drawable.
        assertDrawableResource(ICON_16_DP, mImageButton.getDrawable());
        mImageButton.setVisibility(View.VISIBLE);
    }

    private void advanceLooper(long durationMs) {
        Shadows.shadowOf(Looper.getMainLooper()).idleFor(durationMs, TimeUnit.MILLISECONDS);
    }

    private void assertDrawableResource(@DrawableRes int drawableRes, Drawable drawable) {
        ShadowDrawable shadowDrawable = Shadows.shadowOf(drawable);
        Assert.assertEquals(
                "Drawable resource is not equal.",
                drawableRes,
                shadowDrawable.getCreatedFromResId());
    }
}