// Copyright 2017 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.searchwidget;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Instrumentation;
import android.app.Instrumentation.ActivityMonitor;
import android.app.PendingIntent;
import android.view.KeyEvent;
import android.view.View;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.CriteriaNotSatisfiedException;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.JniMocker;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.WarmupManager;
import org.chromium.chrome.browser.app.metrics.LaunchCauseMetrics;
import org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.locale.LocaleManagerDelegate;
import org.chromium.chrome.browser.omnibox.LocationBarCoordinator;
import org.chromium.chrome.browser.omnibox.UrlBar;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteController;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteControllerJni;
import org.chromium.chrome.browser.omnibox.suggestions.CachedZeroSuggestionsManager;
import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionHandler;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.search_engines.SearchEnginePromoType;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.searchwidget.SearchActivity.SearchActivityDelegate;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityExtras.IntentOrigin;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityExtras.SearchType;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.ActivityTestUtils;
import org.chromium.chrome.test.util.OmniboxTestUtils;
import org.chromium.components.metrics.OmniboxEventProtos.OmniboxEventProto.PageClassification;
import org.chromium.components.omnibox.AutocompleteMatch;
import org.chromium.components.omnibox.AutocompleteMatchBuilder;
import org.chromium.components.omnibox.AutocompleteResult;
import org.chromium.components.omnibox.OmniboxSuggestionType;
import org.chromium.components.search_engines.TemplateUrl;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.ui.test.util.DeviceRestriction;
import org.chromium.url.GURL;
import java.util.List;
import java.util.concurrent.Callable;
/**
* Tests the {@link SearchActivity}.
*
* <p>TODO(dfalcantara): Add tests for: + Performing a search query.
*
* <p>+ Performing a search query while the SearchActivity is alive and the default search engine is
* changed outside the SearchActivity.
*
* <p>+ Add microphone tests somehow (vague query + confident query).
*/
@Restriction({DeviceRestriction.RESTRICTION_TYPE_NON_AUTO}) // Search widget not supported on auto.
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@DoNotBatch(reason = "Test start up behaviors.")
public class SearchActivityTest {
private static class TestDelegate extends SearchActivityDelegate {
public final CallbackHelper shouldDelayNativeInitializationCallback = new CallbackHelper();
public final CallbackHelper showSearchEngineDialogIfNeededCallback = new CallbackHelper();
public final CallbackHelper onFinishDeferredInitializationCallback = new CallbackHelper();
public final CallbackHelper onPromoDialogShownCallback = new CallbackHelper();
public boolean shouldDelayLoadingNative;
public boolean shouldDelayDeferredInitialization;
public boolean shouldShowRealSearchDialog;
public Callback<Boolean> onSearchEngineFinalizedCallback;
@Override
boolean shouldDelayNativeInitialization() {
shouldDelayNativeInitializationCallback.notifyCalled();
return shouldDelayLoadingNative;
}
@Override
void showSearchEngineDialogIfNeeded(
Activity activity, Callback<Boolean> onSearchEngineFinalized) {
onSearchEngineFinalizedCallback = onSearchEngineFinalized;
showSearchEngineDialogIfNeededCallback.notifyCalled();
if (shouldShowRealSearchDialog) {
ThreadUtils.runOnUiThreadBlocking(
() -> {
LocaleManager.getInstance()
.setDelegateForTest(
new LocaleManagerDelegate() {
@Override
public int getSearchEnginePromoShowType() {
return SearchEnginePromoType.SHOW_EXISTING;
}
@Override
public List<TemplateUrl>
getSearchEnginesForPromoDialog(
int promoType) {
return TemplateUrlServiceFactory.getForProfile(
ProfileManager
.getLastUsedRegularProfile())
.getTemplateUrls();
}
});
});
super.showSearchEngineDialogIfNeeded(activity, onSearchEngineFinalized);
} else {
LocaleManager.getInstance()
.setDelegateForTest(
new LocaleManagerDelegate() {
@Override
public boolean needToCheckForSearchEnginePromo() {
return false;
}
});
if (!shouldDelayDeferredInitialization) onSearchEngineFinalized.onResult(true);
}
}
@Override
public void onFinishDeferredInitialization() {
onFinishDeferredInitializationCallback.notifyCalled();
}
}
public @Rule ChromeTabbedActivityTestRule mActivityTestRule =
new ChromeTabbedActivityTestRule();
// Needed for CT connection cleanup.
public @Rule CustomTabActivityTestRule mCustomTabActivityTestRule =
new CustomTabActivityTestRule();
public @Rule JniMocker mJniMocker = new JniMocker();
public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule();
private @Mock AutocompleteController.Natives mAutocompleteControllerJniMock;
private @Mock AutocompleteController mAutocompleteController;
private @Mock VoiceRecognitionHandler mHandler;
private TestDelegate mTestDelegate;
private OmniboxTestUtils mOmnibox;
private AutocompleteController.OnSuggestionsReceivedListener mOnSuggestionsReceivedListener;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
doReturn(true).when(mHandler).isVoiceSearchEnabled();
mJniMocker.mock(AutocompleteControllerJni.TEST_HOOKS, mAutocompleteControllerJniMock);
doReturn(mAutocompleteController).when(mAutocompleteControllerJniMock).getForProfile(any());
doAnswer(
inv ->
mOnSuggestionsReceivedListener =
(AutocompleteController.OnSuggestionsReceivedListener)
inv.getArguments()[0])
.when(mAutocompleteController)
.addOnSuggestionsReceivedListener(any());
doReturn(buildDummyAutocompleteMatch(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL))
.when(mAutocompleteController)
.classify(any());
mTestDelegate = new TestDelegate();
SearchActivity.setDelegateForTests(mTestDelegate);
}
private AutocompleteMatch buildDummyAutocompleteMatch(String url) {
return AutocompleteMatchBuilder.searchWithType(OmniboxSuggestionType.SEARCH_SUGGEST)
.setDisplayText(url)
.setDescription(url)
.setUrl(new GURL(url))
.build();
}
private AutocompleteResult buildDummyAutocompleteResult() {
return AutocompleteResult.fromCache(
List.of(
buildDummyAutocompleteMatch("https://www.google.com"),
buildDummyAutocompleteMatch("https://android.com")),
null);
}
@Test
@SmallTest
public void testOmniboxSuggestionContainerAppears() throws Exception {
startSearchActivity();
// Wait for the Activity to fully load.
mTestDelegate.shouldDelayNativeInitializationCallback.waitForCallback(0);
mTestDelegate.showSearchEngineDialogIfNeededCallback.waitForCallback(0);
mTestDelegate.onFinishDeferredInitializationCallback.waitForCallback(0);
// Type in anything. It should force the suggestions to appear.
mOmnibox.requestFocus();
verify(mAutocompleteController, times(1))
.startZeroSuggest(
eq(""),
any(/* DSE URL*/ ),
eq(PageClassification.ANDROID_SEARCH_WIDGET_VALUE),
eq(""));
ThreadUtils.runOnUiThreadBlocking(
() ->
mOnSuggestionsReceivedListener.onSuggestionsReceived(
buildDummyAutocompleteResult(), true));
mOmnibox.checkSuggestionsShown();
}
@Test
@SmallTest
public void testStartsBrowserAfterUrlSubmitted_aboutblank() throws Exception {
verifyUrlLoads(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
}
@Test
@SmallTest
public void testStartsBrowserAfterUrlSubmitted_chromeUrl() throws Exception {
doReturn(buildDummyAutocompleteMatch("chrome://flags/"))
.when(mAutocompleteController)
.classify(any());
verifyUrlLoads("chrome://flags/");
}
private void verifyUrlLoads(final String url) throws Exception {
startSearchActivity();
// Wait for the Activity to fully load.
mTestDelegate.shouldDelayNativeInitializationCallback.waitForCallback(0);
mTestDelegate.showSearchEngineDialogIfNeededCallback.waitForCallback(0);
mTestDelegate.onFinishDeferredInitializationCallback.waitForCallback(0);
// Monitor for ChromeTabbedActivity.
waitForChromeTabbedActivityToStart(
() -> {
mOmnibox.requestFocus();
mOmnibox.typeText(url, true);
return null;
},
url);
Assert.assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
LaunchCauseMetrics.LAUNCH_CAUSE_HISTOGRAM,
LaunchCauseMetrics.LaunchCause.HOME_SCREEN_WIDGET));
}
@Test
@SmallTest
public void testVoiceSearchBeforeNativeIsLoaded() throws Exception {
// Wait for the activity to load, but don't let it load the native library.
mTestDelegate.shouldDelayLoadingNative = true;
final SearchActivity searchActivity = startSearchActivity(0, /* isVoiceSearch= */ true);
final SearchActivityLocationBarLayout locationBar =
searchActivity.findViewById(R.id.search_location_bar);
LocationBarCoordinator locationBarCoordinator =
searchActivity.getLocationBarCoordinatorForTesting();
locationBarCoordinator.setVoiceRecognitionHandlerForTesting(mHandler);
locationBar.beginQuery(
IntentOrigin.SEARCH_WIDGET, SearchType.VOICE, /* optionalText= */ null, null);
verify(mHandler, times(0))
.startVoiceRecognition(
VoiceRecognitionHandler.VoiceInteractionSource.SEARCH_WIDGET);
mTestDelegate.shouldDelayNativeInitializationCallback.waitForCallback(0);
Assert.assertEquals(0, mTestDelegate.showSearchEngineDialogIfNeededCallback.getCallCount());
Assert.assertEquals(0, mTestDelegate.onFinishDeferredInitializationCallback.getCallCount());
// Start loading native, then let the activity finish initialization.
ThreadUtils.runOnUiThreadBlocking(
() -> searchActivity.startDelayedNativeInitializationForTests());
Assert.assertEquals(
1, mTestDelegate.shouldDelayNativeInitializationCallback.getCallCount());
mTestDelegate.showSearchEngineDialogIfNeededCallback.waitForCallback(0);
mTestDelegate.onFinishDeferredInitializationCallback.waitForCallback(0);
verify(mHandler)
.startVoiceRecognition(
VoiceRecognitionHandler.VoiceInteractionSource.SEARCH_WIDGET);
CriteriaHelper.pollUiThread(
() -> {
return WarmupManager.getInstance().hasSpareWebContents()
|| WarmupManager.getInstance()
.hasSpareTab(ProfileManager.getLastUsedRegularProfile());
});
}
@Test
@SmallTest
public void testTypeBeforeNativeIsLoaded() throws Exception {
// Wait for the activity to load, but don't let it load the native library.
mTestDelegate.shouldDelayLoadingNative = true;
final SearchActivity searchActivity = startSearchActivity();
mTestDelegate.shouldDelayNativeInitializationCallback.waitForCallback(0);
Assert.assertEquals(0, mTestDelegate.showSearchEngineDialogIfNeededCallback.getCallCount());
Assert.assertEquals(0, mTestDelegate.onFinishDeferredInitializationCallback.getCallCount());
// Set some text in the search box (but don't hit enter).
mOmnibox.requestFocus();
mOmnibox.typeText(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL, false);
verifyNoMoreInteractions(mAutocompleteController);
// Start loading native, then let the activity finish initialization.
ThreadUtils.runOnUiThreadBlocking(
() -> searchActivity.startDelayedNativeInitializationForTests());
verifyNoMoreInteractions(mAutocompleteController);
Assert.assertEquals(
1, mTestDelegate.shouldDelayNativeInitializationCallback.getCallCount());
mTestDelegate.showSearchEngineDialogIfNeededCallback.waitForCallback(0);
mTestDelegate.onFinishDeferredInitializationCallback.waitForCallback(0);
// Suggestions requests are always delayed. Rather than check for the request itself
// confirm that any prior requests have been canceled.
verify(mAutocompleteController, times(1)).resetSession();
waitForChromeTabbedActivityToStart(
() -> {
mOmnibox.sendKey(KeyEvent.KEYCODE_ENTER);
return null;
},
ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
}
@Test
@SmallTest
public void testEnterUrlBeforeNativeIsLoaded() throws Exception {
// Wait for the activity to load, but don't let it load the native library.
mTestDelegate.shouldDelayLoadingNative = true;
final SearchActivity searchActivity = startSearchActivity();
mTestDelegate.shouldDelayNativeInitializationCallback.waitForCallback(0);
Assert.assertEquals(0, mTestDelegate.showSearchEngineDialogIfNeededCallback.getCallCount());
Assert.assertEquals(0, mTestDelegate.onFinishDeferredInitializationCallback.getCallCount());
// Submit a URL before native is loaded. The browser shouldn't start yet.
mOmnibox.requestFocus();
mOmnibox.typeText(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL, true);
verifyNoMoreInteractions(mAutocompleteController);
Assert.assertEquals(searchActivity, ApplicationStatus.getLastTrackedFocusedActivity());
Assert.assertFalse(searchActivity.isFinishing());
waitForChromeTabbedActivityToStart(
() -> {
// Finish initialization. It should notice the URL is queued up and start the
// browser.
ThreadUtils.runOnUiThreadBlocking(
() -> {
searchActivity.startDelayedNativeInitializationForTests();
});
Assert.assertEquals(
1,
mTestDelegate.shouldDelayNativeInitializationCallback.getCallCount());
mTestDelegate.showSearchEngineDialogIfNeededCallback.waitForCallback(0);
mTestDelegate.onFinishDeferredInitializationCallback.waitForCallback(0);
return null;
},
ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
}
@Test
@SmallTest
public void testZeroSuggestBeforeNativeIsLoaded() {
ThreadUtils.runOnUiThreadBlocking(
() -> {
LocaleManager.getInstance()
.setDelegateForTest(
new LocaleManagerDelegate() {
@Override
public boolean needToCheckForSearchEnginePromo() {
return false;
}
});
});
CachedZeroSuggestionsManager.saveToCache(buildDummyAutocompleteResult());
// Wait for the activity to load, but don't let it load the native library.
mTestDelegate.shouldDelayLoadingNative = true;
startSearchActivity();
// Focus on the url bar with not text.
mOmnibox.requestFocus();
// Omnibox suggestions should appear now.
mOmnibox.checkSuggestionsShown();
verifyNoMoreInteractions(mAutocompleteController);
}
@Test
@SmallTest
@DisabledTest(message = "crbug.com/346528506")
public void testTypeBeforeDeferredInitialization() throws Exception {
// Start the Activity. It should pause and assume that a promo dialog has appeared.
mTestDelegate.shouldDelayDeferredInitialization = true;
startSearchActivity();
mTestDelegate.shouldDelayNativeInitializationCallback.waitForCallback(0);
mTestDelegate.showSearchEngineDialogIfNeededCallback.waitForCallback(0);
Assert.assertNotNull(mTestDelegate.onSearchEngineFinalizedCallback);
Assert.assertEquals(0, mTestDelegate.onFinishDeferredInitializationCallback.getCallCount());
// Native initialization is finished, but we don't have a DSE elected yet.
verify(mAutocompleteController, times(1)).addOnSuggestionsReceivedListener(any());
// Set some text in the search box, then continue startup.
mOmnibox.requestFocus();
// Confirm specifically:
// - no prefetch,
// - no zero suggestions fetches,
// - no typed suggestions fetches.
verifyNoMoreInteractions(mAutocompleteController);
ThreadUtils.runOnUiThreadBlocking(mTestDelegate.onSearchEngineFinalizedCallback.bind(true));
// Let the initialization finish completely.
Assert.assertEquals(
1, mTestDelegate.shouldDelayNativeInitializationCallback.getCallCount());
Assert.assertEquals(1, mTestDelegate.showSearchEngineDialogIfNeededCallback.getCallCount());
mTestDelegate.onFinishDeferredInitializationCallback.waitForCallback(0);
// Omnibox suggestions should be requested now.
verify(mAutocompleteController, times(1))
.startZeroSuggest(
eq(""),
any(/* DSE URL */ ),
eq(PageClassification.ANDROID_SEARCH_WIDGET_VALUE),
any());
}
@Test
@MediumTest
public void testSetUrl_urlBarTextEmpty() throws Exception {
final SearchActivity searchActivity = startSearchActivity();
mTestDelegate.shouldDelayNativeInitializationCallback.waitForCallback(0);
mTestDelegate.showSearchEngineDialogIfNeededCallback.waitForCallback(0);
mTestDelegate.onFinishDeferredInitializationCallback.waitForCallback(0);
LocationBarCoordinator locationBarCoordinator =
searchActivity.getLocationBarCoordinatorForTesting();
UrlBar urlBar = searchActivity.findViewById(R.id.url_bar);
ThreadUtils.runOnUiThreadBlocking(
() -> {
locationBarCoordinator.onUrlChangedForTesting();
Assert.assertTrue(urlBar.getText().toString().isEmpty());
});
ThreadUtils.runOnUiThreadBlocking(
() -> {
locationBarCoordinator.clearOmniboxFocus();
locationBarCoordinator.onUrlChangedForTesting();
Assert.assertTrue(urlBar.getText().toString().isEmpty());
});
}
@Test
@SmallTest
@DisabledTest(message = "crbug.com/346528506")
public void testupdateAnchorViewLayout() {
SearchActivity searchActivity = startSearchActivity();
View anchorView = searchActivity.findViewById(R.id.toolbar);
var layoutParams = anchorView.getLayoutParams();
int focusedHeight =
searchActivity
.getResources()
.getDimensionPixelSize(R.dimen.toolbar_height_no_shadow_focused);
int expectedHeight =
searchActivity
.getResources()
.getDimensionPixelSize(R.dimen.toolbar_height_no_shadow)
+ searchActivity
.getResources()
.getDimensionPixelSize(R.dimen.toolbar_url_focus_height_increase);
int expectedBottomPadding = 0;
Assert.assertEquals(expectedHeight, focusedHeight);
Assert.assertEquals(expectedHeight, layoutParams.height);
Assert.assertEquals(expectedBottomPadding, anchorView.getPaddingBottom());
}
private SearchActivity startSearchActivity() {
return startSearchActivity(0, /* isVoiceSearch= */ false);
}
private SearchActivity startSearchActivity(int expectedCallCount, boolean isVoiceSearch) {
final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
ActivityMonitor searchMonitor =
new ActivityMonitor(SearchActivity.class.getName(), null, false);
instrumentation.addMonitor(searchMonitor);
// The SearchActivity shouldn't have started yet.
Assert.assertEquals(
expectedCallCount,
mTestDelegate.shouldDelayNativeInitializationCallback.getCallCount());
Assert.assertEquals(
expectedCallCount,
mTestDelegate.showSearchEngineDialogIfNeededCallback.getCallCount());
Assert.assertEquals(
expectedCallCount,
mTestDelegate.onFinishDeferredInitializationCallback.getCallCount());
// Fire the Intent to start up the SearchActivity.
try {
SearchWidgetProvider.createIntent(instrumentation.getContext(), isVoiceSearch).send();
} catch (PendingIntent.CanceledException e) {
Assert.assertTrue("Intent canceled", false);
}
Activity searchActivity =
instrumentation.waitForMonitorWithTimeout(
searchMonitor, CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL);
Assert.assertNotNull("Activity didn't start", searchActivity);
Assert.assertTrue("Wrong activity started", searchActivity instanceof SearchActivity);
instrumentation.removeMonitor(searchMonitor);
mOmnibox = new OmniboxTestUtils(searchActivity);
return (SearchActivity) searchActivity;
}
private void waitForChromeTabbedActivityToStart(Callable<Void> trigger, String expectedUrl)
throws Exception {
final ChromeTabbedActivity cta =
ActivityTestUtils.waitForActivity(
InstrumentationRegistry.getInstrumentation(),
ChromeTabbedActivity.class,
trigger);
CriteriaHelper.pollUiThread(
() -> {
Tab tab = cta.getActivityTab();
Criteria.checkThat(tab, Matchers.notNullValue());
Criteria.checkThat(tab.getUrl().getSpec(), Matchers.is(expectedUrl));
});
mActivityTestRule.setActivity(cta);
}
@SuppressLint("SetTextI18n")
private void setUrlBarText(final Activity activity, final String url) {
CriteriaHelper.pollUiThread(
() -> {
UrlBar urlBar = activity.findViewById(R.id.url_bar);
try {
Criteria.checkThat(
"UrlBar not focusable", urlBar.isFocusable(), Matchers.is(true));
Criteria.checkThat(
"UrlBar does not have focus", urlBar.hasFocus(), Matchers.is(true));
} catch (CriteriaNotSatisfiedException ex) {
urlBar.requestFocus();
throw ex;
}
});
ThreadUtils.runOnUiThreadBlocking(
() -> {
UrlBar urlBar = activity.findViewById(R.id.url_bar);
urlBar.setText(url);
});
}
}