blob: b21ec85b428cb3c129b2732312a902523dadd695 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser.input;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.res.Configuration;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.text.Editable;
import android.text.Selection;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.content.browser.ContentViewCore;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.content.browser.test.util.DOMUtils;
import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper;
import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper.Range;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_shell_apk.ContentShellTestBase;
import org.chromium.ui.base.ime.TextInputType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
/**
* Integration tests for text input using cases based on fixed regressions.
*/
public class ImeTest extends ContentShellTestBase {
private static final String DATA_URL = UrlUtils.encodeHtmlDataUri(
"<html><head><meta name=\"viewport\""
+ "content=\"width=device-width\" /></head>"
+ "<body><form action=\"about:blank\">"
+ "<input id=\"input_text\" type=\"text\" /><br/></form><form>"
+ "<br/><input id=\"input_radio\" type=\"radio\" style=\"width:50px;height:50px\" />"
+ "<br/><textarea id=\"textarea\" rows=\"4\" cols=\"20\"></textarea>"
+ "<br/><textarea id=\"textarea2\" rows=\"4\" cols=\"20\" autocomplete=\"off\">"
+ "</textarea>"
+ "<br/><input id=\"input_number1\" type=\"number\" /><br/>"
+ "<br/><input id=\"input_number2\" type=\"number\" /><br/>"
+ "<br/><p><span id=\"plain_text\">This is Plain Text One</span></p>"
+ "</form></body></html>");
private TestAdapterInputConnection mConnection;
private TestAdapterInputConnectionFactory mConnectionFactory;
private ImeAdapter mImeAdapter;
private ContentViewCore mContentViewCore;
private WebContents mWebContents;
private TestCallbackHelperContainer mCallbackContainer;
private TestInputMethodManagerWrapper mInputMethodManagerWrapper;
@Override
public void setUp() throws Exception {
super.setUp();
launchContentShellWithUrl(DATA_URL);
assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
mContentViewCore = getContentViewCore();
mWebContents = getWebContents();
mInputMethodManagerWrapper = new TestInputMethodManagerWrapper(mContentViewCore);
getImeAdapter().setInputMethodManagerWrapperForTest(mInputMethodManagerWrapper);
assertEquals(0, mInputMethodManagerWrapper.getShowSoftInputCounter());
mConnectionFactory = new TestAdapterInputConnectionFactory();
mContentViewCore.setAdapterInputConnectionFactory(mConnectionFactory);
mCallbackContainer = new TestCallbackHelperContainer(mContentViewCore);
// TODO(aurimas) remove this wait once crbug.com/179511 is fixed.
assertWaitForPageScaleFactorMatch(1);
assertTrue(DOMUtils.waitForNonZeroNodeBounds(
mWebContents, "input_text"));
DOMUtils.clickNode(this, mContentViewCore, "input_text");
assertWaitForKeyboardStatus(true);
mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
mImeAdapter = getImeAdapter();
waitAndVerifyStatesAndCalls(0, "", 0, 0, -1, -1);
waitForKeyboardStates(1, 0, 1, new Integer[] {TextInputType.TEXT});
assertEquals(0, mInputMethodManagerWrapper.getEditorInfo().initialSelStart);
assertEquals(0, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd);
resetAllStates();
}
private void assertNoFurtherStateUpdate(final int index) throws InterruptedException {
final List<TestImeState> states = mConnectionFactory.getImeStateList();
assertFalse(CriteriaHelper.pollForUIThreadCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return states.size() > index;
}
}));
}
@MediumTest
@Feature({"TextInput", "Main"})
public void testSetUpGeneratesNoFurtherStateUpdate() throws Throwable {
assertNoFurtherStateUpdate(0);
waitForKeyboardStates(0, 0, 0, new Integer[] {});
}
@MediumTest
@Feature({"TextInput", "Main"})
public void testKeyboardDismissedAfterClickingGo() throws Throwable {
setComposingText("hello", 1);
waitAndVerifyStatesAndCalls(0, "hello", 5, 5, 0, 5);
performGo(mCallbackContainer);
assertWaitForKeyboardStatus(false);
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testCommitWhileComposingText() throws Throwable {
setComposingText("h", 1);
waitAndVerifyStates(0, "h", 1, 1, 0, 1);
setComposingText("he", 1);
waitAndVerifyStates(1, "he", 2, 2, 0, 2);
setComposingText("hel", 1);
waitAndVerifyStates(2, "hel", 3, 3, 0, 3);
commitText("hel", 1);
waitAndVerifyStates(3, "hel", 3, 3, -1, -1);
}
@SmallTest
@Feature({"TextInput"})
public void testImeCopy() throws Exception {
commitText("hello", 1);
waitAndVerifyStates(0, "hello", 5, 5, -1, -1);
setSelection(2, 5);
waitAndVerifyStates(1, "hello", 2, 5, -1, -1);
copy();
assertClipboardContents(getActivity(), "llo");
}
@SmallTest
@Feature({"TextInput"})
public void testEnterTextAndRefocus() throws Exception {
commitText("hello", 1);
waitAndVerifyStatesAndCalls(0, "hello", 5, 5, -1, -1);
DOMUtils.clickNode(this, mContentViewCore, "input_radio");
assertWaitForKeyboardStatus(false);
DOMUtils.clickNode(this, mContentViewCore, "input_text");
assertWaitForKeyboardStatus(true);
assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelStart);
assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd);
}
@SmallTest
@Feature({"TextInput"})
public void testShowAndHideSoftInput() throws Exception {
focusElement("input_radio", false);
// hideSoftKeyboard(), restartInput()
waitForKeyboardStates(0, 1, 1, new Integer[] {});
// showSoftInput(), restartInput()
focusElement("input_number1");
waitForKeyboardStates(1, 1, 2, new Integer[] {TextInputType.NUMBER});
focusElement("input_number2");
// Hide should never be called here. Otherwise we will see a flicker. Restarted to
// reset internal states to handle the new input form.
waitForKeyboardStates(2, 1, 3, new Integer[] {TextInputType.NUMBER, TextInputType.NUMBER});
focusElement("input_text");
// showSoftInput() on input_text. restartInput() on input_number1 due to focus change,
// and restartInput() on input_text later.
// TODO(changwan): reduce unnecessary restart input.
waitForKeyboardStates(3, 1, 5, new Integer[] {TextInputType.NUMBER, TextInputType.NUMBER,
TextInputType.NUMBER, TextInputType.TEXT});
focusElement("input_radio", false);
// hideSoftInput(), restartInput()
waitForKeyboardStates(3, 2, 6, new Integer[] {TextInputType.NUMBER, TextInputType.NUMBER,
TextInputType.NUMBER, TextInputType.TEXT});
}
private void waitForKeyboardStates(int show, int hide, int restart, Integer[] history)
throws InterruptedException {
final String expected = stringifyKeyboardStates(show, hide, restart, history);
assertTrue("Expected: {" + expected + "}, Actual: {" + getKeyboardStates() + "}",
CriteriaHelper.pollForUIThreadCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return expected.equals(getKeyboardStates());
}
}));
}
private void resetAllStates() {
mInputMethodManagerWrapper.resetCounters();
mConnectionFactory.clearTextInputTypeHistory();
resetUpdateStateList();
}
private String getKeyboardStates() {
try {
return ThreadUtils.runOnUiThreadBlocking(new Callable<String>() {
@Override
public String call() throws Exception {
int showCount = mInputMethodManagerWrapper.getShowSoftInputCounter();
int hideCount = mInputMethodManagerWrapper.getHideSoftInputCounter();
int restartCount = mInputMethodManagerWrapper.getRestartInputCounter();
Integer[] history = mConnectionFactory.getTextInputTypeHistory();
return stringifyKeyboardStates(showCount, hideCount, restartCount, history);
}
});
} catch (ExecutionException e) {
e.printStackTrace();
return null;
}
}
private String stringifyKeyboardStates(int show, int hide, int restart, Integer[] history) {
return "show count: " + show + ", hide count: " + hide + ", restart count: " + restart
+ ", input type history: " + Arrays.deepToString(history);
}
@SmallTest
@Feature({"TextInput"})
public void testKeyboardNotDismissedAfterCopySelection() throws Exception {
commitText("Sample Text", 1);
waitAndVerifyStatesAndCalls(0, "Sample Text", 11, 11, -1, -1);
DOMUtils.clickNode(this, mContentViewCore, "input_text");
assertWaitForKeyboardStatus(true);
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
selectAll();
copy();
assertWaitForKeyboardStatus(true);
assertEquals(11, Selection.getSelectionEnd(mContentViewCore.getEditableForTest()));
}
@SmallTest
@Feature({"TextInput"})
public void testImeNotDismissedAfterCutSelection() throws Exception {
commitText("Sample Text", 1);
waitAndVerifyStatesAndCalls(0, "Sample Text", 11, 11, -1, -1);
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertWaitForSelectActionBarStatus(true);
assertWaitForKeyboardStatus(true);
cut();
assertWaitForKeyboardStatus(true);
assertWaitForSelectActionBarStatus(false);
}
@SmallTest
@Feature({"TextInput"})
public void testImeNotShownOnLongPressingEmptyInput() throws Exception {
DOMUtils.focusNode(mWebContents, "input_radio");
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertWaitForKeyboardStatus(false);
commitText("Sample Text", 1);
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertWaitForKeyboardStatus(true);
}
@SmallTest
@Feature({"TextInput"})
public void testSelectActionBarShownOnLongPressingInput() throws Exception {
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertWaitForSelectActionBarStatus(false);
commitText("Sample Text", 1);
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertWaitForSelectActionBarStatus(true);
}
@SmallTest
@Feature({"TextInput"})
public void testLongPressInputWhileComposingText() throws Exception {
assertWaitForSelectActionBarStatus(false);
setComposingText("Sample Text", 1);
waitAndVerifyStatesAndCalls(0, "Sample Text", 11, 11, 0, 11);
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertWaitForSelectActionBarStatus(true);
// Long press will first change selection region, and then trigger IME app to show up.
// See RenderFrameImpl::didChangeSelection() and RenderWidget::didHandleGestureEvent().
waitAndVerifyStatesAndCalls(1, "Sample Text", 7, 11, 0, 11);
// Now IME app wants to finish composing text because an external selection
// change has been detected. At least Google Latin IME and Samsung IME
// behave this way.
finishComposingText();
waitAndVerifyStatesAndCalls(2, "Sample Text", 7, 11, -1, -1);
}
@SmallTest
@Feature({"TextInput"})
public void testImeShownWhenLongPressOnAlreadySelectedText() throws Exception {
assertWaitForSelectActionBarStatus(false);
commitText("Sample Text", 1);
int showCount = mInputMethodManagerWrapper.getShowSoftInputCounter();
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertWaitForSelectActionBarStatus(true);
assertEquals(showCount + 1, mInputMethodManagerWrapper.getShowSoftInputCounter());
// Now long press again. Selection region remains the same, but the logic
// should trigger IME to show up. Note that Android does not provide show /
// hide status of IME, so we will just check whether showIme() has been triggered.
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
final int newCount = showCount + 2;
assertTrue(CriteriaHelper.pollForUIThreadCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return newCount == mInputMethodManagerWrapper.getShowSoftInputCounter();
}
}));
}
private void attachPhysicalKeyboard() {
Configuration hardKeyboardConfig =
new Configuration(mContentViewCore.getContext().getResources().getConfiguration());
hardKeyboardConfig.keyboard = Configuration.KEYBOARD_QWERTY;
hardKeyboardConfig.keyboardHidden = Configuration.KEYBOARDHIDDEN_YES;
hardKeyboardConfig.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO;
onConfigurationChanged(hardKeyboardConfig);
}
private void detachPhysicalKeyboard() {
Configuration softKeyboardConfig =
new Configuration(mContentViewCore.getContext().getResources().getConfiguration());
softKeyboardConfig.keyboard = Configuration.KEYBOARD_NOKEYS;
softKeyboardConfig.keyboardHidden = Configuration.KEYBOARDHIDDEN_NO;
softKeyboardConfig.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES;
onConfigurationChanged(softKeyboardConfig);
}
private void onConfigurationChanged(final Configuration config) {
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
mContentViewCore.onConfigurationChanged(config);
}
});
}
@SmallTest
@Feature({"TextInput"})
public void testPhysicalKeyboard_AttachDetach() throws Exception {
attachPhysicalKeyboard();
// We still call showSoftKeyboard, which will be ignored by physical keyboard.
waitForKeyboardStates(1, 0, 1, new Integer[] {TextInputType.TEXT});
setComposingText("a", 1);
waitForKeyboardStates(1, 0, 1, new Integer[] {TextInputType.TEXT});
detachPhysicalKeyboard();
// Now we really show soft keyboard. We also call restartInput when configuration changes.
waitForKeyboardStates(2, 0, 2, new Integer[] {TextInputType.TEXT, TextInputType.TEXT});
// Reload the page, then the focus will be lost.
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
getActivity().getActiveShell().loadUrl(DATA_URL);
}
});
detachPhysicalKeyboard();
// We should not show soft keyboard here because focus has been lost.
waitForKeyboardStates(2, 1, 2, new Integer[] {TextInputType.TEXT, TextInputType.TEXT});
}
@SmallTest
@Feature({"TextInput"})
public void testSelectActionBarClearedOnTappingInput() throws Exception {
commitText("Sample Text", 1);
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertWaitForKeyboardStatus(true);
assertWaitForSelectActionBarStatus(true);
DOMUtils.clickNode(this, mContentViewCore, "input_text");
assertWaitForSelectActionBarStatus(false);
}
@SmallTest
@Feature({"TextInput"})
public void testSelectActionBarClearedOnTappingOutsideInput() throws Exception {
commitText("Sample Text", 1);
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertWaitForKeyboardStatus(true);
assertWaitForSelectActionBarStatus(true);
DOMUtils.clickNode(this, mContentViewCore, "input_radio");
assertWaitForKeyboardStatus(false);
assertWaitForSelectActionBarStatus(false);
}
@SmallTest
@Feature({"TextInput"})
public void testImeNotShownOnLongPressingDifferentEmptyInputs() throws Exception {
DOMUtils.focusNode(mWebContents, "input_radio");
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertWaitForKeyboardStatus(false);
DOMUtils.longPressNode(this, mContentViewCore, "textarea");
assertWaitForKeyboardStatus(false);
}
@SmallTest
@Feature({"TextInput"})
public void testImeStaysOnLongPressingDifferentNonEmptyInputs() throws Exception {
DOMUtils.focusNode(mWebContents, "input_text");
assertWaitForKeyboardStatus(true);
commitText("Sample Text", 1);
DOMUtils.focusNode(mWebContents, "textarea");
commitText("Sample Text", 1);
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertWaitForKeyboardStatus(true);
DOMUtils.longPressNode(this, mContentViewCore, "textarea");
assertWaitForKeyboardStatus(true);
}
@SmallTest
@Feature({"TextInput"})
public void testImeCut() throws Exception {
commitText("snarful", 1);
waitAndVerifyStatesAndCalls(0, "snarful", 7, 7, -1, -1);
setSelection(1, 5);
waitAndVerifyStatesAndCalls(1, "snarful", 1, 5, -1, -1);
cut();
waitAndVerifyStatesAndCalls(2, "sul", 1, 1, -1, -1);
assertClipboardContents(getActivity(), "narf");
}
@SmallTest
@Feature({"TextInput"})
public void testImePaste() throws Exception {
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
ClipboardManager clipboardManager =
(ClipboardManager) getActivity().getSystemService(
Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClip(ClipData.newPlainText("blarg", "blarg"));
}
});
paste();
waitAndVerifyStatesAndCalls(0, "blarg", 5, 5, -1, -1);
setSelection(3, 5);
waitAndVerifyStatesAndCalls(1, "blarg", 3, 5, -1, -1);
paste();
// Paste is a two step process when there is a non-zero selection.
waitAndVerifyStates(2, "bla", 3, 3, -1, -1);
waitAndVerifyStatesAndCalls(3, "blablarg", 8, 8, -1, -1);
paste();
waitAndVerifyStatesAndCalls(4, "blablargblarg", 13, 13, -1, -1);
}
@SmallTest
@Feature({"TextInput"})
public void testImeSelectAndUnSelectAll() throws Exception {
commitText("hello", 1);
waitAndVerifyStatesAndCalls(0, "hello", 5, 5, -1, -1);
selectAll();
waitAndVerifyStatesAndCalls(1, "hello", 0, 5, -1, -1);
unselect();
assertWaitForKeyboardStatus(false);
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testShowImeIfNeeded() throws Throwable {
// showImeIfNeeded() is now implicitly called by the updated focus
// heuristic so no need to call explicitly. http://crbug.com/371927
DOMUtils.focusNode(mWebContents, "input_radio");
assertWaitForKeyboardStatus(false);
DOMUtils.focusNode(mWebContents, "input_text");
assertWaitForKeyboardStatus(true);
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testFinishComposingText() throws Throwable {
focusElementAndWaitForStateUpdate("textarea");
commitText("hllo", 1);
waitAndVerifyStatesAndCalls(0, "hllo", 4, 4, -1, -1);
commitText(" ", 1);
waitAndVerifyStatesAndCalls(1, "hllo ", 5, 5, -1, -1);
setSelection(1, 1);
waitAndVerifyStatesAndCalls(2, "hllo ", 1, 1, -1, -1);
setComposingRegion(0, 4);
waitAndVerifyStatesAndCalls(3, "hllo ", 1, 1, 0, 4);
finishComposingText();
waitAndVerifyStatesAndCalls(4, "hllo ", 1, 1, -1, -1);
commitText("\n", 1);
waitAndVerifyStatesAndCalls(5, "h\nllo ", 2, 2, -1, -1);
}
/*
@SmallTest
@Feature({"TextInput", "Main"})
http://crbug.com/445499
*/
public void testDeleteText() throws Throwable {
focusElement("textarea");
// The calls below are a reflection of what the stock Google Keyboard (Andr
// when the noted key is touched on screen.
// H
resetUpdateStateList();
setComposingText("h", 1);
assertUpdateStateCall(1000);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
// O
resetUpdateStateList();
setComposingText("ho", 1);
assertUpdateStateCall(1000);
assertEquals("ho", mConnection.getTextBeforeCursor(9, 0));
resetUpdateStateList();
setComposingText("h", 1);
assertUpdateStateCall(1000);
setComposingRegion(0, 1);
setComposingText("h", 1);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
// I
setComposingText("hi", 1);
assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
// SPACE
commitText("hi", 1);
commitText(" ", 1);
assertEquals("hi ", mConnection.getTextBeforeCursor(9, 0));
// DEL
deleteSurroundingText(1, 0);
setComposingRegion(0, 2);
assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
setComposingText("h", 1);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
commitText("", 1);
assertEquals("", mConnection.getTextBeforeCursor(9, 0));
// DEL (on empty input)
deleteSurroundingText(1, 0); // DEL on empty still sends 1,0
assertEquals("", mConnection.getTextBeforeCursor(9, 0));
}
/*
@SmallTest
@Feature({"TextInput", "Main"})
*/
public void testSwipingText() throws Throwable {
focusElement("textarea");
// The calls below are a reflection of what the stock Google Keyboard (Android 4.4) sends
// when the word is swiped on the soft keyboard. Exercise care when altering to make sure
// that the test reflects reality. If this test breaks, it's possible that code has
// changed and different calls need to be made instead.
// "three"
resetUpdateStateList();
setComposingText("three", 1);
assertUpdateStateCall(1000);
assertEquals("three", mConnection.getTextBeforeCursor(99, 0));
// "word"
commitText("three", 1);
commitText(" ", 1);
setComposingText("word", 1);
resetUpdateStateList();
assertUpdateStateCall(1000);
assertEquals("three word", mConnection.getTextBeforeCursor(99, 0));
// "test"
commitText("word", 1);
commitText(" ", 1);
resetUpdateStateList();
setComposingText("test", 1);
assertUpdateStateCall(1000);
assertEquals("three word test", mConnection.getTextBeforeCursor(99, 0));
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testDeleteMultiCharacterCodepoint() throws Throwable {
// This smiley is a multi character codepoint.
final String smiley = "\uD83D\uDE0A";
commitText(smiley, 1);
waitAndVerifyStatesAndCalls(0, smiley, 2, 2, -1, -1);
// DEL, sent via dispatchKeyEvent like it is in Android WebView or a physical keyboard.
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
waitAndVerifyStatesAndCalls(1, "", 0, 0, -1, -1);
// Make sure that we accept further typing after deleting the smiley.
setComposingText("s", 1);
waitAndVerifyStatesAndCalls(2, "s", 1, 1, 0, 1);
setComposingText("sm", 1);
waitAndVerifyStatesAndCalls(3, "sm", 2, 2, 0, 2);
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testBackspaceKeycode() throws Throwable {
focusElement("textarea");
// H
resetUpdateStateList();
commitText("h", 1);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(1000);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
// O
resetUpdateStateList();
commitText("o", 1);
assertEquals("ho", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(1000);
assertEquals("ho", mConnection.getTextBeforeCursor(9, 0));
// DEL, sent via dispatchKeyEvent like it is in Android WebView or a physical keyboard.
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
// DEL
resetUpdateStateList();
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(1000);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testRepeatBackspaceKeycode() throws Throwable {
focusElement("textarea");
// H
resetUpdateStateList();
commitText("h", 1);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(1000);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
// O
resetUpdateStateList();
commitText("o", 1);
assertEquals("ho", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(1000);
assertEquals("ho", mConnection.getTextBeforeCursor(9, 0));
// Multiple keydowns should each delete one character (this is for physical keyboard
// key-repeat).
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
// DEL
resetUpdateStateList();
assertEquals("", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(1000);
assertEquals("", mConnection.getTextBeforeCursor(9, 0));
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testPhysicalKeyboard() throws Throwable {
focusElementAndWaitForStateUpdate("textarea");
// Type 'a' using a physical keyboard.
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A));
waitAndVerifyStatesAndCalls(0, "a", 1, 1, -1, -1);
// Type 'enter' key.
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
waitAndVerifyStatesAndCalls(1, "a\n\n", 2, 2, -1, -1);
// Type 'b'.
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_B));
waitAndVerifyStatesAndCalls(2, "a\nb", 3, 3, -1, -1);
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testPhysicalKeyboard_AccentKeyCodes() throws Throwable {
focusElementAndWaitForStateUpdate("textarea");
// h
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_H));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_H));
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
waitAndVerifyStatesAndCalls(0, "h", 1, 1, -1, -1);
// ALT-i (circumflex accent key on virtual keyboard)
dispatchKeyEvent(new KeyEvent(
0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
assertUpdateStateCall(1000);
assertEquals("hˆ", mConnection.getTextBeforeCursor(9, 0));
dispatchKeyEvent(new KeyEvent(
0, 0, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
assertEquals("hˆ", mConnection.getTextBeforeCursor(9, 0));
waitAndVerifyStatesAndCalls(1, "hˆ", 2, 2, 1, 2);
// o
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_O));
assertUpdateStateCall(1000);
assertEquals("hô", mConnection.getTextBeforeCursor(9, 0));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_O));
assertEquals("hô", mConnection.getTextBeforeCursor(9, 0));
waitAndVerifyStatesAndCalls(2, "hô", 2, 2, -1, -1);
// ALT-i
dispatchKeyEvent(new KeyEvent(
0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
assertUpdateStateCall(1000);
dispatchKeyEvent(new KeyEvent(
0, 0, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
assertEquals("hôˆ", mConnection.getTextBeforeCursor(9, 0));
waitAndVerifyStatesAndCalls(3, "hôˆ", 3, 3, 2, 3);
// ALT-i again should have no effect
dispatchKeyEvent(new KeyEvent(
0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
assertUpdateStateCall(1000);
dispatchKeyEvent(new KeyEvent(
0, 0, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
assertEquals("hôˆ", mConnection.getTextBeforeCursor(9, 0));
// b (cannot be accented, should just appear after)
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B));
assertUpdateStateCall(1000);
assertEquals("hôˆb", mConnection.getTextBeforeCursor(9, 0));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_B));
assertEquals("hôˆb", mConnection.getTextBeforeCursor(9, 0));
// A transitional state due to finishComposingText.
waitAndVerifyStates(4, "hôˆ", 3, 3, -1, -1);
waitAndVerifyStatesAndCalls(5, "hôˆb", 4, 4, -1, -1);
// ALT-i
dispatchKeyEvent(new KeyEvent(
0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
assertUpdateStateCall(1000);
dispatchKeyEvent(new KeyEvent(
0, 0, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
assertEquals("hôˆbˆ", mConnection.getTextBeforeCursor(9, 0));
waitAndVerifyStatesAndCalls(6, "hôˆbˆ", 5, 5, 4, 5);
// Backspace
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
assertUpdateStateCall(1000);
assertEquals("hôˆb", mConnection.getTextBeforeCursor(9, 0));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
assertEquals("hôˆb", mConnection.getTextBeforeCursor(9, 0));
// A transitional state due to finishComposingText in deleteSurroundingTextImpl.
waitAndVerifyStates(7, "hôˆbˆ", 5, 5, -1, -1);
waitAndVerifyStatesAndCalls(8, "hôˆb", 4, 4, -1, -1);
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testSetComposingRegionOutOfBounds() throws Throwable {
focusElement("textarea");
setComposingText("hello", 1);
setComposingRegion(0, 0);
setComposingRegion(0, 9);
setComposingRegion(9, 0);
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testEnterKey_AfterCommitText() throws Throwable {
focusElementAndWaitForStateUpdate("textarea");
commitText("hello", 1);
waitAndVerifyStatesAndCalls(0, "hello", 5, 5, -1, -1);
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
// TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
// The second new line is not a user visible/editable one, it is a side-effect of Blink
// using <br> internally. This only happens when \n is at the end.
waitAndVerifyStatesAndCalls(1, "hello\n\n", 6, 6, -1, -1);
commitText("world", 1);
waitAndVerifyStatesAndCalls(2, "hello\nworld", 11, 11, -1, -1);
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testEnterKey_WhileComposingText() throws Throwable {
focusElementAndWaitForStateUpdate("textarea");
setComposingText("hello", 1);
waitAndVerifyStatesAndCalls(0, "hello", 5, 5, 0, 5);
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
// TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
// A transitional state due to finishComposingText.
waitAndVerifyStates(1, "hello", 5, 5, -1, -1);
// The second new line is not a user visible/editable one, it is a side-effect of Blink
// using <br> internally. This only happens when \n is at the end.
waitAndVerifyStatesAndCalls(2, "hello\n\n", 6, 6, -1, -1);
commitText("world", 1);
waitAndVerifyStatesAndCalls(3, "hello\nworld", 11, 11, -1, -1);
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testDpadKeyCodesWhileSwipingText() throws Throwable {
focusElement("textarea");
// DPAD_CENTER should cause keyboard to appear
resetUpdateStateList();
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER));
assertUpdateStateCall(1000);
}
@SmallTest
@Feature({"TextInput"})
public void testPastePopupShowAndHide() throws Throwable {
commitText("hello", 1);
waitAndVerifyStatesAndCalls(0, "hello", 5, 5, -1, -1);
selectAll();
waitAndVerifyStatesAndCalls(1, "hello", 0, 5, -1, -1);
cut();
waitAndVerifyStatesAndCalls(2, "", 0, 0, -1, -1);
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertTrue(CriteriaHelper.pollForUIThreadCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return mContentViewCore.isPastePopupShowing();
}
}));
DOMUtils.clickNode(this, mContentViewCore, "input_text");
assertWaitForKeyboardStatus(true);
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
setComposingText("h", 1);
assertTrue(CriteriaHelper.pollForUIThreadCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return !mContentViewCore.isPastePopupShowing();
}
}));
assertFalse(mContentViewCore.hasInsertion());
}
@SmallTest
@Feature({"TextInput"})
public void testSelectionClearedOnKeyEvent() throws Throwable {
commitText("hello", 1);
waitAndVerifyStatesAndCalls(0, "hello", 5, 5, -1, -1);
DOMUtils.clickNode(this, mContentViewCore, "input_text");
assertWaitForKeyboardStatus(true);
DOMUtils.longPressNode(this, mContentViewCore, "input_text");
assertWaitForSelectActionBarStatus(true);
setComposingText("h", 1);
assertWaitForSelectActionBarStatus(false);
assertFalse(mContentViewCore.hasSelection());
}
@SmallTest
@Feature({"TextInput"})
public void testTextHandlesPreservedWithDpadNavigation() throws Throwable {
DOMUtils.longPressNode(this, mContentViewCore, "plain_text");
assertWaitForSelectActionBarStatus(true);
assertTrue(mContentViewCore.hasSelection());
dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN));
assertWaitForSelectActionBarStatus(true);
assertTrue(mContentViewCore.hasSelection());
}
@MediumTest
@Feature({"TextInput"})
public void testRestartInputWhileComposingText() throws Throwable {
setComposingText("abc", 1);
waitAndVerifyStatesAndCalls(0, "abc", 3, 3, 0, 3);
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
mConnection.restartInput();
}
});
// We don't do anything when input gets restarted. But we depend on Android's
// InputMethodManager and/or input methods to call finishComposingText() in setting
// current input connection as active or finishing the current input connection.
assertNoFurtherStateUpdate(1);
}
private void performGo(TestCallbackHelperContainer testCallbackHelperContainer)
throws Throwable {
final AdapterInputConnection inputConnection = mConnection;
handleBlockingCallbackAction(
testCallbackHelperContainer.getOnPageFinishedHelper(),
new Runnable() {
@Override
public void run() {
inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO);
}
});
}
private void assertWaitForKeyboardStatus(final boolean show) throws InterruptedException {
assertTrue(CriteriaHelper.pollForUIThreadCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return show == mInputMethodManagerWrapper.isShowWithoutHideOutstanding()
&& (!show || getAdapterInputConnection() != null);
}
}));
}
private void assertWaitForSelectActionBarStatus(
final boolean show) throws InterruptedException {
assertTrue(CriteriaHelper.pollForUIThreadCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return show == mContentViewCore.isSelectActionBarShowing();
}
}));
}
private void waitAndVerifyStates(final int index, String text, final int selectionStart,
final int selectionEnd, final int compositionStart, final int compositionEnd)
throws InterruptedException {
final List<TestImeState> states = mConnectionFactory.getImeStateList();
assertTrue(CriteriaHelper.pollForUIThreadCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return states.size() > index;
}
}));
states.get(index).assertEqualState(
text, selectionStart, selectionEnd, compositionStart, compositionEnd);
}
private void waitAndVerifyStatesAndCalls(final int index, String text, final int selectionStart,
final int selectionEnd, final int compositionStart, final int compositionEnd)
throws InterruptedException {
waitAndVerifyStates(
index, text, selectionStart, selectionEnd, compositionStart, compositionEnd);
// Wait and verify calls to InputMethodManager.
final Range selection = new Range(selectionStart, selectionEnd);
final Range composition = new Range(compositionStart, compositionEnd);
assertTrue("Actual selection was: " + mInputMethodManagerWrapper.getSelection()
+ ", and actual composition was: "
+ mInputMethodManagerWrapper.getComposition(),
CriteriaHelper.pollForUIThreadCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return mInputMethodManagerWrapper.getSelection().equals(selection)
&& mInputMethodManagerWrapper.getComposition().equals(composition);
}
}));
}
private void resetUpdateStateList() {
mConnectionFactory.getImeStateList().clear();
}
private void assertUpdateStateCall(int maxms) throws Exception {
while (mConnectionFactory.getImeStateList().size() == 0 && maxms > 0) {
try {
Thread.sleep(50);
} catch (Exception e) {
// Not really a problem since we're just going to sleep again.
}
maxms -= 50;
}
assertTrue(mConnectionFactory.getImeStateList().size() > 0);
}
private void assertClipboardContents(final Activity activity, final String expectedContents)
throws InterruptedException {
assertTrue(CriteriaHelper.pollForUIThreadCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
ClipboardManager clipboardManager =
(ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = clipboardManager.getPrimaryClip();
return clip != null && clip.getItemCount() == 1
&& TextUtils.equals(clip.getItemAt(0).getText(), expectedContents);
}
}));
}
private ImeAdapter getImeAdapter() {
return mContentViewCore.getImeAdapterForTest();
}
private AdapterInputConnection getAdapterInputConnection() {
return mContentViewCore.getInputConnectionForTest();
}
private void copy() {
final WebContents webContents = mWebContents;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
webContents.copy();
}
});
}
private void cut() {
final WebContents webContents = mWebContents;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
webContents.cut();
}
});
}
private void paste() {
final WebContents webContents = mWebContents;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
webContents.paste();
}
});
}
private void selectAll() {
final WebContents webContents = mWebContents;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
webContents.selectAll();
}
});
}
private void unselect() {
final WebContents webContents = mWebContents;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
webContents.unselect();
}
});
}
private void commitText(final CharSequence text, final int newCursorPosition) {
final AdapterInputConnection connection = mConnection;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
connection.commitText(text, newCursorPosition);
}
});
}
private void setSelection(final int start, final int end) {
final AdapterInputConnection connection = mConnection;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
connection.setSelection(start, end);
}
});
}
private void setComposingRegion(final int start, final int end) {
final AdapterInputConnection connection = mConnection;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
connection.setComposingRegion(start, end);
}
});
}
private void setComposingText(final CharSequence text, final int newCursorPosition) {
final AdapterInputConnection connection = mConnection;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
connection.setComposingText(text, newCursorPosition);
}
});
}
private void finishComposingText() {
final AdapterInputConnection connection = mConnection;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
connection.finishComposingText();
}
});
}
private void deleteSurroundingText(final int before, final int after) {
final AdapterInputConnection connection = mConnection;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
connection.deleteSurroundingText(before, after);
}
});
}
private void dispatchKeyEvent(final KeyEvent event) {
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
mImeAdapter.dispatchKeyEvent(event);
}
});
}
/**
* Focus element, wait for a single state update, reset state update list.
* @param id ID of the element to focus.
*/
private void focusElementAndWaitForStateUpdate(String id)
throws InterruptedException, TimeoutException {
focusElement(id);
waitAndVerifyStatesAndCalls(0, "", 0, 0, -1, -1);
resetUpdateStateList();
}
private void focusElement(final String id) throws InterruptedException, TimeoutException {
focusElement(id, true);
}
private void focusElement(final String id, boolean shouldShowKeyboard)
throws InterruptedException, TimeoutException {
DOMUtils.focusNode(mWebContents, id);
assertWaitForKeyboardStatus(shouldShowKeyboard);
assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
try {
return id.equals(DOMUtils.getFocusedNode(mWebContents));
} catch (Exception e) {
return false;
}
}
}));
// When we focus another element, the connection may be recreated.
mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
}
private static class TestAdapterInputConnectionFactory extends
ImeAdapter.AdapterInputConnectionFactory {
private final List<TestImeState> mImeStateList = new ArrayList<>();
private final List<Integer> mTextInputTypeList = new ArrayList<>();
@Override
public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
Editable editable, EditorInfo outAttrs) {
mTextInputTypeList.add(imeAdapter.getTextInputType());
return new TestAdapterInputConnection(
mImeStateList, view, imeAdapter, editable, outAttrs);
}
public List<TestImeState> getImeStateList() {
return mImeStateList;
}
public Integer[] getTextInputTypeHistory() {
Integer[] result = new Integer[mTextInputTypeList.size()];
mTextInputTypeList.toArray(result);
return result;
}
public void clearTextInputTypeHistory() {
mTextInputTypeList.clear();
}
}
private static class TestAdapterInputConnection extends AdapterInputConnection {
private final List<TestImeState> mImeStateList;
public TestAdapterInputConnection(List<TestImeState> imeStateList, View view,
ImeAdapter imeAdapter, Editable editable, EditorInfo outAttrs) {
super(view, imeAdapter, editable, outAttrs);
mImeStateList = imeStateList;
}
@Override
public void updateState(String text, int selectionStart, int selectionEnd,
int compositionStart, int compositionEnd, boolean requiredAck) {
mImeStateList.add(new TestImeState(
text, selectionStart, selectionEnd, compositionStart, compositionEnd));
super.updateState(text, selectionStart, selectionEnd, compositionStart,
compositionEnd, requiredAck);
}
}
private static class TestImeState {
private final String mText;
private final int mSelectionStart;
private final int mSelectionEnd;
private final int mCompositionStart;
private final int mCompositionEnd;
public TestImeState(String text, int selectionStart, int selectionEnd,
int compositionStart, int compositionEnd) {
mText = text;
mSelectionStart = selectionStart;
mSelectionEnd = selectionEnd;
mCompositionStart = compositionStart;
mCompositionEnd = compositionEnd;
}
public void assertEqualState(String text, int selectionStart, int selectionEnd,
int compositionStart, int compositionEnd) {
assertEquals("Text did not match", text, mText);
assertEquals("Selection start did not match", selectionStart, mSelectionStart);
assertEquals("Selection end did not match", selectionEnd, mSelectionEnd);
assertEquals("Composition start did not match", compositionStart, mCompositionStart);
assertEquals("Composition end did not match", compositionEnd, mCompositionEnd);
}
}
}