// Copyright 2015 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.contextualsearch;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE;
import static org.chromium.chrome.browser.multiwindow.MultiWindowTestHelper.waitForSecondChromeTabbedActivity;
import static org.chromium.chrome.browser.multiwindow.MultiWindowTestHelper.waitForTabs;
import android.app.Activity;
import android.app.Instrumentation;
import android.app.Instrumentation.ActivityMonitor;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.google.common.collect.ImmutableMap;
import org.hamcrest.Matchers;
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.chromium.base.ThreadUtils;
import org.chromium.base.test.util.ApplicationTestUtils;
import org.chromium.base.test.util.Batch;
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.DisableIf;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.MaxAndroidSdkLevel;
import org.chromium.base.test.util.Restriction;
import org.chromium.base.test.util.UserActionTester;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.PanelState;
import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchBarControl;
import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchImageControl;
import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchPanel;
import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchQuickActionControl;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchInternalStateController.InternalState;
import org.chromium.chrome.browser.contextualsearch.ResolvedSearchTerm.CardTag;
import org.chromium.chrome.browser.findinpage.FindToolbar;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.layouts.animation.CompositorAnimationHandler;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tabmodel.TabClosureParams;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeController;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.FullscreenTestUtils;
import org.chromium.chrome.test.util.MenuUtils;
import org.chromium.components.external_intents.ExternalNavigationHandler;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.test.util.UiRestriction;
import org.chromium.url.GURL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
// TODO(donnd): Create class with limited API to encapsulate the internals of simulations.
/** Tests the Contextual Search Manager using instrumentation tests. */
@RunWith(ChromeJUnit4ClassRunner.class)
// NOTE: Disable online detection so we we'll default to online on test bots with no network.
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@EnableFeatures(ChromeFeatureList.CONTEXTUAL_SEARCH_DISABLE_ONLINE_DETECTION)
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
@Batch(Batch.PER_CLASS)
public class ContextualSearchManagerTest extends ContextualSearchInstrumentationBase {
@Mock private EdgeToEdgeController mMockEdgeToEdgeController;
// DOM element IDs in our test page based on what functions they trigger.
// TODO(donnd): add more, and also the associated Search Term, or build a similar mapping.
/**
* The DOM node for the word "search" on the test page, which causes a plain search response
* with the Search Term "Search" from the Fake server.
*/
private static final String SIMPLE_SEARCH_NODE_ID = "search";
/** Feature maps that we use for parameterized tests. */
/** This represents the current fully-launched configuration. */
private static final ImmutableMap<String, Boolean> ENABLE_NONE = ImmutableMap.of();
private UserActionTester mActionTester;
@Override
@Before
public void setUp() throws Exception {
mTestPage = "/chrome/test/data/android/contextualsearch/tap_test.html";
super.setUp();
}
@Override
@After
public void tearDown() throws Exception {
super.tearDown();
if (mActionTester != null) mActionTester.tearDown();
mPanel.setEdgeToEdgeControllerSupplierForTesting(() -> null);
}
// ============================================================================================
// UMA assertions
// ============================================================================================
private void assertUserActionRecorded(String userActionFullName) throws Exception {
Assert.assertTrue(mActionTester.getActions().contains(userActionFullName));
}
// ============================================================================================
// Test Cases
// ============================================================================================
/** Tests swiping the overlay open, after an initial trigger that activates the peeking card. */
@Test
@SmallTest
@Feature({"ContextualSearch"})
@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
@DisabledTest(message = "crbug.com/1373276")
public void testSwipeExpand() throws Exception {
// TODO(donnd): enable for all features.
assertNoSearchesLoaded();
triggerResolve("intelligence");
assertNoSearchesLoaded();
// Fake a search term resolution response.
fakeResponse(
false, 200, "Intelligence", "United States Intelligence", "alternate-term", false);
assertContainsParameters("Intelligence", "alternate-term");
Assert.assertEquals(1, mFakeServer.getLoadedUrlCount());
assertLoadedLowPriorityUrl();
waitForPanelToPeek();
flingPanelUp();
waitForPanelToExpand();
Assert.assertEquals(1, mFakeServer.getLoadedUrlCount());
assertLoadedLowPriorityUrl();
}
/**
* Tests swiping the overlay open, after an initial non-resolve search that activates the
* peeking card, followed by closing the panel. This test also verifies that we don't create any
* {@link WebContents} or load any URL until the panel is opened.
*/
@Test
@SmallTest
@Feature({"ContextualSearch"})
@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
public void testNonResolveSwipeExpand() throws Exception {
simulateNonResolveSearch("search");
assertNoWebContents();
assertLoadedNoUrl();
expandPanelAndAssert();
assertWebContentsCreated();
assertLoadedNormalPriorityUrl();
Assert.assertEquals(1, mFakeServer.getLoadedUrlCount());
// tap the base page to close.
closePanel();
Assert.assertEquals(1, mFakeServer.getLoadedUrlCount());
assertNoWebContents();
}
/** Tests that long-press selects text, and a subsequent tap will unselect text. */
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testLongPressGestureSelects() throws Exception {
longPressNode("intelligence");
Assert.assertEquals("Intelligence", getSelectedText());
waitForPanelToPeek();
assertLoadedNoUrl(); // No load (preload) after long-press until opening panel.
clickNode("question-mark");
waitForPanelToCloseAndSelectionEmpty();
Assert.assertTrue(TextUtils.isEmpty(getSelectedText()));
assertLoadedNoUrl();
}
/** Tests that a Resolve gesture selects the expected text. */
@Test
@SmallTest
@Feature({"ContextualSearch"})
// Previously flaky, disabled 4/2021. https://crbug.com/1192285, https://crbug.com/1192561
public void testResolveGestureSelects() throws Exception {
simulateResolveSearch("intelligence");
Assert.assertEquals("Intelligence", getSelectedText());
assertLoadedLowPriorityUrl();
clickNode("question-mark");
waitForPanelToClose();
Assert.assertTrue(getSelectedText() == null || getSelectedText().isEmpty());
}
// ============================================================================================
// Various Tests
// ============================================================================================
/*
* Test that tapping on the open-new-tab icon before having a resolved search term does not
* promote to a tab, and that after the resolution it does promote to a tab.
*/
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testPromotesToTab() throws Exception {
// -------- SET UP ---------
// Track Tab creation with this helper.
final CallbackHelper tabCreatedHelper = new CallbackHelper();
int tabCreatedHelperCallCount = tabCreatedHelper.getCallCount();
TabModelSelectorObserver observer =
new TabModelSelectorObserver() {
@Override
public void onNewTabCreated(Tab tab, @TabCreationState int creationState) {
tabCreatedHelper.notifyCalled();
}
};
ThreadUtils.runOnUiThreadBlocking(
() -> sActivityTestRule.getActivity().getTabModelSelector().addObserver(observer));
// Track User Actions
mActionTester = new UserActionTester();
// -------- TEST ---------
// Start a slow-resolve search and maximize the Panel.
simulateSlowResolveSearch("search");
maximizePanel();
waitForPanelToMaximize();
// A click should not promote since we are still waiting to Resolve.
forceOpenTabIconClick();
// Assert that the Panel is still maximized.
waitForPanelToMaximize();
// Let the Search Term Resolution finish.
simulateSlowResolveFinished();
// Now a click to promote to a separate tab.
forceOpenTabIconClick();
// The Panel should now be closed.
waitForPanelToClose();
// Make sure a tab was created.
tabCreatedHelper.waitForCallback(tabCreatedHelperCallCount);
// Make sure we captured the promotion in UMA.
assertUserActionRecorded("ContextualSearch.TabPromotion");
// -------- CLEAN UP ---------
ThreadUtils.runOnUiThreadBlocking(
() -> {
sActivityTestRule.getActivity().getTabModelSelector().removeObserver(observer);
});
}
// ============================================================================================
// Undecided/Decided users.
// ============================================================================================
/** Tests that we do not resolve or preload when the privacy Opt-in has not been accepted. */
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testUnacceptedPrivacy() throws Exception {
mPolicy.overrideDecidedStateForTesting(false);
simulateResolvableSearchAndAssertResolveAndPreload("states", false);
}
/** Tests that we do resolve and preload when the privacy Opt-in has been accepted. */
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testAcceptedPrivacy() throws Exception {
mPolicy.overrideDecidedStateForTesting(true);
simulateResolvableSearchAndAssertResolveAndPreload("states", true);
}
/**
* Tests ContextualSearchManager#shouldInterceptNavigation for a case that an initial navigation
* has a user gesture but the redirected external navigation doesn't.
*/
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testRedirectedExternalNavigationWithUserGesture() throws Exception {
ExternalNavigationHandler.sAllowIntentsToSelfForTesting = true;
simulateResolveSearch("intelligence");
GURL initialUrl = new GURL("http://test.com");
final NavigationHandle navigationHandle =
NavigationHandle.createForTesting(
initialUrl,
/* isRendererInitiated= */ true,
PageTransition.LINK,
/* hasUserGesture= */ true);
GURL redirectUrl = new GURL(EXTERNAL_APP_URL);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
new Runnable() {
@Override
public void run() {
Assert.assertFalse(
mPanel.getOverlayPanelContent()
.getInterceptNavigationDelegateForTesting()
.shouldIgnoreNavigation(
navigationHandle,
initialUrl,
false,
false));
Assert.assertEquals(0, mActivityMonitor.getHits());
navigationHandle.didRedirect(redirectUrl, true);
Assert.assertTrue(
mPanel.getOverlayPanelContent()
.getInterceptNavigationDelegateForTesting()
.shouldIgnoreNavigation(
navigationHandle,
redirectUrl,
false,
false));
Assert.assertEquals(1, mActivityMonitor.getHits());
}
});
}
/**
* Tests ContextualSearchManager#shouldInterceptNavigation for a case that an external
* navigation has a user gesture.
*/
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testExternalNavigationWithUserGesture() throws Exception {
ExternalNavigationHandler.sAllowIntentsToSelfForTesting = true;
testExternalNavigationImpl(true);
}
/**
* Tests ContextualSearchManager#shouldInterceptNavigation for a case that an external
* navigation doesn't have a user gesture.
*/
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testExternalNavigationWithoutUserGesture() throws Exception {
ExternalNavigationHandler.sAllowIntentsToSelfForTesting = true;
testExternalNavigationImpl(false);
}
private void testExternalNavigationImpl(boolean hasGesture) throws Exception {
simulateResolveSearch("intelligence");
GURL url = new GURL(EXTERNAL_APP_URL);
final NavigationHandle navigationHandle =
NavigationHandle.createForTesting(
url, /* isRendererInitiated= */ true, PageTransition.LINK, hasGesture);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
new Runnable() {
@Override
public void run() {
Assert.assertTrue(
mPanel.getOverlayPanelContent()
.getInterceptNavigationDelegateForTesting()
.shouldIgnoreNavigation(
navigationHandle, url, false, false));
}
});
Assert.assertEquals(hasGesture ? 1 : 0, mActivityMonitor.getHits());
}
// ============================================================================================
// Translate Tests
// ============================================================================================
/** Tests that a simple Tap without language determination does not trigger translation. */
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testTapWithoutLanguage() throws Exception {
// Resolving an English word should NOT trigger translation.
simulateResolveSearch("search");
// Make sure we did not try to trigger translate.
Assert.assertFalse(mManager.getRequest().isTranslationForced());
}
/** Tests the Translate Caption on a resolve gesture forces a translation. */
@Test
@LargeTest
@Feature({"ContextualSearch"})
public void testTranslateCaption() throws Exception {
// Resolving a German word should trigger translation.
simulateResolveSearch("german");
// Make sure we tried to trigger translate.
Assert.assertTrue(
"Translation was not forced with the current request URL: "
+ mManager.getRequest().getSearchUrl(),
mManager.getRequest().isTranslationForced());
}
// ============================================================================================
// END Translate Tests
// ============================================================================================
/**
* Tests that Contextual Search works in fullscreen. Specifically, tests that tapping a word
* peeks the panel, expanding the bar results in the bar ending at the correct spot in the page
* and tapping the base page closes the panel.
*/
@Test
@SmallTest
// Previously flaky and disabled 4/2021. See https://crbug.com/1197102
@Feature({"ContextualSearch"})
@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
public void testTapContentAndExpandPanelInFullscreen() throws Exception {
// Toggle tab to fulllscreen.
FullscreenTestUtils.togglePersistentFullscreenAndAssert(
sActivityTestRule.getActivity().getActivityTab(),
true,
sActivityTestRule.getActivity());
// Simulate a resolving search and assert that the panel peeks.
simulateResolveSearch("search");
// Expand the panel and assert that it ends up in the right place.
expandPanelAndAssert();
final ContextualSearchPanel panel =
(ContextualSearchPanel) mManager.getContextualSearchPanel();
Assert.assertEquals(
panel.getHeight(), panel.getPanelHeightFromState(PanelState.EXPANDED), 0);
// Tap the base page and assert that the panel is closed.
tapBasePageToClosePanel();
}
/** Tests that the Contextual Search panel is dismissed when entering or exiting fullscreen. */
@Test
@SmallTest
@Feature({"ContextualSearch"})
// Previously flaky on phones: https://crbug.com/765796
public void testPanelDismissedOnToggleFullscreen() throws Exception {
// Simulate a resolving search and assert that the panel peeks.
simulateResolveSearch("search");
// Toggle tab to fullscreen.
Tab tab = sActivityTestRule.getActivity().getActivityTab();
FullscreenTestUtils.togglePersistentFullscreenAndAssert(
tab, true, sActivityTestRule.getActivity());
// Assert that the panel is closed.
waitForPanelToClose();
// Simulate a resolving search and assert that the panel peeks.
simulateResolveSearch("search");
// Toggle tab to non-fullscreen.
FullscreenTestUtils.togglePersistentFullscreenAndAssert(
tab, false, sActivityTestRule.getActivity());
// Assert that the panel is closed.
waitForPanelToClose();
}
/**
* Tests that ContextualSearchImageControl correctly sets either the icon sprite or thumbnail as
* visible.
*/
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testImageControl() throws Exception {
simulateResolveSearch("search");
final ContextualSearchImageControl imageControl = mPanel.getImageControl();
Assert.assertFalse(imageControl.getThumbnailVisible());
Assert.assertTrue(TextUtils.isEmpty(imageControl.getThumbnailUrl()));
ThreadUtils.runOnUiThreadBlocking(
() -> {
imageControl.setThumbnailUrl("http://someimageurl.com/image.png");
imageControl.onThumbnailFetched(true);
});
Assert.assertTrue(imageControl.getThumbnailVisible());
Assert.assertEquals(imageControl.getThumbnailUrl(), "http://someimageurl.com/image.png");
ThreadUtils.runOnUiThreadBlocking(() -> imageControl.hideCustomImage(false));
Assert.assertFalse(imageControl.getThumbnailVisible());
Assert.assertTrue(TextUtils.isEmpty(imageControl.getThumbnailUrl()));
}
// TODO(twellington): Add an end-to-end integration test for fetching a thumbnail based on a
// a URL that is included with the resolution response.
/**
* Tests that the quick action caption is set correctly when one is available. Also tests that
* the caption gets changed when the panel is expanded and reset when the panel is closed.
*/
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testQuickActionCaptionAndImage() throws Exception {
CompositorAnimationHandler.setTestingMode(true);
// Simulate a resolving search to show the Bar, then set the quick action data.
simulateResolveSearch("search");
ThreadUtils.runOnUiThreadBlocking(
() ->
mPanel.onSearchTermResolved(
"search",
null,
"geo:47.6,-122.3",
QuickActionCategory.ADDRESS,
CardTag.CT_LOCATION,
/* relatedSearchesInBar= */ null));
ContextualSearchBarControl barControl = mPanel.getSearchBarControl();
ContextualSearchQuickActionControl quickActionControl = barControl.getQuickActionControl();
ContextualSearchImageControl imageControl = mPanel.getImageControl();
// Check that the peeking bar is showing the quick action data.
Assert.assertTrue(quickActionControl.hasQuickAction());
Assert.assertTrue(barControl.getCaptionVisible());
// There may be different Map apps on different devices, so we just check that we got an
// open intent of some kind.
final String expectedCaptionStart = "Open in ";
Assert.assertEquals(
expectedCaptionStart,
barControl.getCaptionText().subSequence(0, expectedCaptionStart.length()));
Assert.assertEquals(1.f, imageControl.getCustomImageVisibilityPercentage(), 0);
// Expand the bar.
ThreadUtils.runOnUiThreadBlocking(() -> mPanel.simulateTapOnEndButton());
waitForPanelToExpand();
// Check that the expanded bar is showing the correct image.
Assert.assertEquals(0.f, imageControl.getCustomImageVisibilityPercentage(), 0);
// Go back to peeking.
peekPanel();
// Assert that the quick action data is showing.
Assert.assertTrue(barControl.getCaptionVisible());
Assert.assertEquals(
expectedCaptionStart,
barControl.getCaptionText().subSequence(0, expectedCaptionStart.length()));
// TODO(donnd): figure out why we get ~0.65 on Oreo rather than 1. https://crbug.com/818515.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Assert.assertEquals(1.f, imageControl.getCustomImageVisibilityPercentage(), 0);
} else {
Assert.assertTrue(0.5f < imageControl.getCustomImageVisibilityPercentage());
}
CompositorAnimationHandler.setTestingMode(false);
}
/** Tests that an intent is sent when the bar is tapped and a quick action is available. */
@Test
@SmallTest
@Feature({"ContextualSearch"})
// Previously disabled: https://crbug.com/1315417
public void testQuickActionIntent() throws Exception {
// Add a new filter to the activity monitor that matches the intent that should be fired.
IntentFilter quickActionFilter = new IntentFilter(Intent.ACTION_VIEW);
quickActionFilter.addDataScheme("geo");
// Note that we don't reuse mActivityMonitor here or we would leak the one already added
// (unless we removed it here first). When ActivityMonitors leak, Instrumentation silently
// ignores matching ones added after and tests will fail.
ActivityMonitor activityMonitor =
InstrumentationRegistry.getInstrumentation()
.addMonitor(
quickActionFilter,
new Instrumentation.ActivityResult(Activity.RESULT_OK, null),
true);
// Simulate a resolving search to show the Bar, then set the quick action data.
simulateResolveSearch("search");
ThreadUtils.runOnUiThreadBlocking(
() ->
mPanel.onSearchTermResolved(
"search",
null,
"geo:47.6,-122.3",
QuickActionCategory.ADDRESS,
CardTag.CT_LOCATION,
/* relatedSearchesInBar= */ null));
sActivityTestRule.getActivity().onUserInteraction();
// Expand the panel to trigger the quick action intent to be fired.
expandPanelAndAssert();
CriteriaHelper.pollUiThread(
() -> {
int intentHits = activityMonitor.getHits();
Criteria.checkThat(intentHits, Matchers.is(1));
});
// Assert that an intent was fired.
Assert.assertEquals(1, activityMonitor.getHits());
InstrumentationRegistry.getInstrumentation().removeMonitor(activityMonitor);
}
@Test
@SmallTest
@Feature({"ContextualSearch"})
// TODO(donnd): reenable - recent fixes as of 3/31/2023
@DisableIf.Build(sdk_is_greater_than = Build.VERSION_CODES.O, message = "crbug.com/1075895")
// Previously disabled: https://crbug.com/1127796
public void testQuickActionUrl() throws Exception {
final String testUrl = mTestServer.getURL("/chrome/test/data/android/google.html");
// Simulate a resolving search to show the Bar, then set the quick action data.
simulateResolveSearch("search");
ThreadUtils.runOnUiThreadBlocking(
() ->
mPanel.onSearchTermResolved(
"search",
null,
testUrl,
QuickActionCategory.WEBSITE,
CardTag.CT_URL,
/* relatedSearchesInBar= */ null));
sActivityTestRule.getActivity().onUserInteraction();
// Expand the bar which should trigger the quick action.
expandPanel();
// Wait for that URL to be loaded.
CriteriaHelper.pollUiThread(
() -> {
Criteria.checkThat(
ChromeTabUtils.getUrlStringOnUiThread(
sActivityTestRule.getActivity().getActivityTab()),
Matchers.is(testUrl));
});
}
private void runDictionaryCardTest(@CardTag int cardTag) throws Exception {
// Simulate a resolving search to show the Bar, then set the quick action data.
simulateResolveSearch("search");
ThreadUtils.runOnUiThreadBlocking(
() ->
mPanel.onSearchTermResolved(
"obscure · əbˈskyo͝or",
null,
null,
QuickActionCategory.NONE,
cardTag,
/* relatedSearchesInBar= */ null));
expandPanelAndAssert();
}
/**
* Tests that the flow for showing dictionary definitions works, and that tapping in the bar
* just opens the panel instead of taking some action.
*/
@Test
@SmallTest
@Feature({"ContextualSearch"})
// Previously disabled: http://crbug.com/1296677
public void testDictionaryDefinitions() throws Exception {
runDictionaryCardTest(CardTag.CT_DEFINITION);
}
/**
* Tests that the flow for showing dictionary definitions works, and that tapping in the bar
* just opens the panel instead of taking some action.
*/
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testContextualDictionaryDefinitions() throws Exception {
runDictionaryCardTest(CardTag.CT_CONTEXTUAL_DEFINITION);
}
/** Tests accessibility mode: Tap and Long-press don't activate CS. */
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testAccesibilityMode() throws Exception {
mManager.onAccessibilityModeChanged(true);
// Simulate a tap that resolves to show the Bar.
clickNode("intelligence");
assertNoWebContents();
assertNoSearchesLoaded();
// Simulate a Long-press.
longPressNodeWithoutWaiting("states");
assertNoWebContents();
assertNoSearchesLoaded();
mManager.onAccessibilityModeChanged(false);
}
/** Tests when FirstRun is not completed: Tap and Long-press don't activate CS. */
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testFirstRunNotCompleted() throws Exception {
// Store the original value in a temp, and mark the first run as not completed
// for this test case.
// Getting value from shared preference rather than FirstRunStatus#getFirstRunFlowComplete
// to get rid of the impact from commandline switch. See https://crbug.com/1158467
boolean originalIsFirstRunComplete =
ChromeSharedPreferences.getInstance()
.readBoolean(ChromePreferenceKeys.FIRST_RUN_FLOW_COMPLETE, false);
FirstRunStatus.setFirstRunFlowComplete(false);
// Simulate a tap that resolves to show the Bar.
clickNode("intelligence");
assertNoWebContents();
assertNoSearchesLoaded();
// Simulate a Long-press.
longPressNodeWithoutWaiting("states");
assertNoWebContents();
assertNoSearchesLoaded();
// Restore the original shared preference value before this test case ends.
FirstRunStatus.setFirstRunFlowComplete(originalIsFirstRunComplete);
}
// ============================================================================================
// Internal State Controller tests, which ensure that the internal logic flows as expected for
// each type of triggering gesture.
// ============================================================================================
/**
* Tests that the Manager cycles through all the expected Internal States on Tap that Resolves.
*/
@Test
@SmallTest
@Feature({"ContextualSearch"})
// Previously flaky and disabled 4/2021. https://crbug.com/1058297
public void testAllInternalStatesVisitedResolvingTap() throws Exception {
// Set up a tracking version of the Internal State Controller.
ContextualSearchInternalStateControllerWrapper internalStateControllerWrapper =
ContextualSearchInternalStateControllerWrapper
.makeNewInternalStateControllerWrapper(mManager);
mManager.setContextualSearchInternalStateController(internalStateControllerWrapper);
// Simulate a gesture that resolves to show the Bar.
simulateResolveSearch("search");
Assert.assertEquals(
"Some states were started but never finished",
internalStateControllerWrapper.getStartedStates(),
internalStateControllerWrapper.getFinishedStates());
Assert.assertEquals(
"The resolving Tap gesture did not sequence through the expected states.",
ContextualSearchInternalStateControllerWrapper.EXPECTED_TAP_RESOLVE_SEQUENCE,
internalStateControllerWrapper.getFinishedStates());
Assert.assertEquals(
"The Tap gesture did not trigger a resolved search, or the resolve sequence did "
+ "not complete.",
InternalState.SEARCH_COMPLETED,
internalStateControllerWrapper.getState());
}
/**
* Tests that the Manager cycles through all the expected Internal States on Long-press that
* Resolves.
*/
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testAllInternalStatesVisitedResolvingLongpress__rsearches() throws Exception {
// Set up a tracking version of the Internal State Controller.
ContextualSearchInternalStateControllerWrapper internalStateControllerWrapper =
ContextualSearchInternalStateControllerWrapper
.makeNewInternalStateControllerWrapper(mManager);
mManager.setContextualSearchInternalStateController(internalStateControllerWrapper);
// Simulate a resolving search to show the Bar.
longPressNode(SIMPLE_SEARCH_NODE_ID);
fakeAResponse();
Assert.assertEquals(
"Some states were started but never finished",
internalStateControllerWrapper.getStartedStates(),
internalStateControllerWrapper.getFinishedStates());
Assert.assertEquals(
"The resolving Long-press gesture did not sequence through the expected states.",
ContextualSearchInternalStateControllerWrapper.EXPECTED_LONGPRESS_RESOLVE_SEQUENCE,
internalStateControllerWrapper.getFinishedStates());
Assert.assertEquals(
"The Long-press gesture did not trigger a resolved search, or the resolve sequence "
+ "did not complete.",
InternalState.SEARCH_COMPLETED,
internalStateControllerWrapper.getState());
}
/** Tests that the Manager cycles through all the expected Internal States on a Long-press. */
@Test
@SmallTest
@Feature({"ContextualSearch"})
// Previously flaky and disabled 4/2021. https://crbug.com/1192285
@DisabledTest(
message = "TODO(donnd): reenable after unifying resolving and non-resolving longpress.")
public void testAllInternalStatesVisitedNonResolveLongpress() throws Exception {
// Set up a tracking version of the Internal State Controller.
ContextualSearchInternalStateControllerWrapper internalStateControllerWrapper =
ContextualSearchInternalStateControllerWrapper
.makeNewInternalStateControllerWrapper(mManager);
mManager.setContextualSearchInternalStateController(internalStateControllerWrapper);
// Simulate a Long-press to show the Bar.
simulateNonResolveSearch("search");
Assert.assertEquals(
"Some states were started but never finished",
internalStateControllerWrapper.getStartedStates(),
internalStateControllerWrapper.getFinishedStates());
Assert.assertEquals(
"The non-resolving Long-press gesture didn't sequence through all of the expected "
+ " states.",
ContextualSearchInternalStateControllerWrapper.EXPECTED_LONGPRESS_SEQUENCE,
internalStateControllerWrapper.getFinishedStates());
}
// ============================================================================================
// Various tests
// ============================================================================================
@Test
@SmallTest
@Feature({"ContextualSearch"})
// Previously flaky and disabled 4/2021. https://crbug.com/1180304
public void testTriggeringContextualSearchHidesFindInPageOverlay() throws Exception {
MenuUtils.invokeCustomMenuActionSync(
InstrumentationRegistry.getInstrumentation(),
sActivityTestRule.getActivity(),
R.id.find_in_page_id);
CriteriaHelper.pollUiThread(
() -> {
FindToolbar findToolbar =
(FindToolbar)
sActivityTestRule.getActivity().findViewById(R.id.find_toolbar);
Criteria.checkThat(findToolbar, Matchers.notNullValue());
Criteria.checkThat(findToolbar.isShown(), Matchers.is(true));
Criteria.checkThat(findToolbar.isAnimating(), Matchers.is(false));
});
// Don't type anything to Find because that may cause scrolling which makes clicking in the
// page flaky.
View findToolbar = sActivityTestRule.getActivity().findViewById(R.id.find_toolbar);
Assert.assertTrue(findToolbar.isShown());
simulateResolveSearch("search");
waitForPanelToPeek();
Assert.assertFalse(
"Find Toolbar should no longer be shown once Contextual Search Panel appeared",
findToolbar.isShown());
}
/**
* Tests Tab reparenting. When a tab moves from one activity to another the
* ContextualSearchTabHelper should detect the change and handle gestures for it too. This
* happens with multiwindow modes.
*/
@Test
@LargeTest
@Feature({"ContextualSearch"})
@CommandLineFlags.Add(ChromeSwitches.DISABLE_TAB_MERGING_FOR_TESTING)
@MaxAndroidSdkLevel(value = Build.VERSION_CODES.R, reason = "crbug.com/1301017")
public void testTabReparenting() throws Exception {
// Move our "tap_test" tab to another activity.
final ChromeActivity ca = sActivityTestRule.getActivity();
// Create a new tab so |ca| isn't destroyed.
ChromeTabUtils.newTabFromMenu(InstrumentationRegistry.getInstrumentation(), ca);
ChromeTabUtils.switchTabInCurrentTabModel(ca, 0);
int testTabId = ca.getActivityTab().getId();
MenuUtils.invokeCustomMenuActionSync(
InstrumentationRegistry.getInstrumentation(),
ca,
R.id.move_to_other_window_menu_id);
// Wait for the second activity to start up and be ready for interaction.
final ChromeTabbedActivity activity2 = waitForSecondChromeTabbedActivity();
waitForTabs("CTA2", activity2, 1, testTabId);
// Trigger on a word and wait for the selection to be established.
triggerNode(activity2.getActivityTab(), "search");
CriteriaHelper.pollUiThread(
() -> {
String selection =
activity2
.getContextualSearchManagerForTesting()
.getSelectionController()
.getSelectedText();
Criteria.checkThat(selection, Matchers.is("Search"));
});
ThreadUtils.runOnUiThreadBlocking(
() ->
activity2
.getCurrentTabModel()
.closeTabs(TabClosureParams.closeAllTabs().build()));
ApplicationTestUtils.finishActivity(activity2);
}
// --------------------------------------------------------------------------------------------
// Longpress-resolve Feature tests: force long-press experiment and make sure that triggers.
// --------------------------------------------------------------------------------------------
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testTapIsIgnoredWithLongpressResolveEnabled() throws Exception {
clickNode("states");
Assert.assertNull(getSelectedText());
assertPanelClosedOrUndefined();
assertLoadedNoUrl();
}
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testLongpressResolveEnabled() throws Exception {
longPressNode("states");
assertLoadedNoUrl();
assertSearchTermRequested();
fakeResponse(false, 200, "states", "United States Intelligence", "alternate-term", false);
waitForPanelToPeek();
assertLoadedLowPriorityUrl();
assertContainsParameters("states", "alternate-term");
}
/** Monitor user action UMA recording operations. */
private static class UserActionMonitor extends UserActionTester {
// TODO(donnd): merge into UserActionTester. See https://crbug.com/1103757.
private Set<String> mUserActionPrefixes;
private Map<String, Integer> mUserActionCounts;
/**
* @param userActionPrefixes A set of plain prefix strings for user actions to monitor.
*/
UserActionMonitor(Set<String> userActionPrefixes) {
mUserActionPrefixes = userActionPrefixes;
mUserActionCounts = new HashMap<String, Integer>();
for (String action : mUserActionPrefixes) {
mUserActionCounts.put(action, 0);
}
}
@Override
public void onResult(String action) {
for (String entry : mUserActionPrefixes) {
if (action.startsWith(entry)) {
mUserActionCounts.put(entry, mUserActionCounts.get(entry) + 1);
}
}
}
/**
* Gets the count of user actions recorded for the given prefix.
*
* @param actionPrefix The plain string prefix to lookup (must match a constructed entry)
* @return The count of user actions recorded for that prefix.
*/
int get(String actionPrefix) {
return mUserActionCounts.get(actionPrefix);
}
}
@Test
@SmallTest
@Feature({"ContextualSearch"})
@DisabledTest(message = "https://crbug.com/1048827, https://crbug.com/1181088")
public void testLongpressExtendingSelectionExactResolve() throws Exception {
// Set up UserAction monitoring.
Set<String> userActions = new HashSet();
userActions.add("ContextualSearch.SelectionEstablished");
userActions.add("ContextualSearch.ManualRefine");
UserActionMonitor userActionMonitor = new UserActionMonitor(userActions);
// First test regular long-press. It should not require an exact resolve.
longPressNode("search");
fakeAResponse();
assertSearchTermRequested();
assertExactResolve(false);
// Long press a node without release so we can simulate the user extending the selection.
long downTime = longPressNodeWithoutUp("search");
// Extend the selection to the nearby word.
longPressExtendSelection("term", "resolution", downTime);
waitForSelectActionBarVisible();
fakeAResponse();
assertSearchTermRequested();
assertExactResolve(true);
// Check UMA metrics recorded.
Assert.assertEquals(2, userActionMonitor.get("ContextualSearch.ManualRefine"));
Assert.assertEquals(2, userActionMonitor.get("ContextualSearch.SelectionEstablished"));
}
@Test
@SmallTest
@Feature({"ContextualSearch"})
public void testPeekStateHeight() throws Exception {
final float defaultHeight = 70;
longPressNode("states");
assertLoadedNoUrl();
assertSearchTermRequested();
Assert.assertEquals(
"Default height for the bar should be 70 DP.",
defaultHeight,
mPanel.getBarHeight(),
0.001f);
// Increase the selected TextView height to be taller than the default height.
ThreadUtils.runOnUiThreadBlocking(
() -> {
mPanel.getSearchBarControl().setCaption("Increase Height");
TextView textView = mPanel.getSearchBarControl().getCaptionTextView();
float dpToPx =
InstrumentationRegistry.getInstrumentation()
.getContext()
.getResources()
.getDisplayMetrics()
.density;
textView.getLayoutParams().height = (int) ((defaultHeight * 2) * dpToPx);
ViewUtils.requestLayout(textView, "Update the selected TextView height");
});
fakeResponse(false, 200, "states", "United States Intelligence", "alternate-term", false);
waitForPanelToPeek();
CriteriaHelper.pollUiThread(
() -> {
Criteria.checkThat(mPanel.getBarHeight(), Matchers.greaterThan(defaultHeight));
});
}
@Test
@SmallTest
@Feature({"ContextualSearch"})
@EnableFeatures({"DrawEdgeToEdge, DrawCutoutEdgeToEdge"})
public void testPeekStateHeightGrowsForEdgeToEdge() throws Exception {
// Run through with the fake controller using the default logic.
mPanel.setEdgeToEdgeControllerSupplierForTesting(() -> mMockEdgeToEdgeController);
when(mMockEdgeToEdgeController.getBottomInset()).thenReturn(0);
final float defaultHeight = 70;
simulateResolveSearch("search");
CriteriaHelper.pollUiThread(
() -> {
try {
Criteria.checkThat(
mPanel.getPanelHeightFromState(PanelState.PEEKED),
Matchers.equalTo(defaultHeight));
} catch (CriteriaNotSatisfiedException ex) {
Assert.fail(
"Error - Peek Height or Bar Height is not the normal expected value"
+ " for these tests.");
}
});
closePanel();
verify(mMockEdgeToEdgeController, atLeastOnce()).getBottomInset();
// Set ToEdge, which returns a non-zero inset. The panel should be positioned higher.
final int arbitraryGestureNavHeight = 100;
reset(mMockEdgeToEdgeController);
when(mMockEdgeToEdgeController.getBottomInset()).thenReturn(arbitraryGestureNavHeight);
simulateResolveSearch("search");
CriteriaHelper.pollUiThread(
() -> {
try {
Criteria.checkThat(
mPanel.getPanelHeightFromState(PanelState.PEEKED),
Matchers.equalTo(defaultHeight + arbitraryGestureNavHeight));
} catch (CriteriaNotSatisfiedException ex) {
Assert.fail(
"When EdgeToEdge is active the Peek position should be inset for"
+ " the Bottom Gesture Nav Bar.");
}
});
closePanel();
verify(mMockEdgeToEdgeController, atLeastOnce()).getBottomInset();
}
}