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

import static org.hamcrest.core.IsEqual.equalTo;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;

import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.InputConnection;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;

import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;

import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.params.ParameterAnnotations.UseRunnerDelegate;
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.DisabledTest;
import org.chromium.base.test.util.RequiresRestart;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.OmniboxTestUtils;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.ui.base.Clipboard;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Tests for the URL bar UI component.
 *
 * <p>TODO(ender): Wrap the UrlBar in a separate standalone activity to focus testing on the
 * component alone. This should help deflake several tests here and focus on the logic and behavior.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@UseRunnerDelegate(ChromeJUnit4RunnerDelegate.class)
@Batch(Batch.PER_CLASS)
public class UrlBarTest {
    public static @ClassRule ChromeTabbedActivityTestRule sActivityTestRule =
            new ChromeTabbedActivityTestRule();
    private UrlBar mUrlBar;
    private OmniboxTestUtils mOmnibox;

    @BeforeClass
    public static void setUpClass() throws Exception {
        sActivityTestRule.startMainActivityWithURL(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
        // Needed to make sure all the necessary ChromeFeatureFlags are populated.
        sActivityTestRule.waitForDeferredStartup();
    }

    @Before
    public void setUpTest() throws Exception {
        mOmnibox = new OmniboxTestUtils(sActivityTestRule.getActivity());
        mUrlBar = sActivityTestRule.getActivity().findViewById(R.id.url_bar);
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        // Start with an empty Omnibox and disable all automatic features.
        mOmnibox.disableLiveAutocompletion();
        mOmnibox.requestFocus();
        mOmnibox.setText("");
    }

    private static class AutocompleteState {
        public final boolean hasAutocomplete;
        public final String textWithoutAutocomplete;
        public final String textWithAutocomplete;
        public final String additionalText;

        public AutocompleteState(
                boolean hasAutocomplete,
                String textWithoutAutocomplete,
                String textWithAutocomplete,
                String additionalText) {
            this.hasAutocomplete = hasAutocomplete;
            this.textWithoutAutocomplete = textWithoutAutocomplete;
            this.textWithAutocomplete = textWithAutocomplete;
            this.additionalText = additionalText;
        }
    }

    private AutocompleteState getAutocompleteState(final Runnable action) {
        final AtomicBoolean hasAutocomplete = new AtomicBoolean();
        final AtomicReference<String> textWithoutAutocomplete = new AtomicReference<String>();
        final AtomicReference<String> textWithAutocomplete = new AtomicReference<String>();
        final AtomicReference<String> additionalText = new AtomicReference<String>();

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    if (action != null) action.run();
                    hasAutocomplete.set(mUrlBar.hasAutocomplete());
                    textWithoutAutocomplete.set(mUrlBar.getTextWithoutAutocomplete());
                    textWithAutocomplete.set(mUrlBar.getTextWithAutocomplete());
                    additionalText.set(mUrlBar.getAdditionalText().orElse(""));
                });

