chromium/chrome/browser/commerce/price_tracking/android/javatests/src/org/chromium/chrome/browser/price_tracking/PriceTrackingButtonControllerUnitTest.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.price_tracking;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.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.when;

import android.app.Activity;
import android.content.res.Resources;
import android.os.Looper;
import android.view.View;

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.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;

import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.bookmarks.BookmarkModel;
import org.chromium.chrome.browser.bookmarks.TabBookmarker;
import org.chromium.chrome.browser.commerce.PriceTrackingUtils;
import org.chromium.chrome.browser.commerce.PriceTrackingUtilsJni;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.toolbar.ButtonData;
import org.chromium.chrome.browser.toolbar.ButtonData.ButtonSpec;
import org.chromium.chrome.browser.toolbar.ButtonDataProvider;
import org.chromium.chrome.browser.toolbar.ButtonDataProvider.ButtonDataObserver;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.bookmarks.BookmarkType;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.SheetState;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetObserver;
import org.chromium.ui.modaldialog.ModalDialogManager;

/** Unit test for {@link PriceTrackingButtonController}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@EnableFeatures({ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2})
public class PriceTrackingButtonControllerUnitTest {
    @Rule public JniMocker mJniMocker = new JniMocker();

    private Activity mActivity;
    private ObservableSupplierImpl<Profile> mProfileSupplier;
    private ObservableSupplier<BookmarkModel> mBookmarkModelSupplier;
    private ObservableSupplierImpl<Tab> mTabSupplier;
    private ObservableSupplierImpl<Boolean> mPriceTrackingStateSupplier;
    @Mock private Tab mMockTab;
    @Mock private Supplier<TabBookmarker> mMockTabBookmarkerSupplier;
    @Mock private TabBookmarker mMockTabBookmarker;
    @Mock private BottomSheetController mMockBottomSheetController;
    @Mock private ModalDialogManager mMockModalDialogManager;
    @Mock private SnackbarManager mMockSnackbarManager;
    @Mock private Profile mMockProfile;
    @Mock private BookmarkModel mMockBookmarkModel;
    @Mock PriceTrackingUtils.Natives mMockPriceTrackingUtilsJni;
    @Captor private ArgumentCaptor<BottomSheetObserver> mBottomSheetObserverCaptor;

    @Before
    public void setUp() {
        mActivity = Robolectric.setupActivity(Activity.class);
        mActivity.setTheme(R.style.Theme_BrowserUI_DayNight);

        MockitoAnnotations.initMocks(this);

        mJniMocker.mock(PriceTrackingUtilsJni.TEST_HOOKS, mMockPriceTrackingUtilsJni);
        mPriceTrackingStateSupplier = new ObservableSupplierImpl<>(false);
        mProfileSupplier = new ObservableSupplierImpl<>(mMockProfile);
        mBookmarkModelSupplier = new ObservableSupplierImpl<>(mMockBookmarkModel);
        mTabSupplier = new ObservableSupplierImpl<>(mMockTab);
        when(mMockTab.getContext()).thenReturn(mActivity);
        when(mMockTabBookmarkerSupplier.get()).thenReturn(mMockTabBookmarker);
    }

    private PriceTrackingButtonController createButtonController() {
        return new PriceTrackingButtonController(
                mActivity,
                mTabSupplier,
                mMockModalDialogManager,
                mMockBottomSheetController,
                mMockSnackbarManager,
                mMockTabBookmarkerSupplier,
                mProfileSupplier,
                mBookmarkModelSupplier,
                mPriceTrackingStateSupplier);
    }

    @Test
    public void testButtonShouldUpdateOnPriceTrackingChange() {
        ButtonDataObserver mockButtonObserver = mock(ButtonDataObserver.class);
        PriceTrackingButtonController priceTrackingButtonController = createButtonController();
        priceTrackingButtonController.addObserver(mockButtonObserver);

        ButtonSpec originalButtonSpec = priceTrackingButtonController.get(mMockTab).getButtonSpec();

        mPriceTrackingStateSupplier.set(true);
        verify(mockButtonObserver).buttonDataChanged(true);
        Shadows.shadowOf(Looper.getMainLooper()).idle();

        ButtonSpec updatedButtonSpec = priceTrackingButtonController.get(mMockTab).getButtonSpec();

        Assert.assertNotEquals(
                originalButtonSpec.getContentDescription(),
                updatedButtonSpec.getContentDescription());
        Assert.assertNotEquals(originalButtonSpec.getDrawable(), updatedButtonSpec.getDrawable());
    }

    @Test
    public void testPriceTrackingButtonClick() {
        PriceTrackingButtonController priceTrackingButtonController = createButtonController();
        ButtonData buttonData = priceTrackingButtonController.get(mMockTab);

        buttonData.getButtonSpec().getOnClickListener().onClick(null);

        verify(mMockTabBookmarker).startOrModifyPriceTracking(mMockTab);
    }

    @Test
    public void testPriceTrackingButtonClick_shouldRemoveTrackingWhenAlreadyTracking() {
        View mockView = Mockito.mock(View.class);
        Resources mockResources = Mockito.mock(Resources.class);
        when(mockView.getResources()).thenReturn(mockResources);

        BookmarkId bookmarkId = new BookmarkId(1234, BookmarkType.NORMAL);
        when(mMockBookmarkModel.getUserBookmarkIdForTab(mMockTab)).thenReturn(bookmarkId);
        ArgumentCaptor<Callback<Boolean>> jniCallbackArgumentCaptor =
                ArgumentCaptor.forClass(Callback.class);

        PriceTrackingButtonController priceTrackingButtonController = createButtonController();
        mPriceTrackingStateSupplier.set(true);

        ButtonData buttonData = priceTrackingButtonController.get(mMockTab);

        buttonData.getButtonSpec().getOnClickListener().onClick(mockView);

        verify(mMockTabBookmarker, never()).startOrModifyPriceTracking(mMockTab);
        verify(mMockPriceTrackingUtilsJni)
                .setPriceTrackingStateForBookmark(
                        eq(mMockProfile),
                        eq(bookmarkId.getId()),
                        eq(false),
                        jniCallbackArgumentCaptor.capture(),
                        eq(false));

        jniCallbackArgumentCaptor.getValue().onResult(true);

        verify(mMockSnackbarManager).showSnackbar(any());
    }

    @Test
    public void testPriceTrackingButton_IsDisabledWhenBottomSheetAppears() {
        PriceTrackingButtonController priceTrackingButtonController = createButtonController();
        ButtonDataProvider.ButtonDataObserver buttonDataObserver =
                Mockito.mock(ButtonDataProvider.ButtonDataObserver.class);
        priceTrackingButtonController.addObserver(buttonDataObserver);
        ButtonData buttonData = priceTrackingButtonController.get(mMockTab);

        // The controller should have registered an observer to listen to bottom sheet events.
        verify(mMockBottomSheetController).addObserver(mBottomSheetObserverCaptor.capture());

        mBottomSheetObserverCaptor
                .getValue()
                .onSheetStateChanged(SheetState.FULL, StateChangeReason.NONE);

        Assert.assertFalse(buttonData.isEnabled());
        verify(buttonDataObserver).buttonDataChanged(true);
    }

    @Test
    public void testPriceTrackingButton_IsReenabledWhenBottomSheetDismissed() {
        PriceTrackingButtonController priceTrackingButtonController = createButtonController();

        ButtonDataProvider.ButtonDataObserver buttonDataObserver =
                Mockito.mock(ButtonDataProvider.ButtonDataObserver.class);
        priceTrackingButtonController.addObserver(buttonDataObserver);
        ButtonData buttonData = priceTrackingButtonController.get(mMockTab);

        // The controller should have registered an observer to listen to bottom sheet events.
        verify(mMockBottomSheetController).addObserver(mBottomSheetObserverCaptor.capture());

        // Show bottom sheet to disable button.
        mBottomSheetObserverCaptor
                .getValue()
                .onSheetStateChanged(SheetState.FULL, StateChangeReason.NONE);

        // Close the bottom sheet, button should be enabled again.
        mBottomSheetObserverCaptor
                .getValue()
                .onSheetStateChanged(SheetState.HIDDEN, StateChangeReason.NONE);

        // After the bottom sheet is closed the button should be enabled.
        Assert.assertTrue(buttonData.isEnabled());
        // We should have notified of changes twice (when disabled and when enabled again).
        verify(buttonDataObserver, times(2)).buttonDataChanged(true);
    }
}