blob: ae98c3ba0c0ca310971140b2319da9ea7ae0cb56 [file] [log] [blame]
// 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 android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
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.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.platform.app.InstrumentationRegistry;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
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.OmniboxSuggestionUiType;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestionsDropdown;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestionsDropdownAdapter;
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.content_public.browser.test.util.KeyUtils;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
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;
/** Maximum time to wait for the Omnibox content to stabilize. */
private static final int MAX_TIME_TO_POLL_MS = 300;
/** Interval between subsequent polls. */
private static final int POLL_INTERVAL_MS = 30;
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;
/**
* 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() {
TestThreadUtils.runOnUiThreadBlocking(() -> mUrlBar.setUrlTextChangeListener(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.
*
* Note: this is known to cause issues with tests that run animations.
* In the event you are running into flakes that concentrate around this call, please consider
* adding DisableAnimationsTestRule to your test suite.
*
* @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));
}, MAX_TIME_TO_POLL_MS, POLL_INTERVAL_MS);
}
/**
* Determines whether the UrlBar currently has focus.
* @return Whether the UrlBar has focus.
*/
public boolean getFocus() {
return TestThreadUtils.runOnUiThreadBlockingNoException(() -> 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));
}, MAX_TIME_TO_POLL_MS, POLL_INTERVAL_MS);
TestThreadUtils.runOnUiThreadBlockingNoException(() -> mUrlBar.requestFocus());
waitAnimationsComplete();
checkFocus(true);
}
/**
* Clear the Omnibox focus and wait until keyboard is dismissed.
* Expects the Omnibox to be focused before the call.
*/
public void clearFocus() {
sendKey(KeyEvent.KEYCODE_BACK);
checkFocus(false);
}
/**
* Set the suggestions to the Omnibox to display.
*
* @param autocompleteResult The set of suggestions will be displayed on the Omnibox dropdown
* list.
* @param inlineAutocompleteText the inline-autocomplete text.
*/
public void setSuggestions(
AutocompleteResult autocompleteResult, String inlineAutocompleteText) {
checkFocus(true);
TestThreadUtils.runOnUiThreadBlocking(() -> {
OnSuggestionsReceivedListener listener =
mAutocomplete.getSuggestionsReceivedListenerForTest();
listener.onSuggestionsReceived(autocompleteResult, inlineAutocompleteText, 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));
}, MAX_TIME_TO_POLL_MS, POLL_INTERVAL_MS);
}
/**
* 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(() -> {
ModelList currentModels =
mLocationBar.getAutocompleteCoordinator().getSuggestionModelListForTest();
for (int i = 0; i < currentModels.size(); i++) {
DropdownItemViewInfo info = (DropdownItemViewInfo) currentModels.get(i);
if (filter.apply(info) && getSuggestionViewForIndex(i) != null) {
result.set(new SuggestionInfo<T>(i, info.type, mAutocomplete.getSuggestionAt(i),
info.model, getSuggestionViewForIndex(i)));
return true;
}
}
return false;
}, MAX_TIME_TO_POLL_MS, POLL_INTERVAL_MS);
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 TestThreadUtils.runOnUiThreadBlockingNoException(() -> {
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;
});
}
/**
* Highligh suggestion at a specific index.
*
* @param index The index of the suggestion to be highlighted.
*/
public void focusSuggestion(int index) {
TestThreadUtils.runOnUiThreadBlocking(() -> {
OmniboxSuggestionsDropdownAdapter adapter =
(OmniboxSuggestionsDropdownAdapter) mLocationBar.getAutocompleteCoordinator()
.getSuggestionsDropdownForTest()
.getAdapter();
adapter.setSelectedViewIndex(index);
});
}
/**
* 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);
TestThreadUtils.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(final int keyCode) {
checkFocus(true);
TestThreadUtils.runOnUiThreadBlocking(
() -> KeyUtils.singleKeyEventView(mInstrumentation, mUrlBar, keyCode));
}
/**
* 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);
TestThreadUtils.runOnUiThreadBlocking(() -> {
mUrlBar.setText(userText);
// Push this to the model as well.
mUrlBar.setAutocompleteText(userText, "");
});
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);
TestThreadUtils.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.
*/
public void setAutocompleteText(String autocompleteText) {
checkFocus(true);
AtomicReference<String> userText = new AtomicReference<>();
TestThreadUtils.runOnUiThreadBlocking(() -> {
userText.set(mUrlBar.getTextWithoutAutocomplete());
mUrlBar.setAutocompleteText(userText.get(), autocompleteText);
});
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, null);
}
/**
* Verify the text content of the Omnibox.
*
* @param textMatcher Matcher checking the content of the Omnibox.
* @param autocompleteTextMatcher Optional Matcher for autocompletion.
* @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, int autocompleteSelectionStart,
int autocompleteSelectionEnd) {
checkText(textMatcher, autocompleteTextMatcher, 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 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<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 (autocompleteSelectionStart != null) {
Criteria.checkThat("Autocomplete Selection start", mUrlBar.getSelectionStart(),
autocompleteSelectionStart);
}
// TODO(crbug.com/1289474): 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);
}
}, MAX_TIME_TO_POLL_MS, POLL_INTERVAL_MS);
}
/**
* @return The text contents of the omnibox (without the Autocomplete part).
*/
public String getText() {
return TestThreadUtils.runOnUiThreadBlockingNoException(
() -> 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));
}, MAX_TIME_TO_POLL_MS, POLL_INTERVAL_MS);
}
/**
* Set the Composing text in the Omnibox.
*
* 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);
TestThreadUtils.runOnUiThreadBlocking(() -> {
InputConnection conn = mUrlBar.getInputConnection();
conn.setComposingRegion(composingRegionStart, composingRegionEnd);
conn.setComposingText(composingText, /* newCursorPosition=*/0);
});
}
}