1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.content.browser.input;
7 import android.app.Activity;
8 import android.content.ClipData;
9 import android.content.ClipboardManager;
10 import android.content.Context;
11 import android.test.suitebuilder.annotation.MediumTest;
12 import android.test.suitebuilder.annotation.SmallTest;
13 import android.text.Editable;
14 import android.text.TextUtils;
15 import android.view.KeyEvent;
16 import android.view.View;
17 import android.view.inputmethod.EditorInfo;
19 import org.chromium.base.ThreadUtils;
20 import org.chromium.base.test.util.Feature;
21 import org.chromium.base.test.util.UrlUtils;
22 import org.chromium.content.browser.ContentViewCore;
23 import org.chromium.content.browser.test.util.Criteria;
24 import org.chromium.content.browser.test.util.CriteriaHelper;
25 import org.chromium.content.browser.test.util.DOMUtils;
26 import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
27 import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper;
28 import org.chromium.content_shell_apk.ContentShellTestBase;
30 import java.util.ArrayList;
31 import java.util.concurrent.Callable;
34 * Integration tests for text input using cases based on fixed regressions.
36 public class ImeTest extends ContentShellTestBase {
38 private static final String DATA_URL = UrlUtils.encodeHtmlDataUri(
39 "<html><head><meta name=\"viewport\"" +
40 "content=\"width=device-width, initial-scale=2.0, maximum-scale=2.0\" /></head>" +
41 "<body><form action=\"about:blank\">" +
42 "<input id=\"input_text\" type=\"text\" /><br/>" +
43 "<input id=\"input_radio\" type=\"radio\" style=\"width:50px;height:50px\" />" +
44 "<br/><textarea id=\"textarea\" rows=\"4\" cols=\"20\"></textarea>" +
45 "</form></body></html>");
47 private TestAdapterInputConnection mConnection;
48 private ImeAdapter mImeAdapter;
50 private ContentViewCore mContentViewCore;
51 private TestCallbackHelperContainer mCallbackContainer;
52 private TestInputMethodManagerWrapper mInputMethodManagerWrapper;
55 public void setUp() throws Exception {
58 launchContentShellWithUrl(DATA_URL);
59 assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
60 mContentViewCore = getContentViewCore();
62 mInputMethodManagerWrapper = new TestInputMethodManagerWrapper(mContentViewCore);
63 getImeAdapter().setInputMethodManagerWrapper(mInputMethodManagerWrapper);
64 assertEquals(0, mInputMethodManagerWrapper.getShowSoftInputCounter());
65 mContentViewCore.setAdapterInputConnectionFactory(
66 new TestAdapterInputConnectionFactory());
68 mCallbackContainer = new TestCallbackHelperContainer(mContentViewCore);
69 // TODO(aurimas) remove this wait once crbug.com/179511 is fixed.
70 assertWaitForPageScaleFactorMatch(2);
71 assertTrue(DOMUtils.waitForNonZeroNodeBounds(
72 mContentViewCore, "input_text"));
73 DOMUtils.clickNode(this, mContentViewCore, "input_text");
74 assertWaitForKeyboardStatus(true);
76 mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
77 mImeAdapter = getImeAdapter();
79 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
80 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
81 assertEquals(0, mInputMethodManagerWrapper.getEditorInfo().initialSelStart);
82 assertEquals(0, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd);
86 @Feature({"TextInput", "Main"})
87 public void testKeyboardDismissedAfterClickingGo() throws Throwable {
88 setComposingText(mConnection, "hello", 1);
89 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5);
91 performGo(getAdapterInputConnection(), mCallbackContainer);
93 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "", 0, 0, -1, -1);
94 assertWaitForKeyboardStatus(false);
98 @Feature({"TextInput", "Main"})
99 public void testGetTextUpdatesAfterEnteringText() throws Throwable {
100 setComposingText(mConnection, "h", 1);
101 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "h", 1, 1, 0, 1);
102 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
104 setComposingText(mConnection, "he", 1);
105 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "he", 2, 2, 0, 2);
106 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
108 setComposingText(mConnection, "hel", 1);
109 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hel", 3, 3, 0, 3);
110 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
112 commitText(mConnection, "hel", 1);
113 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hel", 3, 3, -1, -1);
114 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
118 @Feature({"TextInput"})
119 public void testImeCopy() throws Exception {
120 commitText(mConnection, "hello", 1);
121 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
123 setSelection(mConnection, 2, 5);
124 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 2, 5, -1, -1);
127 assertClipboardContents(getActivity(), "llo");
131 @Feature({"TextInput"})
132 public void testEnterTextAndRefocus() throws Exception {
133 commitText(mConnection, "hello", 1);
134 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
136 DOMUtils.clickNode(this, mContentViewCore, "input_radio");
137 assertWaitForKeyboardStatus(false);
139 DOMUtils.clickNode(this, mContentViewCore, "input_text");
140 assertWaitForKeyboardStatus(true);
141 assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelStart);
142 assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd);
146 @Feature({"TextInput"})
147 public void testImeCut() throws Exception {
148 commitText(mConnection, "snarful", 1);
149 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "snarful", 7, 7, -1, -1);
151 setSelection(mConnection, 1, 5);
152 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "snarful", 1, 5, -1, -1);
155 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "sul", 1, 1, -1, -1);
157 assertClipboardContents(getActivity(), "narf");
161 @Feature({"TextInput"})
162 public void testImePaste() throws Exception {
163 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
166 ClipboardManager clipboardManager =
167 (ClipboardManager) getActivity().getSystemService(
168 Context.CLIPBOARD_SERVICE);
169 clipboardManager.setPrimaryClip(ClipData.newPlainText("blarg", "blarg"));
174 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "blarg", 5, 5, -1, -1);
176 setSelection(mConnection, 3, 5);
177 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "blarg", 3, 5, -1, -1);
180 // Paste is a two step process when there is a non-zero selection.
181 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "bla", 3, 3, -1, -1);
182 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "blablarg", 8, 8, -1, -1);
185 waitAndVerifyEditableCallback(
186 mConnection.mImeUpdateQueue, 5, "blablargblarg", 13, 13, -1, -1);
190 @Feature({"TextInput"})
191 public void testImeSelectAndUnSelectAll() throws Exception {
192 commitText(mConnection, "hello", 1);
193 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
195 selectAll(mImeAdapter);
196 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 0, 5, -1, -1);
198 unselect(mImeAdapter);
199 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "", 0, 0, -1, -1);
201 assertWaitForKeyboardStatus(false);
205 @Feature({"TextInput", "Main"})
206 public void testShowImeIfNeeded() throws Throwable {
207 DOMUtils.focusNode(mContentViewCore, "input_radio");
208 assertWaitForKeyboardStatus(false);
210 performShowImeIfNeeded();
211 assertWaitForKeyboardStatus(false);
213 DOMUtils.focusNode(mContentViewCore, "input_text");
214 assertWaitForKeyboardStatus(false);
216 performShowImeIfNeeded();
217 assertWaitForKeyboardStatus(true);
221 @Feature({"TextInput", "Main"})
222 public void testFinishComposingText() throws Throwable {
223 // Focus the textarea. We need to do the following steps because we are focusing using JS.
224 DOMUtils.focusNode(mContentViewCore, "input_radio");
225 assertWaitForKeyboardStatus(false);
226 DOMUtils.focusNode(mContentViewCore, "textarea");
227 assertWaitForKeyboardStatus(false);
228 performShowImeIfNeeded();
229 assertWaitForKeyboardStatus(true);
231 mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
232 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
234 commitText(mConnection, "hllo", 1);
235 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hllo", 4, 4, -1, -1);
237 commitText(mConnection, " ", 1);
238 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hllo ", 5, 5, -1, -1);
240 setSelection(mConnection, 1, 1);
241 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hllo ", 1, 1, -1, -1);
243 setComposingRegion(mConnection, 0, 4);
244 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hllo ", 1, 1, 0, 4);
246 finishComposingText(mConnection);
247 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 5, "hllo ", 1, 1, -1, -1);
249 commitText(mConnection, "\n", 1);
250 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 6, "h\nllo ", 2, 2, -1, -1);
254 @Feature({"TextInput", "Main"})
255 public void testEnterKeyEventWhileComposingText() throws Throwable {
256 // Focus the textarea. We need to do the following steps because we are focusing using JS.
257 DOMUtils.focusNode(mContentViewCore, "input_radio");
258 assertWaitForKeyboardStatus(false);
259 DOMUtils.focusNode(mContentViewCore, "textarea");
260 assertWaitForKeyboardStatus(false);
261 performShowImeIfNeeded();
262 assertWaitForKeyboardStatus(true);
264 mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
265 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
267 setComposingText(mConnection, "hello", 1);
268 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5);
270 getInstrumentation().runOnMainSync(new Runnable() {
273 mConnection.sendKeyEvent(
274 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
275 mConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
279 // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
280 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 5, 5, -1, -1);
281 // The second new line is not a user visible/editable one, it is a side-effect of Blink
282 // using <br> internally.
283 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hello\n\n", 6, 6, -1, -1);
286 private void performShowImeIfNeeded() {
287 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
290 mContentViewCore.showImeIfNeeded();
295 private void performGo(final AdapterInputConnection inputConnection,
296 TestCallbackHelperContainer testCallbackHelperContainer) throws Throwable {
297 handleBlockingCallbackAction(
298 testCallbackHelperContainer.getOnPageFinishedHelper(),
302 inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO);
307 private void assertWaitForKeyboardStatus(final boolean show) throws InterruptedException {
308 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
310 public boolean isSatisfied() {
311 return show == getImeAdapter().mIsShowWithoutHideOutstanding &&
312 (!show || getAdapterInputConnection() != null);
317 private void waitAndVerifyEditableCallback(final ArrayList<TestImeState> states,
318 final int index, String text, int selectionStart, int selectionEnd,
319 int compositionStart, int compositionEnd) throws InterruptedException {
320 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
322 public boolean isSatisfied() {
323 return states.size() > index;
326 states.get(index).assertEqualState(
327 text, selectionStart, selectionEnd, compositionStart, compositionEnd);
330 private void assertClipboardContents(final Activity activity, final String expectedContents)
331 throws InterruptedException {
332 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
334 public boolean isSatisfied() {
335 return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
337 public Boolean call() throws Exception {
338 ClipboardManager clipboardManager =
339 (ClipboardManager) activity.getSystemService(
340 Context.CLIPBOARD_SERVICE);
341 ClipData clip = clipboardManager.getPrimaryClip();
342 return clip != null && clip.getItemCount() == 1
343 && TextUtils.equals(clip.getItemAt(0).getText(), expectedContents);
350 private ImeAdapter getImeAdapter() {
351 return mContentViewCore.getImeAdapterForTest();
354 private AdapterInputConnection getAdapterInputConnection() {
355 return mContentViewCore.getInputConnectionForTest();
358 private void copy(final ImeAdapter adapter) {
359 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
367 private void cut(final ImeAdapter adapter) {
368 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
376 private void paste(final ImeAdapter adapter) {
377 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
385 private void selectAll(final ImeAdapter adapter) {
386 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
394 private void unselect(final ImeAdapter adapter) {
395 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
403 private void commitText(final AdapterInputConnection connection, final CharSequence text,
404 final int newCursorPosition) {
405 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
408 connection.commitText(text, newCursorPosition);
413 private void setSelection(final AdapterInputConnection connection, final int start,
415 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
418 connection.setSelection(start, end);
423 private void setComposingRegion(final AdapterInputConnection connection, final int start,
425 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
428 connection.setComposingRegion(start, end);
433 private void setComposingText(final AdapterInputConnection connection, final CharSequence text,
434 final int newCursorPosition) {
435 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
438 connection.setComposingText(text, newCursorPosition);
443 private void finishComposingText(final AdapterInputConnection connection) {
444 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
447 connection.finishComposingText();
452 private static class TestAdapterInputConnectionFactory extends
453 ImeAdapter.AdapterInputConnectionFactory {
455 public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
456 Editable editable, EditorInfo outAttrs) {
457 return new TestAdapterInputConnection(view, imeAdapter, editable, outAttrs);
461 private static class TestAdapterInputConnection extends AdapterInputConnection {
462 private final ArrayList<TestImeState> mImeUpdateQueue = new ArrayList<TestImeState>();
464 public TestAdapterInputConnection(View view, ImeAdapter imeAdapter,
465 Editable editable, EditorInfo outAttrs) {
466 super(view, imeAdapter, editable, outAttrs);
470 public void updateState(String text, int selectionStart, int selectionEnd,
471 int compositionStart, int compositionEnd, boolean requiredAck) {
472 mImeUpdateQueue.add(new TestImeState(text, selectionStart, selectionEnd,
473 compositionStart, compositionEnd));
474 super.updateState(text, selectionStart, selectionEnd, compositionStart,
475 compositionEnd, requiredAck);
479 private static class TestImeState {
480 private final String mText;
481 private final int mSelectionStart;
482 private final int mSelectionEnd;
483 private final int mCompositionStart;
484 private final int mCompositionEnd;
486 public TestImeState(String text, int selectionStart, int selectionEnd,
487 int compositionStart, int compositionEnd) {
489 mSelectionStart = selectionStart;
490 mSelectionEnd = selectionEnd;
491 mCompositionStart = compositionStart;
492 mCompositionEnd = compositionEnd;
495 public void assertEqualState(String text, int selectionStart, int selectionEnd,
496 int compositionStart, int compositionEnd) {
497 assertEquals("Text did not match", text, mText);
498 assertEquals("Selection start did not match", selectionStart, mSelectionStart);
499 assertEquals("Selection end did not match", selectionEnd, mSelectionEnd);
500 assertEquals("Composition start did not match", compositionStart, mCompositionStart);
501 assertEquals("Composition end did not match", compositionEnd, mCompositionEnd);