chromium/chrome/test/android/javatests/src/org/chromium/chrome/test/util/OmniboxTestUtils.java

// 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.test.util;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
import static androidx.test.espresso.matcher.ViewMatchers.withId;

import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.os.SystemClock;
import android.text.Editable;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;

import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Matcher;
import org.hamcrest.Matchers;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.omnibox.LocationBarLayout;
import org.chromium.chrome.browser.omnibox.UrlBar;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteController.OnSuggestionsReceivedListener;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteCoordinator;
import org.chromium.chrome.browser.omnibox.suggestions.DropdownItemViewInfo;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestionsDropdown;
import org.chromium.chrome.browser.omnibox.suggestions.base.ActionChipsProperties;
import org.chromium.chrome.browser.omnibox.suggestions.header.HeaderView;
import org.chromium.chrome.browser.searchwidget.SearchActivity;
import org.chromium.chrome.browser.toolbar.top.ToolbarLayout;
import org.chromium.components.omnibox.AutocompleteMatch;
import org.chromium.components.omnibox.AutocompleteResult;
import org.chromium.components.omnibox.suggestions.OmniboxSuggestionUiType;
import org.chromium.content_public.browser.test.util.KeyUtils;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

/** Utility methods and classes for testing the Omnibox. */
public class OmniboxTestUtils {
    /** Value indicating that the index is not valid. */
    public static final int SUGGESTION_INDEX_INVALID = -1;

    private final @NonNull Activity mActivity;
    private final @NonNull LocationBarLayout mLocationBar;
    private final @NonNull AutocompleteCoordinator mAutocomplete;
    private final @NonNull UrlBar mUrlBar;
    private final @NonNull Instrumentation mInstrumentation;
    private final @Nullable ToolbarLayout mToolbar;

    /**
     * Invokes a specific ViewAction on an {@link
     * org.chromium.chrome.browser.omnibox.suggestions.action.OmniboxAction} at specific position.
     *
     * <p>This class can be chained with {@link
     * androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition}.
     */
    private static class ActionOnOmniboxActionAtPosition implements ViewAction {
        private final ViewAction mAction;

        public ActionOnOmniboxActionAtPosition(int position, ViewAction action) {
            mAction = actionOnItemAtPosition(position, action);
        }

        @Override
        public Matcher<View> getConstraints() {
            return withId(R.id.omnibox_actions_carousel);
        }

        @Override
        public String getDescription() {
            return mAction.getDescription();
        }

        @Override
        public void perform(UiController uiController, View view) {
            mAction.perform(uiController, view.findViewById(R.id.omnibox_actions_carousel));
        }
    }

    /**
     * Create a ViewAction that can be executed on a Suggestion with Action Chips.
     *
     * @param position the index of an {@link
     *         org.chromium.chrome.browser.omnibox.suggestions.action.OmniboxAction},
     * @param action the action to perform.
     */
    public static ViewAction actionOnOmniboxActionAtPosition(int position, ViewAction action) {
        return new ActionOnOmniboxActionAtPosition(position, action);
    }

    /**
     * Class describing individual suggestion, delivering access to broad range of information.
     *
     * @param <T> The type of suggestion view.
     */
    public static class SuggestionInfo<T extends View> {
        public final int index;
        public final @OmniboxSuggestionUiType int type;
        public final @NonNull AutocompleteMatch suggestion;
        public final @NonNull PropertyModel model;
        public final @NonNull T view;

        protected SuggestionInfo(
                int index,
                @OmniboxSuggestionUiType int type,
                @NonNull AutocompleteMatch suggestion,
                @NonNull PropertyModel model,
                @NonNull T view) {
            this.index = index;
            this.type = type;
            this.suggestion = suggestion;
            this.model = model;
            this.view = view;
        }
    }

