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

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.widget.ImageView;
import android.widget.TextView;

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.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.Callback;
import org.chromium.base.supplier.LazyOneshotSupplier;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.bookmarks.ImprovedBookmarkRowProperties.ImageVisibility;
import org.chromium.ui.base.TestActivity;
import org.chromium.ui.listmenu.ListMenuButtonDelegate;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;

/** Unit tests for {@link ImprovedBookmarkRow}. */
@Batch(Batch.UNIT_TESTS)
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ImprovedBookmarkRowTest {
    private static final String TITLE = "Test title";
    private static final String DESCRIPTION = "Test description";

    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();

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

    @Mock View mView;
    @Mock ViewGroup mViewGroup;
    @Mock ListMenuButtonDelegate mListMenuButtonDelegate;
    @Mock Runnable mPopupListener;
    @Mock Runnable mOpenBookmarkCallback;
    @Mock LazyOneshotSupplier<Drawable> mMockDrawableSupplier;

    ImageView mStartImageView;
    @Spy ViewPropertyAnimator mStartImageViewAnimator;

    @Captor ArgumentCaptor<Callback<Drawable>> mDrawableCallbackCaptor;

    Activity mActivity;
    ImprovedBookmarkRow mImprovedBookmarkRow;
    PropertyModel mModel;
    BitmapDrawable mDrawable;
    LazyOneshotSupplier<Drawable> mDrawableSupplier;
    LazyOneshotSupplier<Drawable> mNullDrawableSupplier;

    @Before
    public void setUp() {
        mActivityScenarioRule.getScenario().onActivity((activity) -> mActivity = activity);
        mStartImageView =
                spy(
                        new ImageView(mActivity) {
                            @Override
                            public ViewPropertyAnimator animate() {
                                ViewPropertyAnimator animator = super.animate();
                                mStartImageViewAnimator = spy(animator);
                                return mStartImageViewAnimator;
                            }
                        });
        mDrawable =
                new BitmapDrawable(
                        mActivity.getResources(),
                        Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
        mDrawableSupplier = LazyOneshotSupplier.fromValue(mDrawable);
        mNullDrawableSupplier = LazyOneshotSupplier.fromValue(null);
        mImprovedBookmarkRow = ImprovedBookmarkRow.buildView(mActivity, /* isVisual= */ true);

        mModel =
                new PropertyModel.Builder(ImprovedBookmarkRowProperties.ALL_KEYS)
                        .with(ImprovedBookmarkRowProperties.TITLE, TITLE)
                        .with(ImprovedBookmarkRowProperties.DESCRIPTION, DESCRIPTION)
                        .with(ImprovedBookmarkRowProperties.DESCRIPTION_VISIBLE, true)
                        .with(
                                ImprovedBookmarkRowProperties.LIST_MENU_BUTTON_DELEGATE,
                                mListMenuButtonDelegate)
                        .with(ImprovedBookmarkRowProperties.POPUP_LISTENER, mPopupListener)
                        .with(
                                ImprovedBookmarkRowProperties.ROW_CLICK_LISTENER,
                                mOpenBookmarkCallback)
                        .with(ImprovedBookmarkRowProperties.EDITABLE, true)
                        .with(
                                ImprovedBookmarkRowProperties.END_IMAGE_VISIBILITY,
                                ImageVisibility.MENU)
                        .build();

        PropertyModelChangeProcessor.create(
                mModel, mImprovedBookmarkRow, ImprovedBookmarkRowViewBinder::bind);
    }

    private void toggleSelection() {
        mModel.set(ImprovedBookmarkRowProperties.SELECTED, true);
        mModel.set(ImprovedBookmarkRowProperties.SELECTION_ACTIVE, true);
        mModel.set(ImprovedBookmarkRowProperties.SELECTED, false);
        mModel.set(ImprovedBookmarkRowProperties.SELECTION_ACTIVE, false);
    }

    @Test
    public void testTitleAndDescription() {
        Assert.assertEquals(
                TITLE, ((TextView) mImprovedBookmarkRow.findViewById(R.id.title)).getText());
        Assert.assertEquals(
                DESCRIPTION,
                ((TextView) mImprovedBookmarkRow.findViewById(R.id.description)).getText());
    }

    @Test
    public void testDescriptionVisibility() {
        mModel.set(ImprovedBookmarkRowProperties.DESCRIPTION_VISIBLE, false);
        Assert.assertEquals(
                View.GONE, mImprovedBookmarkRow.findViewById(R.id.description).getVisibility());
    }

    @Test
    public void testNullAccessoryViewClearsExistingViews() {
        mModel.set(ImprovedBookmarkRowProperties.ACCESSORY_VIEW, mView);
        Assert.assertEquals(
                0,
                ((ViewGroup) mImprovedBookmarkRow.findViewById(R.id.custom_content_container))
                        .indexOfChild(mView));

        mModel.set(ImprovedBookmarkRowProperties.ACCESSORY_VIEW, null);
        Assert.assertEquals(
                -1,
                ((ViewGroup) mImprovedBookmarkRow.findViewById(R.id.custom_content_container))
                        .indexOfChild(mView));
    }

    @Test
    public void testSelectedShowsCheck() {
        mModel.set(ImprovedBookmarkRowProperties.SELECTION_ACTIVE, true);
        mModel.set(ImprovedBookmarkRowProperties.SELECTED, true);
        Assert.assertEquals(
                View.VISIBLE, mImprovedBookmarkRow.findViewById(R.id.check_image).getVisibility());
        Assert.assertEquals(
                View.GONE, mImprovedBookmarkRow.findViewById(R.id.more).getVisibility());
    }

    @Test
    public void testUnselectedShowsLastActive() {
        View check = mImprovedBookmarkRow.findViewById(R.id.check_image);
        View more = mImprovedBookmarkRow.findViewById(R.id.more);
        View image = mImprovedBookmarkRow.findViewById(R.id.end_image);

        // More button is set as visible, so it should be visible after the selection transition.
        mModel.set(ImprovedBookmarkRowProperties.END_IMAGE_VISIBILITY, ImageVisibility.MENU);
        toggleSelection();
        Assert.assertEquals(View.GONE, check.getVisibility());
        Assert.assertEquals(View.GONE, image.getVisibility());
        Assert.assertEquals(View.VISIBLE, more.getVisibility());

        mModel.set(ImprovedBookmarkRowProperties.END_IMAGE_VISIBILITY, ImageVisibility.DRAWABLE);
        toggleSelection();
        Assert.assertEquals(View.GONE, check.getVisibility());
        Assert.assertEquals(View.GONE, more.getVisibility());
        Assert.assertEquals(View.VISIBLE, image.getVisibility());

        mModel.set(ImprovedBookmarkRowProperties.END_IMAGE_VISIBILITY, ImageVisibility.NONE);
        toggleSelection();
        Assert.assertEquals(View.GONE, check.getVisibility());
        Assert.assertEquals(View.GONE, more.getVisibility());
        Assert.assertEquals(View.GONE, image.getVisibility());
    }

    @Test
    public void testSelectionActive() {
        mModel.set(ImprovedBookmarkRowProperties.SELECTION_ACTIVE, true);
        assertFalse(mImprovedBookmarkRow.findViewById(R.id.more).isClickable());
        assertFalse(mImprovedBookmarkRow.findViewById(R.id.more).isEnabled());
        Assert.assertEquals(
                View.IMPORTANT_FOR_ACCESSIBILITY_NO,
                mImprovedBookmarkRow.findViewById(R.id.more).getImportantForAccessibility());
    }

    @Test
    public void testSelectionInactive() {
        mModel.set(ImprovedBookmarkRowProperties.SELECTION_ACTIVE, false);
        assertTrue(mImprovedBookmarkRow.findViewById(R.id.more).isClickable());
        assertTrue(mImprovedBookmarkRow.findViewById(R.id.more).isEnabled());
        Assert.assertEquals(
                View.IMPORTANT_FOR_ACCESSIBILITY_YES,
                mImprovedBookmarkRow.findViewById(R.id.more).getImportantForAccessibility());
    }

    @Test
    public void testListMenuButtonDelegateDoesNotChangeVisibility() {
        int visibility = mImprovedBookmarkRow.findViewById(R.id.more).getVisibility();
        mModel.set(ImprovedBookmarkRowProperties.LIST_MENU_BUTTON_DELEGATE, null);
        // Setting the delegate shouldn't affect visibility.
        Assert.assertEquals(
                visibility, mImprovedBookmarkRow.findViewById(R.id.more).getVisibility());
    }

    @Test
    public void testNotEditableButMenuVisibility() {
        mModel.set(ImprovedBookmarkRowProperties.END_IMAGE_VISIBILITY, ImageVisibility.MENU);
        mModel.set(ImprovedBookmarkRowProperties.EDITABLE, false);
        Assert.assertEquals(
                View.GONE, mImprovedBookmarkRow.findViewById(R.id.more).getVisibility());
    }

    @Test
    public void testStartImageVisibility() {
        mModel.set(ImprovedBookmarkRowProperties.START_IMAGE_VISIBILITY, ImageVisibility.DRAWABLE);
        Assert.assertEquals(
                View.VISIBLE, mImprovedBookmarkRow.findViewById(R.id.start_image).getVisibility());
        Assert.assertEquals(
                View.GONE, mImprovedBookmarkRow.findViewById(R.id.folder_view).getVisibility());

        mModel.set(
                ImprovedBookmarkRowProperties.START_IMAGE_VISIBILITY,
                ImageVisibility.FOLDER_DRAWABLE);
        Assert.assertEquals(
                View.GONE, mImprovedBookmarkRow.findViewById(R.id.start_image).getVisibility());
        Assert.assertEquals(
                View.VISIBLE, mImprovedBookmarkRow.findViewById(R.id.folder_view).getVisibility());
    }

    @Test
    public void testEndImageVisibility() {
        mModel.set(ImprovedBookmarkRowProperties.END_IMAGE_VISIBILITY, ImageVisibility.DRAWABLE);
        Assert.assertEquals(
                View.GONE, mImprovedBookmarkRow.findViewById(R.id.more).getVisibility());
        Assert.assertEquals(
                View.VISIBLE, mImprovedBookmarkRow.findViewById(R.id.end_image).getVisibility());
    }

    @Test
    public void testAccessoryViewHasParent() {
        doReturn(mViewGroup).when(mView).getParent();
        doAnswer(
                        (invocation) -> {
                            doReturn(null).when(mView).getParent();
                            return null;
                        })
                .when(mViewGroup)
                .removeView(mView);

        mModel.set(ImprovedBookmarkRowProperties.ACCESSORY_VIEW, mView);
        verify(mViewGroup).removeView(mView);
    }

    @Test
    public void testSetStartImageDrawable() {
        mImprovedBookmarkRow.setStartImageViewForTesting(mStartImageView);

        mModel.set(ImprovedBookmarkRowProperties.END_IMAGE_VISIBILITY, ImageVisibility.DRAWABLE);
        mModel.set(ImprovedBookmarkRowProperties.START_ICON_DRAWABLE, mDrawableSupplier);

        ShadowLooper.runUiThreadTasks();

        verify(mStartImageView).setImageDrawable(null);
        verify(mStartImageView).setImageDrawable(mDrawable);
        verify(mStartImageView).setAlpha(0f);
        verify(mStartImageView).animate();
        verify(mStartImageViewAnimator).alpha(1f);
        verify(mStartImageViewAnimator).setDuration(ImprovedBookmarkRow.BASE_ANIMATION_DURATION_MS);
        verify(mStartImageViewAnimator).start();
    }

    @Test
    public void testSetStartImageDrawable_nullDrawableDoesNotAnimate() {
        mImprovedBookmarkRow.setStartImageViewForTesting(mStartImageView);

        mModel.set(ImprovedBookmarkRowProperties.END_IMAGE_VISIBILITY, ImageVisibility.DRAWABLE);
        mModel.set(ImprovedBookmarkRowProperties.START_ICON_DRAWABLE, mNullDrawableSupplier);

        ShadowLooper.runUiThreadTasks();

        verify(mStartImageView, times(2)).setImageDrawable(null);
        verify(mStartImageView, never()).setAlpha(0f);
        verify(mStartImageView, never()).animate();
        verify(mStartImageViewAnimator, never()).alpha(1f);
        verify(mStartImageViewAnimator, never())
                .setDuration(ImprovedBookmarkRow.BASE_ANIMATION_DURATION_MS);
        verify(mStartImageViewAnimator, never()).start();
    }

    @Test
    public void testCancelAnimation() {
        // This is tricky because the supplier introduces a post by default. But if we wait, we risk
        // letting the animation finish. So use a mock/captor to make it synchronous.
        mModel.set(ImprovedBookmarkRowProperties.START_ICON_DRAWABLE, mMockDrawableSupplier);
        verify(mMockDrawableSupplier).onAvailable(mDrawableCallbackCaptor.capture());
        verify(mMockDrawableSupplier).get();
        mDrawableCallbackCaptor.getValue().onResult(mDrawable);
        assertTrue(mImprovedBookmarkRow.hasTransientState());

        mImprovedBookmarkRow.cancelAnimation();
        assertFalse(mImprovedBookmarkRow.hasTransientState());
    }

    @Test
    public void testClick() {
        mImprovedBookmarkRow.performClick();
        verify(mOpenBookmarkCallback).run();
    }

    @Test
    public void testLocalAndRemoteBookmarks() {
        View localBookmarkImageView = mImprovedBookmarkRow.findViewById(R.id.local_bookmark_image);
        mModel.set(ImprovedBookmarkRowProperties.IS_LOCAL_BOOKMARK, true);
        assertEquals(View.VISIBLE, localBookmarkImageView.getVisibility());

        mModel.set(ImprovedBookmarkRowProperties.IS_LOCAL_BOOKMARK, false);
        assertEquals(View.GONE, localBookmarkImageView.getVisibility());
    }
}