// Copyright 2021 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.adaptive;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
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 static org.chromium.chrome.browser.preferences.ChromePreferenceKeys.ADAPTIVE_TOOLBAR_CUSTOMIZATION_SETTINGS;
import android.app.Activity;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.Pair;
import android.view.View;
import android.view.View.OnLongClickListener;
import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionUtil;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.settings.SettingsLauncherFactory;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.toolbar.ButtonData.ButtonSpec;
import org.chromium.chrome.browser.toolbar.ButtonDataImpl;
import org.chromium.chrome.browser.toolbar.ButtonDataProvider;
import org.chromium.chrome.browser.toolbar.ButtonDataProvider.ButtonDataObserver;
import org.chromium.chrome.browser.toolbar.R;
import org.chromium.chrome.browser.toolbar.adaptive.settings.AdaptiveToolbarSettingsFragment;
import org.chromium.components.browser_ui.settings.SettingsLauncher;
import org.chromium.ui.permissions.AndroidPermissionDelegate;
import java.util.List;
/** Unit tests for the {@link AdaptiveToolbarButtonController} */
@Config(manifest = Config.NONE)
@RunWith(BaseRobolectricTestRunner.class)
@EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2)
public class AdaptiveToolbarButtonControllerTest {
@Mock private AndroidPermissionDelegate mAndroidPermissionDelegate;
@Mock private ButtonDataProvider mShareButtonController;
@Mock private ButtonDataProvider mVoiceToolbarButtonController;
@Mock private ButtonDataProvider mNewTabButtonController;
@Mock private ButtonDataProvider mPriceTrackingButtonController;
@Mock private SettingsLauncher mSettingsLauncher;
@Mock private ActivityLifecycleDispatcher mActivityLifecycleDispatcher;
@Mock private Profile mProfile;
@Mock private Tab mTab;
@Mock private Configuration mConfiguration;
private ButtonDataImpl mButtonData;
private ObservableSupplierImpl<Profile> mProfileSupplier;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
VoiceRecognitionUtil.setIsVoiceSearchEnabledForTesting(true);
AdaptiveToolbarFeatures.clearParsedParamsForTesting();
SettingsLauncherFactory.setInstanceForTesting(mSettingsLauncher);
mButtonData =
new ButtonDataImpl(
/* canShow= */ true,
/* drawable= */ null,
mock(View.OnClickListener.class),
/* contentDescription= */ "",
/* supportsTinting= */ false,
/* iphCommandBuilder= */ null,
/* isEnabled= */ true,
AdaptiveToolbarButtonVariant.UNKNOWN,
/* tooltipTextResId= */ Resources.ID_NULL,
/* showHoverHighlight= */ false);
mConfiguration.screenWidthDp = 420;
doReturn(mProfile).when(mProfile).getOriginalProfile();
mProfileSupplier = new ObservableSupplierImpl<>();
}
@After
public void tearDown() {
ChromeSharedPreferences.getInstance()
.removeKey(ChromePreferenceKeys.ADAPTIVE_TOOLBAR_CUSTOMIZATION_ENABLED);
ChromeSharedPreferences.getInstance().removeKey(ADAPTIVE_TOOLBAR_CUSTOMIZATION_SETTINGS);
}
@Test
@SmallTest
@EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2)
public void testCustomization_newTab() {
AdaptiveToolbarPrefs.saveToolbarSettingsToggleState(true);
AdaptiveToolbarStatePredictor.setSegmentationResultsForTesting(
new Pair<>(true, List.of(AdaptiveToolbarButtonVariant.NEW_TAB)));
AdaptiveToolbarButtonController adaptiveToolbarButtonController = buildController();
verify(mActivityLifecycleDispatcher).register(adaptiveToolbarButtonController);
ButtonDataObserver observer = mock(ButtonDataObserver.class);
adaptiveToolbarButtonController.addObserver(observer);
mProfileSupplier.set(mProfile);
verify(observer).buttonDataChanged(true);
Assert.assertEquals(
mNewTabButtonController,
adaptiveToolbarButtonController.getSingleProviderForTesting());
}
@Test
@SmallTest
@EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2)
public void testCustomization_share() {
AdaptiveToolbarPrefs.saveToolbarSettingsToggleState(true);
AdaptiveToolbarStatePredictor.setSegmentationResultsForTesting(
new Pair<>(true, List.of(AdaptiveToolbarButtonVariant.SHARE)));
AdaptiveToolbarButtonController adaptiveToolbarButtonController = buildController();
verify(mActivityLifecycleDispatcher).register(adaptiveToolbarButtonController);
ButtonDataObserver observer = mock(ButtonDataObserver.class);
adaptiveToolbarButtonController.addObserver(observer);
mProfileSupplier.set(mProfile);
verify(observer).buttonDataChanged(true);
Assert.assertEquals(
mShareButtonController,
adaptiveToolbarButtonController.getSingleProviderForTesting());
}
@Test
@SmallTest
@EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2)
public void testCustomization_voice() {
AdaptiveToolbarPrefs.saveToolbarSettingsToggleState(true);
AdaptiveToolbarStatePredictor.setSegmentationResultsForTesting(
new Pair<>(true, List.of(AdaptiveToolbarButtonVariant.VOICE)));
AdaptiveToolbarButtonController adaptiveToolbarButtonController = buildController();
verify(mActivityLifecycleDispatcher).register(adaptiveToolbarButtonController);
ButtonDataObserver observer = mock(ButtonDataObserver.class);
adaptiveToolbarButtonController.addObserver(observer);
mProfileSupplier.set(mProfile);
verify(observer).buttonDataChanged(true);
Assert.assertEquals(
mVoiceToolbarButtonController,
adaptiveToolbarButtonController.getSingleProviderForTesting());
}
@Test
@SmallTest
@EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2)
public void testCustomization_prefChangeTriggersButtonChange() {
AdaptiveToolbarPrefs.saveToolbarSettingsToggleState(true);
AdaptiveToolbarStatePredictor.setSegmentationResultsForTesting(
new Pair<>(true, List.of(AdaptiveToolbarButtonVariant.VOICE)));
AdaptiveToolbarButtonController adaptiveToolbarButtonController = buildController();
verify(mActivityLifecycleDispatcher).register(adaptiveToolbarButtonController);
ButtonDataObserver observer = mock(ButtonDataObserver.class);
adaptiveToolbarButtonController.addObserver(observer);
mProfileSupplier.set(mProfile);
verify(observer).buttonDataChanged(true);
Assert.assertEquals(
mVoiceToolbarButtonController,
adaptiveToolbarButtonController.getSingleProviderForTesting());
ChromeSharedPreferences.getInstance()
.writeInt(
ADAPTIVE_TOOLBAR_CUSTOMIZATION_SETTINGS,
AdaptiveToolbarButtonVariant.NEW_TAB);
verify(observer, times(2)).buttonDataChanged(true);
Assert.assertEquals(
mNewTabButtonController,
adaptiveToolbarButtonController.getSingleProviderForTesting());
}
@Test
@SmallTest
@EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2)
public void testLongPress() {
AdaptiveToolbarPrefs.saveToolbarSettingsToggleState(true);
AdaptiveToolbarStatePredictor.setSegmentationResultsForTesting(
new Pair<>(true, List.of(AdaptiveToolbarButtonVariant.NEW_TAB)));
Activity activity = Robolectric.setupActivity(Activity.class);
AdaptiveButtonActionMenuCoordinator menuCoordinator =
mock(AdaptiveButtonActionMenuCoordinator.class);
Answer<OnLongClickListener> listenerAnswer =
invocation ->
(view -> {
invocation
.<Callback<Integer>>getArgument(0)
.onResult(
Integer.valueOf(
R.id.customize_adaptive_button_menu_id));
return true;
});
doAnswer(listenerAnswer).when(menuCoordinator).createOnLongClickListener(any());
AdaptiveToolbarButtonController adaptiveToolbarButtonController =
new AdaptiveToolbarButtonController(
activity,
mActivityLifecycleDispatcher,
mProfileSupplier,
menuCoordinator,
mAndroidPermissionDelegate,
ChromeSharedPreferences.getInstance());
adaptiveToolbarButtonController.addButtonVariant(
AdaptiveToolbarButtonVariant.NEW_TAB, mNewTabButtonController);
mProfileSupplier.set(mProfile);
mButtonData.setCanShow(true);
mButtonData.setEnabled(true);
mButtonData.setButtonSpec(makeButtonSpec(AdaptiveToolbarButtonVariant.NEW_TAB));
when(mNewTabButtonController.get(any())).thenReturn(mButtonData);
View view = mock(View.class);
when(view.getContext()).thenReturn(activity);
View.OnLongClickListener longClickListener =
adaptiveToolbarButtonController.get(mTab).getButtonSpec().getOnLongClickListener();
longClickListener.onLongClick(view);
adaptiveToolbarButtonController.destroy();
verify(mSettingsLauncher)
.launchSettingsActivity(activity, AdaptiveToolbarSettingsFragment.class);
}
@Test
@SmallTest
@EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2)
public void testShowDynamicAction() {
Activity activity = Robolectric.setupActivity(Activity.class);
AdaptiveToolbarStatePredictor.setSegmentationResultsForTesting(
new Pair<>(true, List.of(AdaptiveToolbarButtonVariant.NEW_TAB)));
AdaptiveButtonActionMenuCoordinator menuCoordinator =
mock(AdaptiveButtonActionMenuCoordinator.class);
doReturn(
new OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
Assert.fail("This long click listener shouldn't be invoked.");
return false;
}
})
.when(menuCoordinator)
.createOnLongClickListener(any());
AdaptiveToolbarButtonController adaptiveToolbarButtonController =
new AdaptiveToolbarButtonController(
activity,
mActivityLifecycleDispatcher,
mProfileSupplier,
menuCoordinator,
mAndroidPermissionDelegate,
ChromeSharedPreferences.getInstance());
adaptiveToolbarButtonController.addButtonVariant(
AdaptiveToolbarButtonVariant.PRICE_TRACKING, mPriceTrackingButtonController);
ButtonDataObserver observer = mock(ButtonDataObserver.class);
adaptiveToolbarButtonController.addObserver(observer);
mProfileSupplier.set(mProfile);
mButtonData.setCanShow(true);
mButtonData.setEnabled(true);
mButtonData.setButtonSpec(makeButtonSpec(AdaptiveToolbarButtonVariant.PRICE_TRACKING));
when(mPriceTrackingButtonController.get(any())).thenReturn(mButtonData);
View view = mock(View.class);
when(view.getContext()).thenReturn(activity);
adaptiveToolbarButtonController.showDynamicAction(
AdaptiveToolbarButtonVariant.PRICE_TRACKING);
// Button data should have change twice, first on native initialization and then after
// showing the dynamic action.
verify(observer, times(2)).buttonDataChanged(true);
Assert.assertEquals(
mPriceTrackingButtonController,
adaptiveToolbarButtonController.getSingleProviderForTesting());
ButtonSpec buttonSpec = adaptiveToolbarButtonController.get(mTab).getButtonSpec();
Assert.assertEquals(
AdaptiveToolbarButtonVariant.PRICE_TRACKING, buttonSpec.getButtonVariant());
Assert.assertTrue(buttonSpec.isDynamicAction());
// Dynamic actions should have no long click handlers.
Assert.assertNull(buttonSpec.getOnLongClickListener());
adaptiveToolbarButtonController.destroy();
}
@Test
@SmallTest
@EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2)
public void testButtonOnLargeScreens() {
// Screen is wide enough to fit the button, it should appear.
mConfiguration.screenWidthDp = 450;
mButtonData.setCanShow(true);
mButtonData.setEnabled(true);
when(mVoiceToolbarButtonController.get(any())).thenReturn(mButtonData);
AdaptiveToolbarPrefs.saveToolbarSettingsToggleState(true);
AdaptiveToolbarStatePredictor.setSegmentationResultsForTesting(
new Pair<>(true, List.of(AdaptiveToolbarButtonVariant.VOICE)));
AdaptiveToolbarButtonController adaptiveToolbarButtonController = buildController();
ButtonDataObserver observer = mock(ButtonDataObserver.class);
adaptiveToolbarButtonController.addObserver(observer);
mProfileSupplier.set(mProfile);
verify(observer).buttonDataChanged(true);
Assert.assertEquals(
mVoiceToolbarButtonController,
adaptiveToolbarButtonController.getSingleProviderForTesting());
Assert.assertTrue(adaptiveToolbarButtonController.get(mTab).canShow());
}
@Test
@SmallTest
@EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2)
public void testButtonNotShownOnSmallScreens() {
// Screen too narrow, button shouldn't appear.
mConfiguration.screenWidthDp = 320;
mButtonData.setCanShow(true);
mButtonData.setEnabled(true);
when(mVoiceToolbarButtonController.get(any())).thenReturn(mButtonData);
AdaptiveToolbarPrefs.saveToolbarSettingsToggleState(true);
AdaptiveToolbarStatePredictor.setSegmentationResultsForTesting(
new Pair<>(true, List.of(AdaptiveToolbarButtonVariant.VOICE)));
AdaptiveToolbarButtonController adaptiveToolbarButtonController = buildController();
ButtonDataObserver observer = mock(ButtonDataObserver.class);
adaptiveToolbarButtonController.addObserver(observer);
mProfileSupplier.set(mProfile);
verify(observer).buttonDataChanged(true);
Assert.assertEquals(
mVoiceToolbarButtonController,
adaptiveToolbarButtonController.getSingleProviderForTesting());
Assert.assertFalse(adaptiveToolbarButtonController.get(mTab).canShow());
}
@Test
@SmallTest
@EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2)
public void testButtonVisibilityChangeOnConfigurationChange() {
// Screen too narrow, button shouldn't appear.
mConfiguration.screenWidthDp = 320;
mButtonData.setCanShow(true);
mButtonData.setEnabled(true);
when(mVoiceToolbarButtonController.get(any())).thenReturn(mButtonData);
doReturn(true).when(mActivityLifecycleDispatcher).isNativeInitializationFinished();
AdaptiveToolbarPrefs.saveToolbarSettingsToggleState(true);
AdaptiveToolbarStatePredictor.setSegmentationResultsForTesting(
new Pair<>(true, List.of(AdaptiveToolbarButtonVariant.VOICE)));
AdaptiveToolbarButtonController adaptiveToolbarButtonController = buildController();
ButtonDataObserver observer = mock(ButtonDataObserver.class);
adaptiveToolbarButtonController.addObserver(observer);
mProfileSupplier.set(mProfile);
verify(observer).buttonDataChanged(true);
Assert.assertEquals(
mVoiceToolbarButtonController,
adaptiveToolbarButtonController.getSingleProviderForTesting());
Assert.assertFalse(adaptiveToolbarButtonController.get(mTab).canShow());
// New screen configuration is wider, button should be visible.
mConfiguration.screenWidthDp = 450;
adaptiveToolbarButtonController.onConfigurationChanged(mConfiguration);
verify(observer).buttonDataChanged(false);
Assert.assertTrue(adaptiveToolbarButtonController.get(mTab).canShow());
}
@Test
@SmallTest
@EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2)
public void testConfigurationChangeIgnoredWhenNativeNotReady() {
AdaptiveToolbarPrefs.saveToolbarSettingsToggleState(true);
AdaptiveToolbarStatePredictor.setSegmentationResultsForTesting(
new Pair<>(true, List.of(AdaptiveToolbarButtonVariant.VOICE)));
// If native is not done initializing then ignore all configuration changes.
doReturn(false).when(mActivityLifecycleDispatcher).isNativeInitializationFinished();
AdaptiveToolbarButtonController adaptiveToolbarButtonController = buildController();
ButtonDataObserver observer = mock(ButtonDataObserver.class);
adaptiveToolbarButtonController.addObserver(observer);
// Change configuration, button shouldn't update.
mConfiguration.screenWidthDp = 320;
adaptiveToolbarButtonController.onConfigurationChanged(mConfiguration);
verify(observer, never()).buttonDataChanged(true);
}
private AdaptiveToolbarButtonController buildController() {
Activity mockActivity = mock(Activity.class);
Resources mockResources = mock(Resources.class);
doReturn(mockResources).when(mockActivity).getResources();
doReturn(mConfiguration).when(mockResources).getConfiguration();
AdaptiveToolbarButtonController adaptiveToolbarButtonController =
new AdaptiveToolbarButtonController(
mockActivity,
mActivityLifecycleDispatcher,
mProfileSupplier,
mock(AdaptiveButtonActionMenuCoordinator.class),
mAndroidPermissionDelegate,
ChromeSharedPreferences.getInstance());
adaptiveToolbarButtonController.addButtonVariant(
AdaptiveToolbarButtonVariant.NEW_TAB, mNewTabButtonController);
adaptiveToolbarButtonController.addButtonVariant(
AdaptiveToolbarButtonVariant.SHARE, mShareButtonController);
adaptiveToolbarButtonController.addButtonVariant(
AdaptiveToolbarButtonVariant.VOICE, mVoiceToolbarButtonController);
return adaptiveToolbarButtonController;
}
private static ButtonSpec makeButtonSpec(@AdaptiveToolbarButtonVariant int variant) {
return new ButtonSpec(
/* drawable= */ null,
mock(View.OnClickListener.class),
/* onLongClickListener= */ null,
/* contentDescription= */ "description",
/* supportsTinting= */ false,
/* iphCommandBuilder= */ null,
variant,
/* actionChipLabelResId= */ 0,
/* tooltipTextResId= */ Resources.ID_NULL,
/* showHoverHighlight= */ false);
}
}