    /**
     * Create a new OmniboxTestUtils instance from supplied activity.
     *
     * This method should be called if the caller intends to retain the instance
     * for a longer period of time.
     * For short or single-time uses, consider calling static method below.
     */
    public OmniboxTestUtils(@NonNull Activity activity) {
        mActivity = activity;
        if (activity instanceof SearchActivity) {
            mLocationBar = mActivity.findViewById(R.id.search_location_bar);
            mToolbar = null;
        } else {
            mLocationBar = mActivity.findViewById(R.id.location_bar);
            mToolbar = mActivity.findViewById(R.id.toolbar);
        }
        mAutocomplete = mLocationBar.getAutocompleteCoordinator();
        mUrlBar = mActivity.findViewById(R.id.url_bar);
        mInstrumentation = InstrumentationRegistry.getInstrumentation();
    }

    /** Disables any live autocompletion, making Omnibox behave like a standard text field. */
    public void disableLiveAutocompletion() {
        ThreadUtils.runOnUiThreadBlocking(() -> mUrlBar.setTextChangeListener(null));
    }

    /**
     * Waits for all the animations to complete. Allows any preceding operation to kick off an
     * animation.
     */
    public void waitAnimationsComplete() {
        // Note: SearchActivity has no toolbar and no animations, but we still need to
        // give keyboard a bit of time to pop up (requested with delay).
        do {
            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        } while (mToolbar != null && mToolbar.isAnimationRunningForTesting());
    }

