chromium/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchTriggerTest.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.contextualsearch;

import static org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE;

import android.text.TextUtils;
import android.view.ViewConfiguration;

import androidx.test.filters.SmallTest;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.FeatureList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
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.Restriction;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.content_public.browser.SelectionClient;
import org.chromium.ui.test.util.UiRestriction;

/** Tests the Related Searches Feature of Contextual Search 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 ContextualSearchTriggerTest extends ContextualSearchInstrumentationBase {
    @Override
    @Before
    public void setUp() throws Exception {
        mTestPage = "/chrome/test/data/android/contextualsearch/tap_test.html";
        super.setUp();
    }

    // ============================================================================================
    // Test Cases
    // ============================================================================================

    /** Tests the doesContainAWord method. TODO(donnd): Change to a unit test. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    public void testDoesContainAWord() {
        Assert.assertTrue(mSelectionController.doesContainAWord("word"));
        Assert.assertTrue(mSelectionController.doesContainAWord("word "));
        Assert.assertFalse(
                "Emtpy string should not be considered a word!",
                mSelectionController.doesContainAWord(""));
        Assert.assertFalse(
                "Special symbols should not be considered a word!",
                mSelectionController.doesContainAWord("@"));
        Assert.assertFalse(
                "White space should not be considered a word",
                mSelectionController.doesContainAWord(" "));
        Assert.assertTrue(mSelectionController.doesContainAWord("Q2"));
        Assert.assertTrue(mSelectionController.doesContainAWord("123"));
    }

    /** Tests the isValidSelection method. TODO(donnd): Change to a unit test. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    public void testIsValidSelection() {
        StubbedSelectionPopupController c = new StubbedSelectionPopupController();
        Assert.assertTrue(mSelectionController.isValidSelection("valid", c));
        Assert.assertFalse(mSelectionController.isValidSelection(" ", c));
        c.setIsFocusedNodeEditableForTest(true);
        Assert.assertFalse(mSelectionController.isValidSelection("editable", c));
        c.setIsFocusedNodeEditableForTest(false);
        String numberString = "0123456789";
        Assert.assertTrue(mSelectionController.isValidSelection(numberString, c));
        StringBuilder longStringBuilder = new StringBuilder().append(numberString);
        for (int i = 0; i < 10; i++) {
            longStringBuilder.append(longStringBuilder.toString());
            if (longStringBuilder.toString().length() < 1000) {
                Assert.assertTrue(
                        mSelectionController.isValidSelection(longStringBuilder.toString(), c));
            } else {
                Assert.assertFalse(
                        mSelectionController.isValidSelection(longStringBuilder.toString(), c));
                break;
            }
        }
    }

    /** Tests a simple non-resolving gesture, without opening the panel. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    public void testNonResolveTrigger() throws Exception {
        triggerNonResolve("states");

        Assert.assertNull(mFakeServer.getSearchTermRequested());
        waitForPanelToPeek();
        assertLoadedNoUrl();
        assertNoWebContents();
    }

    // ============================================================================================
    // Tap=gesture Tests
    // ============================================================================================

    /** Tests that a Tap gesture on a special character does not select or show the panel. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    // Previously flaky and disabled 4/2021.  https://crbug.com/1180304
    public void testTapGestureOnSpecialCharacterDoesntSelect() throws Exception {
        clickNode("question-mark");
        Assert.assertNull(getSelectedText());
        assertPanelClosedOrUndefined();
        assertLoadedNoUrl();
    }

    /** Tests that a Tap gesture followed by scrolling clears the selection. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    public void testTapGestureFollowedByScrollClearsSelection() throws Exception {
        clickWordNode("intelligence");
        fakeResponse(false, 200, "Intelligence", "Intelligence", "alternate-term", false);
        assertContainsParameters("Intelligence", "alternate-term");
        waitForPanelToPeek();
        assertLoadedLowPriorityUrl();
        scrollBasePage();
        assertPanelClosedOrUndefined();
        Assert.assertTrue(TextUtils.isEmpty(mSelectionController.getSelectedText()));
    }

    /** Tests that a Tap gesture followed by tapping an invalid character doesn't select. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    // Previously flaky and disabled 4/2021.  https://crbug.com/1192285
    public void testTapGestureFollowedByInvalidTextTapCloses() throws Exception {
        clickWordNode("states-far");
        waitForPanelToPeek();
        clickNode("question-mark");
        waitForPanelToClose();
        Assert.assertNull(mSelectionController.getSelectedText());
    }

    /** Tests that a Tap gesture followed by tapping a non-text element doesn't select. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    @DisabledTest(message = "crbug.com/662104")
    public void testTapGestureFollowedByNonTextTap() throws Exception {
        clickWordNode("states-far");
        waitForPanelToPeek();
        clickNode("button");
        waitForPanelToCloseAndSelectionEmpty();
    }

    /** Tests that a Tap gesture far away toggles selecting text. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    public void testTapGestureFarAwayTogglesSelecting() throws Exception {
        clickWordNode("states");
        Assert.assertEquals("States", getSelectedText());
        waitForPanelToPeek();
        clickNode("states-far");
        waitForPanelToClose();
        Assert.assertNull(getSelectedText());
        clickNode("states-far");
        waitForPanelToPeek();
        Assert.assertEquals("States", getSelectedText());
    }

    /** Tests a "tap-near" -- that sequential Tap gestures nearby keep selecting. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    // Previously disabled at https://crbug.com/1075895
    @DisabledTest(message = "See crbug.com/1455161") // Disabled because it is flaky
    public void testTapGesturesNearbyKeepSelecting() throws Exception {
        clickWordNode("states");
        Assert.assertEquals("States", getSelectedText());
        waitForPanelToPeek();
        // Avoid issues with double-tap detection by ensuring sequential taps
        // aren't treated as such. Double-tapping can also select words much as
        // longpress, in turn showing the pins and preventing contextual tap
        // refinement from nearby taps. The double-tap timeout is sufficiently
        // short that this shouldn't conflict with tap refinement by the user.
        Thread.sleep(ViewConfiguration.getDoubleTapTimeout());
        // Because sequential taps never hide the bar, we we can't wait for it to peek.
        // Instead we use clickNode (which doesn't wait) instead of clickWordNode and wait
        // for the selection to change.
        clickNode("states-near");
        waitForSelectionToBe("StatesNear");
        Thread.sleep(ViewConfiguration.getDoubleTapTimeout());
        clickNode("states");
        waitForSelectionToBe("States");
    }

    // ============================================================================================
    // Long-press non-triggering gesture tests.
    // ============================================================================================

    /** Tests that a long-press gesture followed by scrolling does not clear the selection. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    public void testLongPressGestureFollowedByScrollMaintainsSelection() throws Exception {
        longPressNode("intelligence");
        waitForPanelToPeek();
        scrollBasePage();
        assertPanelClosedOrUndefined();
        Assert.assertEquals("Intelligence", getSelectedText());
        assertLoadedNoUrl();
    }

    /** Tests that a long-press gesture followed by a tap does not select. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
    @DisabledTest(message = "See https://crbug.com/837998")
    public void testLongPressGestureFollowedByTapDoesntSelect() throws Exception {
        longPressNode("intelligence");
        waitForPanelToPeek();
        clickWordNode("states-far");
        waitForGestureToClosePanelAndAssertNoSelection();
        assertLoadedNoUrl();
    }

    /** Tests suppression of any triggering on small view heights. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    @Restriction(Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SEARCH_SUPPRESS_SHORT_VIEW)
    public void testIsSuppressedOnViewHeight_ridiculouslyShort() {
        FeatureList.TestValues testValues = new FeatureList.TestValues();
        testValues.addFieldTrialParamOverride(
                ChromeFeatureList.CONTEXTUAL_SEARCH_SUPPRESS_SHORT_VIEW,
                ContextualSearchFieldTrial.CONTEXTUAL_SEARCH_MINIMUM_PAGE_HEIGHT_NAME,
                "100");
        FeatureList.setTestValues(testValues);
        Assert.assertFalse(mContextualSearchManager.isSuppressed());
    }

    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    @Restriction(Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SEARCH_SUPPRESS_SHORT_VIEW)
    public void testIsSuppressedOnViewHeight_ridiculouslyTall() {
        FeatureList.TestValues testValues = new FeatureList.TestValues();
        testValues.addFieldTrialParamOverride(
                ChromeFeatureList.CONTEXTUAL_SEARCH_SUPPRESS_SHORT_VIEW,
                ContextualSearchFieldTrial.CONTEXTUAL_SEARCH_MINIMUM_PAGE_HEIGHT_NAME,
                "500000");
        FeatureList.setTestValues(testValues);
        Assert.assertTrue(mContextualSearchManager.isSuppressed());
    }

    // ============================================================================================
    // Tap-non-triggering when ARIA annotated as interactive.
    // ============================================================================================

    /** Tests that a Tap gesture on an element with an ARIA role does not trigger. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    public void testTapOnRoleIgnored() throws Exception {
        @OverlayPanel.PanelState int initialState = mPanel.getPanelState();
        clickNode("role");
        assertPanelStillInState(initialState);
    }

    /**
     * Tests that a Tap gesture on an element with an ARIA attribute does not trigger.
     * http://crbug.com/542874
     */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    public void testTapOnARIAIgnored() throws Exception {
        @OverlayPanel.PanelState int initialState = mPanel.getPanelState();
        clickNode("aria");
        assertPanelStillInState(initialState);
    }

    /** Tests that a Tap gesture on an element that is focusable does not trigger. */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    public void testTapOnFocusableIgnored() throws Exception {
        @OverlayPanel.PanelState int initialState = mPanel.getPanelState();
        clickNode("focusable");
        assertPanelStillInState(initialState);
    }

    // ============================================================================================
    // Search-term resolution (server request to determine a search).
    // ============================================================================================

    /**
     * Tests expanding the panel before the search term has resolved, verifies that nothing loads
     * until the resolve completes and that it's now a normal priority URL.
     */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    public void testExpandBeforeSearchTermResolution() throws Exception {
        simulateSlowResolveSearch("states");
        assertNoWebContents();

        // Expanding before the search term resolves should not load anything.
        expandPanelAndAssert();
        assertLoadedNoUrl();

        // Once the response comes in, it should load.
        simulateSlowResolveFinished();
        assertContainsParameters("States");
        assertLoadedNormalPriorityUrl();
        assertWebContentsCreated();
        assertWebContentsVisible();
    }

    /**
     * Tests that the Contextual Search panel does not reappear when a long-press selection is
     * modified after the user has taken an action to explicitly dismiss the panel. Also tests that
     * the panel reappears when a new selection is made.
     */
    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    // Previously flaky, disabled 4/2021.  https://crbug.com/1192285, https://crbug.com/1291558
    public void testPreventHandlingCurrentSelectionModification() throws Exception {
        longPressNode("search");

        // Dismiss the Contextual Search panel.
        closePanel();
        Assert.assertEquals("Search", getSelectedText());

        // Simulate a selection change event and assert that the panel has not reappeared.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    SelectionClient selectionClient = mManager.getContextualSearchSelectionClient();
                    selectionClient.onSelectionEvent(
                            org.chromium.ui.touch_selection.SelectionEventType
                                    .SELECTION_HANDLE_DRAG_STARTED,
                            333,
                            450);
                    selectionClient.onSelectionEvent(
                            org.chromium.ui.touch_selection.SelectionEventType
                                    .SELECTION_HANDLE_DRAG_STOPPED,
                            303,
                            450);
                });
        assertPanelClosedOrUndefined();

        // Select a different word and assert that the panel has appeared.
        longPressNode("resolution");
        // The simulateNonResolveSearch call will verify that the panel peeks.
    }

    @Test
    @SmallTest
    @Feature({"ContextualSearch"})
    // Previously flaky and disabled 4/2021.  https://crbug.com/1180304
    public void testSelectionExpansionOnSearchTermResolution() throws Exception {
        triggerResolve("intelligence");
        waitForPanelToPeek();

        ResolvedSearchTerm resolvedSearchTerm =
                new ResolvedSearchTerm.Builder(
                                false, 200, "Intelligence", "United States Intelligence")
                        .setSelectionStartAdjust(-14)
                        .build();
        fakeResponse(resolvedSearchTerm);
        waitForSelectionToBe("United States Intelligence");
    }

    // ============================================================================================
    // Read Aloud Tap to Seek Suppression
    // ============================================================================================

    /**
     * Tests that Contextual Search does not show when ReadAloud has an active playback on the tab.
     */
    @Test
    @SmallTest
    @Feature({"ContextualSearch", "ReadAloud"})
    @EnableFeatures(ChromeFeatureList.READALOUD_TAP_TO_SEEK)
    public void testTapToSeekSuppression() throws Exception {
        changeReadAloudActivePlaybackTab();

        clickNode("intelligence");
        Assert.assertEquals(null, getSelectedText());
    }
}