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.TextUtils;
14 import android.view.KeyEvent;
15 import android.view.View;
16 import android.view.inputmethod.EditorInfo;
18 import org.chromium.base.ThreadUtils;
19 import org.chromium.base.test.util.Feature;
20 import org.chromium.base.test.util.UrlUtils;
21 import org.chromium.content.browser.ContentView;
22 import org.chromium.content.browser.test.util.Criteria;
23 import org.chromium.content.browser.test.util.CriteriaHelper;
24 import org.chromium.content.browser.test.util.DOMUtils;
25 import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
26 import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper;
27 import org.chromium.content_shell_apk.ContentShellTestBase;
29 import java.util.ArrayList;
30 import java.util.concurrent.Callable;
32 public class ImeTest extends ContentShellTestBase {
34 private static final String DATA_URL = UrlUtils.encodeHtmlDataUri(
35 "<html><head><meta name=\"viewport\"" +
36 "content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\" /></head>" +
37 "<body><form action=\"about:blank\">" +
38 "<input id=\"input_text\" type=\"text\" />" +
39 "<input id=\"input_radio\" type=\"radio\" style=\"width:50px;height:50px\" />" +
40 "<br/><textarea id=\"textarea\" rows=\"4\" cols=\"20\"></textarea>" +
41 "</form></body></html>");
43 private TestAdapterInputConnection mConnection;
44 private ImeAdapter mImeAdapter;
45 private ContentView mContentView;
46 private TestCallbackHelperContainer mCallbackContainer;
47 private TestInputMethodManagerWrapper mInputMethodManagerWrapper;
50 public void setUp() throws Exception {
53 launchContentShellWithUrl(DATA_URL);
54 assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
56 mInputMethodManagerWrapper = new TestInputMethodManagerWrapper(getContentViewCore());
57 getImeAdapter().setInputMethodManagerWrapper(mInputMethodManagerWrapper);
58 assertEquals(0, mInputMethodManagerWrapper.getShowSoftInputCounter());
59 getContentViewCore().setAdapterInputConnectionFactory(
60 new TestAdapterInputConnectionFactory());
62 mContentView = getActivity().getActiveContentView();
63 mCallbackContainer = new TestCallbackHelperContainer(mContentView);
64 // TODO(aurimas) remove this wait once crbug.com/179511 is fixed.
65 assertWaitForPageScaleFactorMatch(1);
66 DOMUtils.clickNode(this, mContentView, mCallbackContainer, "input_text");
67 assertWaitForKeyboardStatus(true);
69 mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
70 mImeAdapter = getImeAdapter();
72 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
73 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
74 assertEquals(0, mInputMethodManagerWrapper.getEditorInfo().initialSelStart);
75 assertEquals(0, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd);
79 @Feature({"TextInput", "Main"})
80 public void testKeyboardDismissedAfterClickingGo() throws Throwable {
81 mConnection.setComposingText("hello", 1);
82 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5);
84 performGo(getAdapterInputConnection(), mCallbackContainer);
86 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "", 0, 0, -1, -1);
87 assertWaitForKeyboardStatus(false);
91 @Feature({"TextInput", "Main"})
92 public void testGetTextUpdatesAfterEnteringText() throws Throwable {
93 mConnection.setComposingText("h", 1);
94 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "h", 1, 1, 0, 1);
95 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
97 mConnection.setComposingText("he", 1);
98 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "he", 2, 2, 0, 2);
99 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
101 mConnection.setComposingText("hel", 1);
102 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hel", 3, 3, 0, 3);
103 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
105 mConnection.commitText("hel", 1);
106 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hel", 3, 3, -1, -1);
107 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
111 @Feature({"TextInput"})
112 public void testImeCopy() throws Exception {
113 mConnection.commitText("hello", 1);
114 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
116 mConnection.setSelection(2, 5);
117 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 2, 5, -1, -1);
120 assertClipboardContents(getActivity(), "llo");
124 @Feature({"TextInput"})
125 public void testEnterTextAndRefocus() throws Exception {
126 mConnection.commitText("hello", 1);
127 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
129 DOMUtils.clickNode(this, mContentView, mCallbackContainer, "input_radio");
130 assertWaitForKeyboardStatus(false);
132 DOMUtils.clickNode(this, mContentView, mCallbackContainer, "input_text");
133 assertWaitForKeyboardStatus(true);
134 assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelStart);
135 assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd);
139 @Feature({"TextInput"})
140 public void testImeCut() throws Exception {
141 mConnection.commitText("snarful", 1);
142 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "snarful", 7, 7, -1, -1);
144 mConnection.setSelection(1, 5);
145 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "snarful", 1, 5, -1, -1);
148 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "sul", 1, 1, -1, -1);
150 assertClipboardContents(getActivity(), "narf");
154 @Feature({"TextInput"})
155 public void testImePaste() throws Exception {
156 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
159 ClipboardManager clipboardManager =
160 (ClipboardManager) getActivity().getSystemService(
161 Context.CLIPBOARD_SERVICE);
162 clipboardManager.setPrimaryClip(ClipData.newPlainText("blarg", "blarg"));
167 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "blarg", 5, 5, -1, -1);
169 mConnection.setSelection(3, 5);
170 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "blarg", 3, 5, -1, -1);
173 // Paste is a two step process when there is a non-zero selection.
174 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "bla", 3, 3, -1, -1);
175 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "blablarg", 8, 8, -1, -1);
178 waitAndVerifyEditableCallback(
179 mConnection.mImeUpdateQueue, 5, "blablargblarg", 13, 13, -1, -1);
183 @Feature({"TextInput"})
184 public void testImeSelectAndUnSelectAll() throws Exception {
185 mConnection.commitText("hello", 1);
186 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
188 mImeAdapter.selectAll();
189 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 0, 5, -1, -1);
191 mImeAdapter.unselect();
192 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "", 0, 0, -1, -1);
194 assertWaitForKeyboardStatus(false);
198 @Feature({"TextInput", "Main"})
199 public void testShowImeIfNeeded() throws Throwable {
200 DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_radio");
201 assertWaitForKeyboardStatus(false);
203 performShowImeIfNeeded();
204 assertWaitForKeyboardStatus(false);
206 DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_text");
207 assertWaitForKeyboardStatus(false);
209 performShowImeIfNeeded();
210 assertWaitForKeyboardStatus(true);
214 @Feature({"TextInput", "Main"})
215 public void testFinishComposingText() throws Throwable {
216 // Focus the textarea. We need to do the following steps because we are focusing using JS.
217 DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_radio");
218 assertWaitForKeyboardStatus(false);
219 DOMUtils.focusNode(this, mContentView, mCallbackContainer, "textarea");
220 assertWaitForKeyboardStatus(false);
221 performShowImeIfNeeded();
222 assertWaitForKeyboardStatus(true);
224 mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
225 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
227 mConnection.commitText("hllo", 1);
228 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hllo", 4, 4, -1, -1);
230 mConnection.commitText(" ", 1);
231 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hllo ", 5, 5, -1, -1);
233 mConnection.setSelection(1, 1);
234 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hllo ", 1, 1, -1, -1);
236 mConnection.setComposingRegion(0, 4);
237 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hllo ", 1, 1, 0, 4);
239 mConnection.finishComposingText();
240 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 5, "hllo ", 1, 1, -1, -1);
242 mConnection.commitText("\n", 1);
243 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 6, "h\nllo ", 2, 2, -1, -1);
247 @Feature({"TextInput", "Main"})
248 public void testEnterKeyEventWhileComposingText() throws Throwable {
249 // Focus the textarea. We need to do the following steps because we are focusing using JS.
250 DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_radio");
251 assertWaitForKeyboardStatus(false);
252 DOMUtils.focusNode(this, mContentView, mCallbackContainer, "textarea");
253 assertWaitForKeyboardStatus(false);
254 performShowImeIfNeeded();
255 assertWaitForKeyboardStatus(true);
257 mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
258 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
260 mConnection.setComposingText("hello", 1);
261 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5);
263 getInstrumentation().runOnMainSync(new Runnable() {
266 mConnection.sendKeyEvent(
267 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
268 mConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
272 // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
273 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 5, 5, -1, -1);
274 // The second new line is not a user visible/editable one, it is a side-effect of Blink
275 // using <br> internally.
276 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hello\n\n", 6, 6, -1, -1);
279 private void performShowImeIfNeeded() {
280 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
283 mContentView.getContentViewCore().showImeIfNeeded();
288 private void performGo(final AdapterInputConnection inputConnection,
289 TestCallbackHelperContainer testCallbackHelperContainer) throws Throwable {
290 handleBlockingCallbackAction(
291 testCallbackHelperContainer.getOnPageFinishedHelper(),
295 inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO);
300 private void assertWaitForKeyboardStatus(final boolean show) throws InterruptedException {
301 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
303 public boolean isSatisfied() {
304 return show == getImeAdapter().mIsShowWithoutHideOutstanding &&
305 (!show || getAdapterInputConnection() != null);
310 private void waitAndVerifyEditableCallback(final ArrayList<TestImeState> states,
311 final int index, String text, int selectionStart, int selectionEnd,
312 int compositionStart, int compositionEnd) throws InterruptedException {
313 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
315 public boolean isSatisfied() {
316 return states.size() > index;
319 states.get(index).assertEqualState(
320 text, selectionStart, selectionEnd, compositionStart, compositionEnd);
323 private void assertClipboardContents(final Activity activity, final String expectedContents)
324 throws InterruptedException {
325 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
327 public boolean isSatisfied() {
328 return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
330 public Boolean call() throws Exception {
331 ClipboardManager clipboardManager =
332 (ClipboardManager) activity.getSystemService(
333 Context.CLIPBOARD_SERVICE);
334 ClipData clip = clipboardManager.getPrimaryClip();
335 return clip != null && clip.getItemCount() == 1
336 && TextUtils.equals(clip.getItemAt(0).getText(), expectedContents);
343 private ImeAdapter getImeAdapter() {
344 return getContentViewCore().getImeAdapterForTest();
347 private AdapterInputConnection getAdapterInputConnection() {
348 return getContentViewCore().getInputConnectionForTest();
351 private static class TestAdapterInputConnectionFactory extends
352 ImeAdapter.AdapterInputConnectionFactory {
354 public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
355 EditorInfo outAttrs) {
356 return new TestAdapterInputConnection(view, imeAdapter, outAttrs);
360 private static class TestAdapterInputConnection extends AdapterInputConnection {
361 private final ArrayList<TestImeState> mImeUpdateQueue = new ArrayList<TestImeState>();
363 public TestAdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
364 super(view, imeAdapter, outAttrs);
368 public void updateState(String text, int selectionStart, int selectionEnd,
369 int compositionStart, int compositionEnd, boolean requiredAck) {
370 mImeUpdateQueue.add(new TestImeState(text, selectionStart, selectionEnd,
371 compositionStart, compositionEnd));
372 super.updateState(text, selectionStart, selectionEnd, compositionStart,
373 compositionEnd, requiredAck);
377 private static class TestImeState {
378 private final String mText;
379 private final int mSelectionStart;
380 private final int mSelectionEnd;
381 private final int mCompositionStart;
382 private final int mCompositionEnd;
384 public TestImeState(String text, int selectionStart, int selectionEnd,
385 int compositionStart, int compositionEnd) {
387 mSelectionStart = selectionStart;
388 mSelectionEnd = selectionEnd;
389 mCompositionStart = compositionStart;
390 mCompositionEnd = compositionEnd;
393 public void assertEqualState(String text, int selectionStart, int selectionEnd,
394 int compositionStart, int compositionEnd) {
395 assertEquals("Text did not match", text, mText);
396 assertEquals("Selection start did not match", selectionStart, mSelectionStart);
397 assertEquals("Selection end did not match", selectionEnd, mSelectionEnd);
398 assertEquals("Composition start did not match", compositionStart, mCompositionStart);
399 assertEquals("Composition end did not match", compositionEnd, mCompositionEnd);