        return new AutocompleteState(
                hasAutocomplete.get(),
                textWithoutAutocomplete.get(),
                textWithAutocomplete.get(),
                additionalText.get());
    }

    private AutocompleteState setSelection(final int selectionStart, final int selectionEnd) {
        return getAutocompleteState(() -> mUrlBar.setSelection(selectionStart, selectionEnd));
    }

    private void setTextAndVerifyTextDirection(String text, int expectedDirection)
            throws TimeoutException {
        CallbackHelper directionCallback = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mUrlBar.setUrlDirectionListener(
                            (direction) -> {
                                if (direction == expectedDirection) {
                                    directionCallback.notifyCalled();
                                }
                            });
                });
        mOmnibox.setText(text);
        directionCallback.waitForOnly(
                "Direction never reached expected direction: " + expectedDirection);
        assertUrlDirection(expectedDirection);
        ThreadUtils.runOnUiThreadBlocking(() -> mUrlBar.setUrlDirectionListener(null));
    }

    private void assertUrlDirection(int expectedDirection) {
        int actualDirection = ThreadUtils.runOnUiThreadBlocking(() -> mUrlBar.getUrlDirection());
        Assert.assertEquals(expectedDirection, actualDirection);
    }

    @Test
    @SmallTest
    public void testRefocusing() {
        // This test is flaky, because of the asynchronous nature of keyboard management that does
        // not involve canceling previously scheduled tasks.
        // For cases where keyboard is requested and dismissed rapidly, tasks begin to compete with
        // each other, and eventually a subsequent action's keyboard callup / dismiss is scheduled
        // before the previous action's counter-request, leading to a flake.
        for (int i = 0; i < 5; i++) {
            mOmnibox.requestFocus();
            mOmnibox.clearFocus();
        }
    }

    @Test
    @SmallTest
    public void testAutocompleteUpdatedOnSetText() {
        // Verify that setting a new string will clear the autocomplete.
        mOmnibox.setText("test");
        mOmnibox.setAutocompleteText("ing is fun", Optional.empty());

        // Replace part of the non-autocomplete text
        mOmnibox.setText("test");
        mOmnibox.setAutocompleteText("ing is fun", Optional.empty());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mUrlBar.setText(mUrlBar.getText().replace(1, 2, "a"));
                });
        mOmnibox.checkText(equalTo("tast"), null);

        // Replace part of the autocomplete text.
        mOmnibox.setText("test");
        mOmnibox.setAutocompleteText("ing is fun", Optional.empty());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mUrlBar.setText(mUrlBar.getText().replace(8, 10, "no"));
                });
        mOmnibox.checkText(equalTo("test"), null);
    }

    private void verifySelectionState(
            String text,
            String inlineAutocomplete,
            String additionalText,
            int selectionStart,
            int selectionEnd,
            boolean expectedHasAutocomplete,
            String expectedTextWithoutAutocomplete,
            String expectedTextWithAutocomplete,
            boolean expectedPreventInline,
            String expectedRequestedAutocompleteText)
            throws TimeoutException {
        mOmnibox.setText(text);
        mOmnibox.setAutocompleteText(inlineAutocomplete, Optional.of(additionalText));

        final CallbackHelper autocompleteHelper = new CallbackHelper();
        final AtomicReference<String> requestedAutocompleteText = new AtomicReference<String>();
        final AtomicBoolean didPreventInlineAutocomplete = new AtomicBoolean();
        mUrlBar.setTextChangeListener(
                (textWithoutAutocomplete) -> {
                    autocompleteHelper.notifyCalled();
                    requestedAutocompleteText.set(textWithoutAutocomplete);
                    didPreventInlineAutocomplete.set(!mUrlBar.shouldAutocomplete());
                    mUrlBar.setTextChangeListener(null);
                });

        AutocompleteState state = setSelection(selectionStart, selectionEnd);
        Assert.assertEquals("Has autocomplete", expectedHasAutocomplete, state.hasAutocomplete);
        Assert.assertEquals(
                "Text w/o Autocomplete",
                expectedTextWithoutAutocomplete,
                state.textWithoutAutocomplete);
        Assert.assertEquals(
                "Text w/ Autocomplete", expectedTextWithAutocomplete, state.textWithAutocomplete);
        Assert.assertEquals("Addition Text", additionalText, state.additionalText);

        autocompleteHelper.waitForCallback(0);
        Assert.assertEquals(
                "Prevent inline autocomplete",
                expectedPreventInline,
                didPreventInlineAutocomplete.get());
        Assert.assertEquals(
                "Requested autocomplete text",
                expectedRequestedAutocompleteText,
                requestedAutocompleteText.get());
    }

    @Test
    @SmallTest
    public void testAutocompleteUpdatedOnSelection() throws TimeoutException {
        // Verify that setting a selection before the autocomplete clears it.
        verifySelectionState(
                "test", "ing is fun", "foo.com", 1, 1, false, "test", "test", true, "test");

        // Verify that setting a selection range before the autocomplete clears it.
        verifySelectionState(
                "test", "ing is fun", "foo.com", 0, 4, false, "test", "test", true, "test");

        // Verify that setting a selection range that covers a portion of the non-autocomplete
        // and autocomplete text does not delete the autocomplete text.
        verifySelectionState(
                "test",
                "ing is fun",
                "foo.com",
                2,
                5,
                false,
                "testing is fun",
                "testing is fun",
                true,
                "testing is fun");

        // Verify that setting a selection range that over the entire string does not delete
        // the autocomplete text.
        verifySelectionState(
                "test",
                "ing is fun",
                "foo.com",
                0,
                14,
                false,
                "testing is fun",
                "testing is fun",
                true,
                "testing is fun");

        // Note: with new model touching the beginning of the autocomplete text is a no-op.
        // Verify that setting a selection at the end of the text does not delete the
        // autocomplete text.
        verifySelectionState(
                "test",
                "ing is fun",
                "foo.com",
                14,
                14,
                false,
                "testing is fun",
                "testing is fun",
                true,
                "testing is fun");

        // Verify that setting a selection in the middle of the autocomplete text does not delete
        // the autocomplete text.
        verifySelectionState(
                "test",
                "ing is fun",
                "foo.com",
                9,
                9,
                false,
                "testing is fun",
                "testing is fun",
                true,
                "testing is fun");

        // Verify that setting a selection range in the middle of the autocomplete text does not
        // delete the autocomplete text.
        verifySelectionState(
                "test",
                "ing is fun",
                "foo.com",
                8,
                11,
                false,
                "testing is fun",
                "testing is fun",
                true,
                "testing is fun");

        // Select autocomplete text. As we do not expect the suggestions to be refreshed, we test
        // this slightly differently than the other cases.
        mOmnibox.setText("test");
        mOmnibox.setAutocompleteText("ing is fun", Optional.of("www.bar.com"));
        ThreadUtils.runOnUiThreadBlocking(() -> mUrlBar.setSelection(4, 14));
        mOmnibox.checkText(equalTo("testing is fun"), null, equalTo("www.bar.com"));
    }

    /**
     * Ensure that we send cursor position with autocomplete requests.
     *
     * <p>When reading this test, it helps to remember that autocomplete requests are not sent with
     * the user simply moves the cursor. They're only sent on text modifications.
     */
    @Test
    @SmallTest
    public void testSendCursorPosition() throws TimeoutException {
        final CallbackHelper autocompleteHelper = new CallbackHelper();
        final AtomicInteger cursorPositionUsed = new AtomicInteger();
        mUrlBar.setTextChangeListener(
                (textWithoutAutocomplete) -> {
                    int cursorPosition =
                            mUrlBar.getSelectionEnd() == mUrlBar.getSelectionStart()
                                    ? mUrlBar.getSelectionStart()
                                    : -1;
                    cursorPositionUsed.set(cursorPosition);
                    autocompleteHelper.notifyCalled();
                });

        // User types "a".
        // Omnibox: a|
        mOmnibox.typeText("a", false);
        autocompleteHelper.waitForCallback(0);
        Assert.assertEquals(1, cursorPositionUsed.get());

        // Keyboard autocompletes "cd".
        // Omnibox: acd|
        mOmnibox.commitText("cd", true);
        autocompleteHelper.waitForCallback(1);
        Assert.assertEquals(3, cursorPositionUsed.get());

        // User moves the cursor.
        mOmnibox.sendKey(KeyEvent.KEYCODE_DPAD_LEFT);
        mOmnibox.sendKey(KeyEvent.KEYCODE_DPAD_LEFT);

        // Omnibox: a|cd.
        // No new events sent - cursor position movements don't count as autocomplete events.
        Assert.assertEquals(2, autocompleteHelper.getCallCount());
        Assert.assertEquals(3, cursorPositionUsed.get());

        // User appends "b"
        // Omnibox: ab|cd.
        mOmnibox.typeText("b", false);
        autocompleteHelper.waitForCallback(2);
        Assert.assertEquals(2, cursorPositionUsed.get());

        // User deletes "b"
        // Omnibox text: a|cd
        mOmnibox.sendKey(KeyEvent.KEYCODE_DEL);
        autocompleteHelper.waitForCallback(3);
        Assert.assertEquals(1, cursorPositionUsed.get());

        // User deletes "a"
        // Omnibox text: |cd
        mOmnibox.sendKey(KeyEvent.KEYCODE_DEL);
        autocompleteHelper.waitForCallback(4);
        Assert.assertEquals(0, cursorPositionUsed.get());

        mUrlBar.setTextChangeListener(null);
    }

    /**
     * Ensure that we allow inline autocomplete when the text gets shorter but is not an explicit
     * delete action by the user.
     *
     * <p>If you focus the omnibox and there is the selected text "[about:blank]", then typing new
     * text should clear that entirely and allow autocomplete on the newly entered text.
     *
     * <p>If we assume deletes happen any time the text gets shorter, then this would be prevented.
     */
    @Test
    @SmallTest
    public void testAutocompleteAllowedWhenReplacingText() throws TimeoutException {
        final String textToBeEntered = "c";

        final CallbackHelper autocompleteHelper = new CallbackHelper();
        final AtomicBoolean didPreventInlineAutocomplete = new AtomicBoolean();
        mUrlBar.setTextChangeListener(
                (textWithoutAutocomplete) -> {
                    if (!TextUtils.equals(textToBeEntered, mUrlBar.getTextWithoutAutocomplete())) {
                        return;
                    }
                    didPreventInlineAutocomplete.set(!mUrlBar.shouldAutocomplete());
                    autocompleteHelper.notifyCalled();
                    mUrlBar.setTextChangeListener(null);
                });

        mOmnibox.typeText(textToBeEntered, false);
        autocompleteHelper.waitForCallback(0);
        Assert.assertFalse(
                "Inline autocomplete incorrectly prevented.", didPreventInlineAutocomplete.get());
    }

    /**
     * Ensure that if the user deletes just the inlined autocomplete text that the suggestions are
     * regenerated.
     */
    @Test
    @SmallTest
    public void testSuggestionsUpdatedWhenDeletingInlineAutocomplete() throws TimeoutException {
        mOmnibox.setText("test");
        mOmnibox.setAutocompleteText("ing", Optional.empty());

        final CallbackHelper autocompleteHelper = new CallbackHelper();
        final AtomicBoolean didPreventInlineAutocomplete = new AtomicBoolean();
        mUrlBar.setTextChangeListener(
                (textWithoutAutocomplete) -> {
                    if (!TextUtils.equals("test", mUrlBar.getTextWithoutAutocomplete())) return;
                    didPreventInlineAutocomplete.set(!mUrlBar.shouldAutocomplete());
                    autocompleteHelper.notifyCalled();
                    mUrlBar.setTextChangeListener(null);
                });

        mOmnibox.sendKey(KeyEvent.KEYCODE_DEL);

        mOmnibox.checkText(equalTo("test"), null);

        autocompleteHelper.waitForCallback(0);
        Assert.assertTrue(
                "Inline autocomplete incorrectly allowed after delete.",
                didPreventInlineAutocomplete.get());
    }

    @Test
    @SmallTest
    public void testAutocorrectionChangesTriggerCorrectSuggestions() {
        mOmnibox.setComposingText("test", 0, 4);
        mOmnibox.setAutocompleteText("ing is fun", Optional.empty());
        mOmnibox.checkText(equalTo("test"), equalTo("testing is fun"));
        mOmnibox.commitText("rest", false);
        mOmnibox.checkText(equalTo("rest"), null);
    }

    @Test
    @SmallTest
    public void testAutocompletionChangesTriggerCorrectSuggestions() {
        // Type text. Make sure it appears as composing text for the IME.
        mOmnibox.setComposingText("test", 0, 4);
        mOmnibox.setAutocompleteText("ing is fun", Optional.empty());
        mOmnibox.checkText(equalTo("test"), equalTo("testing is fun"));
        mOmnibox.commitText("y", true);
        mOmnibox.checkText(equalTo("testy"), null);
    }

    @Test
    @SmallTest
    public void testAutocompleteCorrectlyPerservedOnBatchMode() {
        // Valid case (cursor at the end of text, single character, matches previous autocomplete).
        mOmnibox.setText("g");
        mOmnibox.setAutocompleteText("oogle.com", Optional.empty());
        mOmnibox.typeText("o", false);
        mOmnibox.checkText(equalTo("go"), equalTo("google.com"));

        // Invalid case (cursor not at the end of the text).
        mOmnibox.setText("g");
        mOmnibox.setAutocompleteText("oogle.com", Optional.empty());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    InputConnection conn = mUrlBar.getInputConnection();
                    conn.beginBatchEdit();
                    conn.finishComposingText();
                    conn.commitText("o", 1);
                    conn.setSelection(0, 0);
                    conn.endBatchEdit();
                });
        mOmnibox.checkText(equalTo("go"), null);

        // Invalid case (next character did not match previous autocomplete)
        mOmnibox.setText("g");
        mOmnibox.setAutocompleteText("oogle.com", Optional.empty());
        mOmnibox.typeText("a", false);
        mOmnibox.checkText(equalTo("ga"), null);

        // Multiple characters entered instead of 1.
        mOmnibox.setText("g");
        mOmnibox.setAutocompleteText("oogle.com", Optional.empty());
        mOmnibox.commitText("oogl", true);
        mOmnibox.checkText(equalTo("googl"), equalTo("google.com"));
    }

    @Test
    @SmallTest
    public void testAutocompleteSpanClearedOnNonMatchingCommitText() {
        mOmnibox.setText("a");
        mOmnibox.setAutocompleteText("mazon.com", Optional.empty());
        mOmnibox.checkText(equalTo("a"), equalTo("amazon.com"));

        mOmnibox.typeText("l", false);
        mOmnibox.checkText(equalTo("al"), null);
    }

    @Test
    @SmallTest
    public void testAutocompleteClearedOnComposition() {
        mOmnibox.setText("test");
        mOmnibox.setAutocompleteText("ing is fun", Optional.empty());

        mOmnibox.setComposingText("ing compose", 4, 4);
        mOmnibox.checkText(equalTo("testing compose"), null);
    }

    @Test
    @SmallTest
    public void testDelayedCompositionCorrectedWithAutocomplete() {
        // Test with a single IME autocomplete
        mOmnibox.typeText("chrome://f", false);
        mOmnibox.setAutocompleteText("lags", Optional.empty());
        mOmnibox.setComposingText("l", 13, 14);
        mOmnibox.checkText(equalTo("chrome://fl"), equalTo("chrome://flags"));

        // Test with > 1 characters in composition.
        mOmnibox.setText("chrome://fl");
        mOmnibox.setAutocompleteText("ags", Optional.empty());
        mOmnibox.checkText(equalTo("chrome://fl"), equalTo("chrome://flags"));
        mOmnibox.setComposingText("fl", 12, 14);
        mOmnibox.checkText(equalTo("chrome://flfl"), null);

        // Test with non-matching composition.  Should just append to the URL text.
        mOmnibox.setText("chrome://f");
        mOmnibox.setAutocompleteText("lags", Optional.empty());
        mOmnibox.checkText(equalTo("chrome://f"), equalTo("chrome://flags"));
        mOmnibox.setComposingText("g", 13, 14);
        mOmnibox.checkText(equalTo("chrome://fg"), null);

        // Test with composition text that matches the entire text w/o autocomplete.
        mOmnibox.setText("chrome://f");
        mOmnibox.setAutocompleteText("lags", Optional.empty());
        mOmnibox.checkText(equalTo("chrome://f"), equalTo("chrome://flags"));
        mOmnibox.setComposingText("chrome://f", 13, 14);
        mOmnibox.checkText(equalTo("chrome://fchrome://f"), null);

        // Test with composition text longer than the URL text.
        // Shouldn't crash and should just append text.
        mOmnibox.setText("chrome://f");
        mOmnibox.setAutocompleteText("lags", Optional.empty());
        mOmnibox.checkText(equalTo("chrome://f"), equalTo("chrome://flags"));
        mOmnibox.setComposingText("blahblahblah", 13, 14);
        mOmnibox.checkText(equalTo("chrome://fblahblahblah"), null);
    }

    @Test
    @SmallTest
    @DisabledTest(message = "Disabled because of b/333536371")
    public void testUrlTextChangeListener() {
        Callback<String> listener = mock(Callback.class);
        mUrlBar.setTextChangeListener(listener);

        mOmnibox.setText("onomatop");
        Mockito.verify(listener).onResult("onomatop");

        // Setting autocomplete does not send a change update.
        mOmnibox.setAutocompleteText("oeia", Optional.empty());

        mOmnibox.setText("");
        Mockito.verify(listener).onResult("");
    }

    @Test
    @SmallTest
    public void testSetAutocompleteText_ShrinkingText() {
        mOmnibox.setText("test");
        mOmnibox.setAutocompleteText("ing is awesome", Optional.empty());
        mOmnibox.setAutocompleteText("ing is hard", Optional.empty());
        mOmnibox.setAutocompleteText("ingz", Optional.empty());
        mOmnibox.checkText(equalTo("test"), equalTo("testingz"), null, 4, 8);
    }

    @Test
    @SmallTest
    public void testSetAutocompleteTextWithAdditionalText_ShrinkingText() {
        mOmnibox.setText("test");
        mOmnibox.setAutocompleteText("ing is awesome", Optional.of("www.foobar.com"));
        mOmnibox.setAutocompleteText("ing is hard", Optional.of("www.bar.com"));
        mOmnibox.setAutocompleteText("ingz", Optional.of("www.foo.com"));
        mOmnibox.checkText(equalTo("test"), equalTo("testingz"), equalTo("www.foo.com"), 4, 8);
    }

    @Test
    @SmallTest
    public void testSetAutocompleteText_GrowingText() {
        mOmnibox.setText("test");
        mOmnibox.setAutocompleteText("ingz", Optional.empty());
        mOmnibox.setAutocompleteText("ing is hard", Optional.empty());
        mOmnibox.setAutocompleteText("ing is awesome", Optional.empty());
        mOmnibox.checkText(equalTo("test"), equalTo("testing is awesome"), null, 4, 18);
    }

    @Test
    @SmallTest
    public void testSetAutocompleteTextWithAdditionalText_GrowingText() {
        mOmnibox.setText("test");
        mOmnibox.setAutocompleteText("ingz", Optional.of("www.foo.com"));
        mOmnibox.setAutocompleteText("ing is hard", Optional.of("www.bar.com"));
        mOmnibox.setAutocompleteText("ing is awesome", Optional.of("www.foobar.com"));
        mOmnibox.checkText(
                equalTo("test"), equalTo("testing is awesome"), equalTo("www.foobar.com"), 4, 18);
    }

    @Test
    @SmallTest
    public void testSetAutocompleteText_DuplicateText() {
        mOmnibox.setText("test");
        mOmnibox.setAutocompleteText("ingz", Optional.empty());
        mOmnibox.setAutocompleteText("ingz", Optional.empty());
        mOmnibox.setAutocompleteText("ingz", Optional.empty());
        mOmnibox.checkText(equalTo("test"), equalTo("testingz"), null, 4, 8);
    }

    @Test
    @SmallTest
    public void testSetAutocompleteTextWithAdditionalText_DuplicateText() {
        mOmnibox.setText("test");
        mOmnibox.setAutocompleteText("ingz", Optional.of("www.foo.com"));
        mOmnibox.setAutocompleteText("ingz", Optional.of("www.foo.com"));
        mOmnibox.setAutocompleteText("ingz", Optional.of("www.foo.com"));
        mOmnibox.checkText(equalTo("test"), equalTo("testingz"), equalTo("www.foo.com"), 4, 8);
    }

    @Test
    @SmallTest
    public void testUrlDirection() throws TimeoutException {
        setTextAndVerifyTextDirection("ل", View.LAYOUT_DIRECTION_RTL);
        setTextAndVerifyTextDirection("a", View.LAYOUT_DIRECTION_LTR);
        setTextAndVerifyTextDirection("للك", View.LAYOUT_DIRECTION_RTL);
        setTextAndVerifyTextDirection("f", View.LAYOUT_DIRECTION_LTR);
    }

    @Test
    @SmallTest
    public void testAutocompleteUpdatedOnDefocus() throws InterruptedException {
        sActivityTestRule.loadUrl(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
        mOmnibox.requestFocus();
        mOmnibox.typeText("test", false);
        mOmnibox.clearFocus();
        mOmnibox.checkText(equalTo(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL), null);
    }

    @Test
    @SmallTest
    public void typingStarted_emittedOncePerFocus() {
        var listener = mock(Runnable.class);

        mOmnibox.clearFocus();
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        mUrlBar.setTypingStartedListener(listener);
        mOmnibox.requestFocus();

        verifyNoInteractions(listener);

        // Verify that UrlBar emits a single Typing Started event.
        mOmnibox.typeText("a", false);
        verify(listener).run();

        clearInvocations(listener);

        // Verify no subsequent events emitted.
        mOmnibox.typeText("a", false);
        verifyNoInteractions(listener);
    }

    @Test
    @SmallTest
    public void typingStarted_emittedOnceEveryFocus() {
        var listener = mock(Runnable.class);

        mOmnibox.clearFocus();
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        mUrlBar.setTypingStartedListener(listener);
        mOmnibox.requestFocus();

        verifyNoInteractions(listener);

        // Verify that UrlBar emits a single Typing Started event.
        mOmnibox.typeText("a", false);
        verify(listener).run();

        mOmnibox.clearFocus();
        clearInvocations(listener);
        mOmnibox.requestFocus();

        // Verify no subsequent events emitted.
        mOmnibox.typeText("a", false);
        verify(listener).run();
    }

    @Test
    @SmallTest
    @RequiresRestart("crbug.com/358170962")
    public void typingStarted_notEmittedForNonTypingCharacters() {
        var listener = mock(Runnable.class);

        mOmnibox.clearFocus();
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        mUrlBar.setTypingStartedListener(listener);
        mOmnibox.requestFocus();

        var nonTypingKeys =
                List.of(
                        KeyEvent.KEYCODE_F1,
                        KeyEvent.KEYCODE_TAB,
                        KeyEvent.KEYCODE_SHIFT_LEFT,
                        KeyEvent.KEYCODE_DEL,
                        KeyEvent.KEYCODE_PAGE_UP,
                        KeyEvent.KEYCODE_DPAD_LEFT);

        for (int key : nonTypingKeys) {
            mOmnibox.sendKey(key);
            verifyNoInteractions(listener);
        }
    }

    @Test
    @SmallTest
    public void typingStarted_clipboardPasteTriggersTypingStarted() {
        var listener = mock(Runnable.class);

        mOmnibox.clearFocus();
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        mUrlBar.setTypingStartedListener(listener);
        mOmnibox.requestFocus();

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Clipboard.getInstance().setText("");
                    // Paste directly. This is because Keyboard paste normally goes through an IME,
                    // which
                    // requires a lengthier process, rendering test flaky.
                    mUrlBar.onTextContextMenuItem(android.R.id.paste);
                });
        verifyNoInteractions(listener);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Clipboard.getInstance().setText("asdf");
                    // Paste directly. This is because Keyboard paste normally goes through an IME,
                    // which
                    // requires a lengthier process, rendering test flaky.
                    mUrlBar.onTextContextMenuItem(android.R.id.paste);
                });
        verify(listener).run();
    }
}