| // 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); |
| } |
| } |
| } |