    /**
     * Check that the Omnibox reaches the expected focus state.
     *
     * <p>Note: this is known to cause issues with tests that run animations.
     *
     * @param active Whether the Omnibox is expected to have focus or not.
     */
    public void checkFocus(boolean active) {
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "unexpected Omnibox focus state",
                            mUrlBar.hasFocus(),
                            Matchers.is(active));
                    InputMethodManager imm =
                            (InputMethodManager)
                                    mUrlBar.getContext()
                                            .getSystemService(Context.INPUT_METHOD_SERVICE);
                    Criteria.checkThat(
                            "Keyboard did not reach expected state",
                            imm.isActive(mUrlBar),
                            Matchers.is(active));
                });
    }

    /**
     * Determines whether the UrlBar currently has focus.
     *
     * @return Whether the UrlBar has focus.
     */
    public boolean getFocus() {
        return ThreadUtils.runOnUiThreadBlocking(() -> mUrlBar.hasFocus());
    }

    /** Request the Omnibox focus and wait for soft keyboard to show. */
    public void requestFocus() {
        // During early startup (before completion of its first onDraw), the UrlBar
        // is not focusable. Tests have to wait for that to happen before trying to focus it.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat("Omnibox not shown.", mUrlBar.isShown(), Matchers.is(true));
                    Criteria.checkThat(
                            "Omnibox not focusable.", mUrlBar.isFocusable(), Matchers.is(true));
                    if (!mUrlBar.hasFocus()) mUrlBar.requestFocus();
                    Criteria.checkThat(
                            "Omnibox is focused.", mUrlBar.hasFocus(), Matchers.is(true));
                });
    }

    /**
     * Clear the Omnibox focus and wait until keyboard is dismissed. Performs no action if the
     * Omnibox is already unfocused.
     */
    public void clearFocus() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    if (mUrlBar.hasFocus()) {
                        ((ComponentActivity) mActivity)
                                .getOnBackPressedDispatcher()
                                .onBackPressed();
                    }
                });
        // Needed to complete scrolling the UrlBar to TLD.
        mInstrumentation.waitForIdleSync();
        checkFocus(false);
    }

    /**
     * Set the suggestions to the Omnibox to display.
     *
     * @param autocompleteResult The set of suggestions will be displayed on the Omnibox dropdown
     *     list.
     */
    public void setSuggestions(AutocompleteResult autocompleteResult) {
        checkFocus(true);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    OnSuggestionsReceivedListener listener =
                            mAutocomplete.getSuggestionsReceivedListenerForTest();
                    listener.onSuggestionsReceived(autocompleteResult, true);
                });
    }

    /** Waits for a non-empty list of omnibox suggestions is shown. */
    public void checkSuggestionsShown() {
        CriteriaHelper.pollUiThread(
                () -> {
                    OmniboxSuggestionsDropdown suggestionsDropdown =
                            mLocationBar
                                    .getAutocompleteCoordinator()
                                    .getSuggestionsDropdownForTest();
                    Criteria.checkThat(
                            "suggestion list is null",
                            suggestionsDropdown,
                            Matchers.notNullValue());
                    Criteria.checkThat(
                            "suggestion list is not shown",
                            suggestionsDropdown.getViewGroup().isShown(),
                            Matchers.is(true));
                    Criteria.checkThat(
                            "suggestion list has no entries",
                            suggestionsDropdown.getDropdownItemViewCountForTest(),
                            Matchers.greaterThan(0));
                });
    }

    /**
     * Stops any subsequent AutocompleteResults from being generated.
     * Ensures that no subsequent asynchronous AutocompleteResults could tamper with test execution.
     */
    public void waitForAutocomplete() {
        AtomicLong previousId = new AtomicLong(-1);
        AtomicLong count = new AtomicLong();

        CriteriaHelper.pollUiThread(
                () -> {
                    long currentId = mAutocomplete.getCurrentNativeAutocompleteResult();
                    // Suggestions have changed as a result of a recent push.
                    // Reset the counter and monitor for possible updates.
                    if (currentId != previousId.get()) {
                        previousId.set(currentId);
                        count.set(0);
                        return false;
                    }

                    // Check that nothing has changed 3 times in a row, rejecting everything that
                    // arrives late. This guarantees that the suggestions will not change and the
                    // list can be used for testing purposes.
                    if (count.incrementAndGet() < 3) return false;
                    mAutocomplete.stopAutocompleteForTest(false);
                    return true;
                });
    }

    /**
     * Return the first suggestion of the specific type.
     *
     * @param type The type of suggestion to check.
     */
    public <T extends View> SuggestionInfo<T> findSuggestionWithType(
            @OmniboxSuggestionUiType int type) {
        return findSuggestion(info -> info.type == type);
    }

    /** Return the first suggestion that features Action Chips. */
    public @Nullable <T extends View> SuggestionInfo<T> findSuggestionWithActionChips() {
        return findSuggestion(
                info -> {
                    if (!info.model
                            .getAllSetProperties()
                            .contains(ActionChipsProperties.ACTION_CHIPS)) {
                        return false;
                    }
                    return info.model.get(ActionChipsProperties.ACTION_CHIPS) != null;
                });
    }

    /**
     * Return the first suggestion that meets requirements set by supplied filter.
     *
     * @param filter The filter to use to identify appropriate suggestion type.
     */
    public @Nullable <T extends View> SuggestionInfo<T> findSuggestion(
            @NonNull Function<DropdownItemViewInfo, Boolean> filter) {
        checkSuggestionsShown();
        AtomicReference<SuggestionInfo<T>> result = new AtomicReference<>();

        CriteriaHelper.pollUiThread(
                () -> {
                    OmniboxSuggestionsDropdown dropdown =
                            mLocationBar
                                    .getAutocompleteCoordinator()
                                    .getSuggestionsDropdownForTest();

                    ModelList currentModels =
                            mLocationBar
                                    .getAutocompleteCoordinator()
                                    .getSuggestionModelListForTest();
                    for (int i = 0; i < currentModels.size(); i++) {
                        DropdownItemViewInfo info = (DropdownItemViewInfo) currentModels.get(i);
                        T view = (T) dropdown.getDropdownItemViewForTest(i);
                        if (filter.apply(info) && view != null) {
                            result.set(
                                    new SuggestionInfo<T>(
                                            i,
                                            info.type,
                                            mAutocomplete.getSuggestionAt(i),
                                            info.model,
                                            view));
                            return true;
                        }
                    }
                    return false;
                });

        return result.get();
    }

    /**
     * Retrieve the Suggestion View for specific suggestion index. Traverses the Suggestions list
     * and skips over the Headers.
     *
     * @param <T> The type of the expected view. Inferred from call.
     * @param indexOfSuggestion The index of the suggestion view (not including the headers).
     * @return The View corresponding to suggestion with specific index, or null if there's no such
     *     suggestion.
     */
    private @Nullable <T extends View> T getSuggestionViewForIndex(int indexOfSuggestion) {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    OmniboxSuggestionsDropdown dropdown =
                            mLocationBar
                                    .getAutocompleteCoordinator()
                                    .getSuggestionsDropdownForTest();
                    int numViews = dropdown.getDropdownItemViewCountForTest();
                    int nonHeaderViewIndex = 0;

                    for (int childIndex = 0; childIndex < numViews; childIndex++) {
                        View view = dropdown.getDropdownItemViewForTest(childIndex);
                        if (view instanceof HeaderView) continue;

                        if (nonHeaderViewIndex == indexOfSuggestion) return (T) view;
                        nonHeaderViewIndex++;
                    }

                    return null;
                });
    }

    /**
     * Type text in the Omnibox. Requires that the Omnibox is focused ahead of call.
     *
     * @param text Text to be "typed" in the Omnibox.
     * @param execute Whether to perform the default action after typing text (ie. press the "go"
     *     button/enter key).
     */
    public void typeText(String text, boolean execute) {
        checkFocus(true);
        ThreadUtils.runOnUiThreadBlocking(
                () -> KeyUtils.typeTextIntoView(mInstrumentation, mUrlBar, text));

        if (execute) sendKey(KeyEvent.KEYCODE_ENTER);
    }

    /**
     * Send key event to the Omnibox. Requires that the Omnibox is focused.
     *
     * @param keyCode The Key code to send to the Omnibox.
     */
    public void sendKey(int keyCode) {
        sendKey(keyCode, 0);
    }

    /**
     * Send key event to the Omnibox. Requires that the Omnibox is focused.
     *
     * @param keyCode The Key code to send to the Omnibox.
     * @param modifiers Additional modifiers pressed with the key (shift, alt, ...).
     */
    public void sendKey(int keyCode, int modifiers) {
        checkFocus(true);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    var currentTime = SystemClock.uptimeMillis();
                    var event =
                            new KeyEvent(
                                    /* downTime= */ currentTime,
                                    /* eventTime= */ currentTime,
                                    KeyEvent.ACTION_DOWN,
                                    keyCode,
                                    /* repeat= */ 0,
                                    modifiers);
                    if (!mUrlBar.dispatchKeyEventPreIme(event)) mUrlBar.dispatchKeyEvent(event);

                    event =
                            new KeyEvent(
                                    /* downTime= */ currentTime,
                                    /* eventTime= */ currentTime,
                                    KeyEvent.ACTION_UP,
                                    keyCode,
                                    /* repeat= */ 0,
                                    modifiers);

                    if (!mUrlBar.dispatchKeyEventPreIme(event)) mUrlBar.dispatchKeyEvent(event);
                });
    }

    /**
     * Specify the text to be shown in the Omnibox. Cancels all autocompletion. Use this to
     * initialize the state of the Omnibox, but avoid using this to validate any behavior.
     *
     * @param userText The text to be shown in the Omnibox.
     */
    public void setText(String userText) {
        checkFocus(true);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mUrlBar.setText(userText);
                    // Push this to the model as well.
                    mUrlBar.setAutocompleteText(userText, "", Optional.empty());
                });
        checkText(Matchers.equalTo(userText), null);
    }

    /**
     * Commit text to the Omnibox, as if it was supplied by the soft keyboard
     * autocorrect/autocomplete feature.
     *
     * @param textToCommit The text to supply as if it was supplied by Soft Keyboard.
     * @param commitAsAutocomplete Whether the text should be applied as autocompletion (true) or
     *     autocorrection (false). Note that autocorrection works only if the Omnibox is currently
     *     composing text.
     */
    public void commitText(@NonNull String textToCommit, boolean commitAsAutocomplete) {
        checkFocus(true);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    InputConnection conn = mUrlBar.getInputConnection();
                    if (commitAsAutocomplete) conn.finishComposingText();
                    // Value of 1 always advance the cursor to the position after the full text
                    // being inserted.
                    conn.commitText(textToCommit, 1);
                });
    }

    /**
     * Specify the text to be offered as an inline autocompletion for the current user input.
     *
     * @param autocompleteText The suggested autocompletion for the text.
     * @param additionalText The additional autocompletion for the text.
     */
    public void setAutocompleteText(String autocompleteText, Optional<String> additionalText) {
        checkFocus(true);

        AtomicReference<String> userText = new AtomicReference<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    userText.set(mUrlBar.getTextWithoutAutocomplete());
                    mUrlBar.setAutocompleteText(userText.get(), autocompleteText, additionalText);
                });
        checkText(
                Matchers.equalTo(userText.get()),
                Matchers.equalTo(userText.get() + autocompleteText));
    }

    /**
     * Verify the text content of the Omnibox.
     *
     * @param textMatcher Matcher checking the content of the Omnibox.
     * @param autocompleteTextMatcher Optional Matcher for autocompletion.
     */
    public void checkText(
            @NonNull Matcher<String> textMatcher,
            @Nullable Matcher<String> autocompleteTextMatcher) {
        checkText(textMatcher, autocompleteTextMatcher, null);
    }

    /**
     * Verify the text content of the Omnibox.
     *
     * @param textMatcher Matcher checking the content of the Omnibox.
     * @param autocompleteTextMatcher Optional Matcher for autocompletion.
     * @param additionalTextMatcher Optional Matcher for additional text.
     */
    public void checkText(
            @NonNull Matcher<String> textMatcher,
            @Nullable Matcher<String> autocompleteTextMatcher,
            @Nullable Matcher<String> additionalTextMatcher) {
        checkText(textMatcher, autocompleteTextMatcher, additionalTextMatcher, null, null);
    }

    /**
     * Verify the text content of the Omnibox.
     *
     * @param textMatcher Matcher checking the content of the Omnibox.
     * @param autocompleteTextMatcher Optional Matcher for autocompletion.
     * @param additionalTextMatcher Optional Matcher for additional text.
     * @param autocompleteSelectionStart Matcher for Autocomplete's start position.
     * @param autocompleteSelectionEnd Matcher for Autocomplete's end position.
     */
    public void checkText(
            @NonNull Matcher<String> textMatcher,
            @Nullable Matcher<String> autocompleteTextMatcher,
            @Nullable Matcher<String> additionalTextMatcher,
            int autocompleteSelectionStart,
            int autocompleteSelectionEnd) {
        checkText(
                textMatcher,
                autocompleteTextMatcher,
                additionalTextMatcher,
                Matchers.is(autocompleteSelectionStart),
                Matchers.is(autocompleteSelectionEnd));
    }

    /**
     * Verify the text content of the Omnibox.
     *
     * @param textMatcher Matcher checking the content of the Omnibox.
     * @param autocompleteTextMatcher Optional Matcher for autocompletion.
     * @param additionalTextMatcher Optional Matcher for additional text.
     * @param autocompleteSelectionStart Optional Matcher for Autocomplete's start position.
     * @param autocompleteSelectionEnd Optional Matcher for Autocomplete's end position.
     */
    public void checkText(
            @NonNull Matcher<String> textMatcher,
            @Nullable Matcher<String> autocompleteTextMatcher,
            @Nullable Matcher<String> additionalTextMatcher,
            @Nullable Matcher<Integer> autocompleteSelectionStart,
            @Nullable Matcher<Integer> autocompleteSelectionEnd) {
        waitAnimationsComplete();
        CriteriaHelper.pollUiThread(
                () -> {
                    if (mUrlBar.hasFocus()) {
                        // URL bar is focused. Match against the edit state.
                        Criteria.checkThat(
                                "Text without autocomplete should match",
                                mUrlBar.getTextWithoutAutocomplete(),
                                textMatcher);

                        Criteria.checkThat(
                                "Unexpected Autocomplete state",
                                mUrlBar.hasAutocomplete(),
                                Matchers.is(autocompleteTextMatcher != null));

                        if (autocompleteTextMatcher != null) {
                            Criteria.checkThat(
                                    "Text with autocomplete should match",
                                    mUrlBar.getTextWithAutocomplete(),
                                    autocompleteTextMatcher);
                        }

                        if (additionalTextMatcher != null) {
                            Criteria.checkThat(
                                    "Additional Text should match",
                                    mUrlBar.getAdditionalText().orElse(""),
                                    additionalTextMatcher);
                        }

                        if (autocompleteSelectionStart != null) {
                            Criteria.checkThat(
                                    "Autocomplete Selection start",
                                    mUrlBar.getSelectionStart(),
                                    autocompleteSelectionStart);
                        }

                        // TODO(crbug.com/40211958): Investigate why AutocompleteSelectionEnd was
                        // never enforced and why it doesn't work, then possibly re-enable the
                        // logic below: if (autocompleteSelectionEnd != null) {
                        //     Criteria.checkThat("Autocomplete Selection end",
                        //             mUrlBar.getSelectionEnd(),
                        //             autocompleteSelectionEnd);
                        // }
                    } else {
                        // URL bar is not focused. Match against the content.
                        Criteria.checkThat(mUrlBar.getText().toString(), textMatcher);
                    }
                });
    }

    /**
     * @return The text contents of the omnibox (without the Autocomplete part).
     */
    public String getText() {
        return ThreadUtils.runOnUiThreadBlocking(() -> mUrlBar.getTextWithoutAutocomplete());
    }

    /**
     * Verify the Composing text in the Omnibox.
     *
     * Unlike Autocomplete, Composing text enables more finegrained control of the edited text.
     * This is particularly relevant to certain family of languages and input connections, where
     * individual characters or sequences are modified with subsequent keystrokes (eg. T9).
     *
     * @param textMatcher Matcher for the Omnibox content containing composed text.
     * @param composingRangeStart Character index where the compose begins.
     * @param composingRangeEnd Character index where the compose ends.
     */
    public void checkComposingText(
            @NonNull Matcher<String> textMatcher, int composingRangeStart, int composingRangeEnd) {
        checkFocus(true);
        CriteriaHelper.pollUiThread(
                () -> {
                    // Confirm no autocompletion during active compose.
                    Criteria.checkThat(
                            "Composing text should have no Autocompletion",
                            mUrlBar.hasAutocomplete(),
                            Matchers.is(false));

                    // Here getTextWithAutocomplete, getTextWithoutAutocomplete and getText should
                    // all return the same content. Since we already know there's no
                    // autocompletion, we can skip the additional validation.
                    Editable composingText = mUrlBar.getText();
                    Criteria.checkThat(composingText.toString(), textMatcher);

                    Criteria.checkThat(
                            "Composing Span Start",
                            BaseInputConnection.getComposingSpanStart(composingText),
                            Matchers.is(composingRangeStart));
                    Criteria.checkThat(
                            "Composing Span End",
                            BaseInputConnection.getComposingSpanEnd(composingText),
                            Matchers.is(composingRangeEnd));
                });
    }

    /**
     * Set the Composing text in the Omnibox.
     *
     * <p>Assumes that the supplied composingRegionStart is a valid text position (does not verify
     * test's sanity).
     *
     * @param composingText The composing text to apply.
     * @param composingRegionStart The placement inside the existing text where composing starts.
     * @param composingRegionEnd The placement inside the existing text where composing ends.
     */
    public void setComposingText(
            @NonNull String composingText, int composingRegionStart, int composingRegionEnd) {
        checkFocus(true);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    InputConnection conn = mUrlBar.getInputConnection();
                    conn.setComposingRegion(composingRegionStart, composingRegionEnd);
                    conn.setComposingText(
                            composingText, /* newCursorPosition= */ composingText.length());
                });
    }

    /**
     * Click the n-th action.
     *
     * @param suggestionIndex the index of suggestion to click an action on.
     * @param actionIndex the index of action to invoke.
     */
    public void clickOnAction(int suggestionIndex, int actionIndex) {
        onView(withId(R.id.omnibox_suggestions_dropdown))
                .perform(
                        actionOnItemAtPosition(
                                suggestionIndex,
                                OmniboxTestUtils.actionOnOmniboxActionAtPosition(
                                        actionIndex, click())));
    }
}