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.
4
5package org.chromium.content.browser.input;
6
7import android.app.Activity;
8import android.content.ClipData;
9import android.content.ClipboardManager;
10import android.content.Context;
11import android.test.suitebuilder.annotation.MediumTest;
12import android.test.suitebuilder.annotation.SmallTest;
13import android.text.Editable;
14import android.text.Selection;
15import android.text.TextUtils;
16import android.view.KeyEvent;
17import android.view.View;
18import android.view.inputmethod.EditorInfo;
19
20import org.chromium.base.ThreadUtils;
21import org.chromium.base.test.util.DisabledTest;
22import org.chromium.base.test.util.Feature;
23import org.chromium.base.test.util.UrlUtils;
24import org.chromium.content.browser.ContentViewCore;
25import org.chromium.content.browser.test.util.Criteria;
26import org.chromium.content.browser.test.util.CriteriaHelper;
27import org.chromium.content.browser.test.util.DOMUtils;
28import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
29import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper;
30import org.chromium.content_shell_apk.ContentShellTestBase;
31
32import java.util.ArrayList;
33import java.util.concurrent.Callable;
34
35/**
36 * Integration tests for text input using cases based on fixed regressions.
37 */
38public class ImeTest extends ContentShellTestBase {
39
40    private static final String DATA_URL = UrlUtils.encodeHtmlDataUri(
41            "<html><head><meta name=\"viewport\"" +
42            "content=\"width=device-width, initial-scale=2.0, maximum-scale=2.0\" /></head>" +
43            "<body><form action=\"about:blank\">" +
44            "<input id=\"input_text\" type=\"text\" /><br/>" +
45            "<input id=\"input_radio\" type=\"radio\" style=\"width:50px;height:50px\" />" +
46            "<br/><textarea id=\"textarea\" rows=\"4\" cols=\"20\"></textarea>" +
47            "<br/><p><span id=\"plain_text\">This is Plain Text One</span></p>" +
48            "</form></body></html>");
49
50    private TestAdapterInputConnection mConnection;
51    private ImeAdapter mImeAdapter;
52
53    private ContentViewCore mContentViewCore;
54    private TestCallbackHelperContainer mCallbackContainer;
55    private TestInputMethodManagerWrapper mInputMethodManagerWrapper;
56
57    @Override
58    public void setUp() throws Exception {
59        super.setUp();
60
61        launchContentShellWithUrl(DATA_URL);
62        assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
63        mContentViewCore = getContentViewCore();
64
65        mInputMethodManagerWrapper = new TestInputMethodManagerWrapper(mContentViewCore);
66        getImeAdapter().setInputMethodManagerWrapper(mInputMethodManagerWrapper);
67        assertEquals(0, mInputMethodManagerWrapper.getShowSoftInputCounter());
68        mContentViewCore.setAdapterInputConnectionFactory(
69                new TestAdapterInputConnectionFactory());
70
71        mCallbackContainer = new TestCallbackHelperContainer(mContentViewCore);
72        // TODO(aurimas) remove this wait once crbug.com/179511 is fixed.
73        assertWaitForPageScaleFactorMatch(2);
74        assertTrue(DOMUtils.waitForNonZeroNodeBounds(
75                mContentViewCore, "input_text"));
76        DOMUtils.clickNode(this, mContentViewCore, "input_text");
77        assertWaitForKeyboardStatus(true);
78
79        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
80        mImeAdapter = getImeAdapter();
81
82        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
83        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
84        assertEquals(0, mInputMethodManagerWrapper.getEditorInfo().initialSelStart);
85        assertEquals(0, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd);
86    }
87
88    @MediumTest
89    @Feature({"TextInput", "Main"})
90    public void testKeyboardDismissedAfterClickingGo() throws Throwable {
91        setComposingText(mConnection, "hello", 1);
92        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5);
93
94        performGo(getAdapterInputConnection(), mCallbackContainer);
95
96        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "", 0, 0, -1, -1);
97        assertWaitForKeyboardStatus(false);
98    }
99
100    @SmallTest
101    @Feature({"TextInput", "Main"})
102    @RerunWithUpdatedContainerView
103    public void testGetTextUpdatesAfterEnteringText() throws Throwable {
104        setComposingText(mConnection, "h", 1);
105        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "h", 1, 1, 0, 1);
106        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
107
108        setComposingText(mConnection, "he", 1);
109        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "he", 2, 2, 0, 2);
110        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
111
112        setComposingText(mConnection, "hel", 1);
113        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hel", 3, 3, 0, 3);
114        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
115
116        commitText(mConnection, "hel", 1);
117        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hel", 3, 3, -1, -1);
118        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
119    }
120
121    @SmallTest
122    @Feature({"TextInput"})
123    @RerunWithUpdatedContainerView
124    public void testImeCopy() throws Exception {
125        commitText(mConnection, "hello", 1);
126        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
127
128        setSelection(mConnection, 2, 5);
129        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 2, 5, -1, -1);
130
131        copy(mImeAdapter);
132        assertClipboardContents(getActivity(), "llo");
133    }
134
135    @SmallTest
136    @Feature({"TextInput"})
137    public void testEnterTextAndRefocus() throws Exception {
138        commitText(mConnection, "hello", 1);
139        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
140
141        DOMUtils.clickNode(this, mContentViewCore, "input_radio");
142        assertWaitForKeyboardStatus(false);
143
144        DOMUtils.clickNode(this, mContentViewCore, "input_text");
145        assertWaitForKeyboardStatus(true);
146        assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelStart);
147        assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd);
148    }
149
150    @SmallTest
151    @Feature({"TextInput"})
152    public void testKeyboardNotDismissedAfterCopySelection() throws Exception {
153        commitText(mConnection, "Sample Text", 1);
154        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1,
155                "Sample Text", 11, 11, -1, -1);
156        DOMUtils.clickNode(this, mContentViewCore, "input_text");
157        assertWaitForKeyboardStatus(true);
158        DOMUtils.longPressNode(this, mContentViewCore, "input_text");
159        selectAll(mImeAdapter);
160        copy(mImeAdapter);
161        assertWaitForKeyboardStatus(true);
162        assertEquals(11, Selection.getSelectionEnd(mContentViewCore.getEditableForTest()));
163    }
164
165    @SmallTest
166    @Feature({"TextInput"})
167    public void testImeNotDismissedAfterCutSelection() throws Exception {
168        commitText(mConnection, "Sample Text", 1);
169        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1,
170                "Sample Text", 11, 11, -1, -1);
171        DOMUtils.longPressNode(this, mContentViewCore, "input_text");
172        assertWaitForSelectActionBarStatus(true);
173        assertWaitForKeyboardStatus(true);
174        cut(mImeAdapter);
175        assertWaitForKeyboardStatus(true);
176        assertWaitForSelectActionBarStatus(false);
177    }
178
179    @SmallTest
180    @Feature({"TextInput"})
181    public void testImeNotShownOnLongPressingEmptyInput() throws Exception {
182        DOMUtils.focusNode(mContentViewCore, "input_radio");
183        DOMUtils.longPressNode(this, mContentViewCore, "input_text");
184        assertWaitForKeyboardStatus(false);
185        commitText(mConnection, "Sample Text", 1);
186        DOMUtils.longPressNode(this, mContentViewCore, "input_text");
187        assertWaitForKeyboardStatus(true);
188    }
189
190    @SmallTest
191    @Feature({"TextInput"})
192    public void testSelectActionBarShownOnLongPressingInput() throws Exception {
193        DOMUtils.longPressNode(this, mContentViewCore, "input_text");
194        assertWaitForSelectActionBarStatus(false);
195        commitText(mConnection, "Sample Text", 1);
196        DOMUtils.longPressNode(this, mContentViewCore, "input_text");
197        assertWaitForSelectActionBarStatus(true);
198    }
199
200    /*
201    @SmallTest
202    @Feature({"TextInput"})
203    */
204    @DisabledTest
205    public void testSelectActionBarClearedOnTappingInput() throws Exception {
206        commitText(mConnection, "Sample Text", 1);
207        DOMUtils.longPressNode(this, mContentViewCore, "input_text");
208        assertWaitForKeyboardStatus(true);
209        assertWaitForSelectActionBarStatus(true);
210        DOMUtils.clickNode(this, mContentViewCore, "input_text");
211        assertWaitForSelectActionBarStatus(false);
212    }
213
214    @SmallTest
215    @Feature({"TextInput"})
216    public void testSelectActionBarClearedOnTappingOutsideInput() throws Exception {
217        commitText(mConnection, "Sample Text", 1);
218        DOMUtils.longPressNode(this, mContentViewCore, "input_text");
219        assertWaitForKeyboardStatus(true);
220        assertWaitForSelectActionBarStatus(true);
221        DOMUtils.clickNode(this, mContentViewCore, "input_radio");
222        assertWaitForKeyboardStatus(false);
223        assertWaitForSelectActionBarStatus(false);
224    }
225
226    @SmallTest
227    @Feature({"TextInput"})
228    public void testImeNotShownOnLongPressingDifferentEmptyInputs() throws Exception {
229        DOMUtils.focusNode(mContentViewCore, "input_radio");
230        DOMUtils.longPressNode(this, mContentViewCore, "input_text");
231        assertWaitForKeyboardStatus(false);
232        DOMUtils.longPressNode(this, mContentViewCore, "textarea");
233        assertWaitForKeyboardStatus(false);
234    }
235
236    @SmallTest
237    @Feature({"TextInput"})
238    public void testImeStaysOnLongPressingDifferentNonEmptyInputs() throws Exception {
239        DOMUtils.focusNode(mContentViewCore, "input_text");
240        assertWaitForKeyboardStatus(true);
241        commitText(mConnection, "Sample Text", 1);
242        DOMUtils.focusNode(mContentViewCore, "textarea");
243        commitText(mConnection, "Sample Text", 1);
244        DOMUtils.longPressNode(this, mContentViewCore, "input_text");
245        assertWaitForKeyboardStatus(true);
246        DOMUtils.longPressNode(this, mContentViewCore, "textarea");
247        assertWaitForKeyboardStatus(true);
248    }
249
250    @SmallTest
251    @Feature({"TextInput"})
252    public void testImeCut() throws Exception {
253        commitText(mConnection, "snarful", 1);
254        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "snarful", 7, 7, -1, -1);
255
256        setSelection(mConnection, 1, 5);
257        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "snarful", 1, 5, -1, -1);
258
259        cut(mImeAdapter);
260        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "sul", 1, 1, -1, -1);
261
262        assertClipboardContents(getActivity(), "narf");
263    }
264
265    @SmallTest
266    @Feature({"TextInput"})
267    public void testImePaste() throws Exception {
268        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
269            @Override
270            public void run() {
271                ClipboardManager clipboardManager =
272                        (ClipboardManager) getActivity().getSystemService(
273                                Context.CLIPBOARD_SERVICE);
274                clipboardManager.setPrimaryClip(ClipData.newPlainText("blarg", "blarg"));
275            }
276        });
277
278        paste(mImeAdapter);
279        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "blarg", 5, 5, -1, -1);
280
281        setSelection(mConnection, 3, 5);
282        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "blarg", 3, 5, -1, -1);
283
284        paste(mImeAdapter);
285        // Paste is a two step process when there is a non-zero selection.
286        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "bla", 3, 3, -1, -1);
287        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "blablarg", 8, 8, -1, -1);
288
289        paste(mImeAdapter);
290        waitAndVerifyEditableCallback(
291                mConnection.mImeUpdateQueue, 5, "blablargblarg", 13, 13, -1, -1);
292    }
293
294    @SmallTest
295    @Feature({"TextInput"})
296    public void testImeSelectAndUnSelectAll() throws Exception {
297        commitText(mConnection, "hello", 1);
298        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
299
300        selectAll(mImeAdapter);
301        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 0, 5, -1, -1);
302
303        unselect(mImeAdapter);
304        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "", 0, 0, -1, -1);
305
306        assertWaitForKeyboardStatus(false);
307    }
308
309    @SmallTest
310    @Feature({"TextInput", "Main"})
311    public void testShowImeIfNeeded() throws Throwable {
312        // showImeIfNeeded() is now implicitly called by the updated focus
313        // heuristic so no need to call explicitly. http://crbug.com/371927
314        DOMUtils.focusNode(mContentViewCore, "input_radio");
315        assertWaitForKeyboardStatus(false);
316
317        DOMUtils.focusNode(mContentViewCore, "input_text");
318        assertWaitForKeyboardStatus(true);
319    }
320
321    /*
322    @SmallTest
323    @Feature({"TextInput", "Main"})
324    */
325    @DisabledTest
326    public void testFinishComposingText() throws Throwable {
327        DOMUtils.focusNode(mContentViewCore, "input_radio");
328        assertWaitForKeyboardStatus(false);
329        DOMUtils.focusNode(mContentViewCore, "textarea");
330        assertWaitForKeyboardStatus(true);
331
332        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
333        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
334
335        commitText(mConnection, "hllo", 1);
336        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hllo", 4, 4, -1, -1);
337
338        commitText(mConnection, " ", 1);
339        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hllo ", 5, 5, -1, -1);
340
341        setSelection(mConnection, 1, 1);
342        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hllo ", 1, 1, -1, -1);
343
344        setComposingRegion(mConnection, 0, 4);
345        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hllo ", 1, 1, 0, 4);
346
347        finishComposingText(mConnection);
348        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 5, "hllo ", 1, 1, -1, -1);
349
350        commitText(mConnection, "\n", 1);
351        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 6, "h\nllo ", 2, 2, -1, -1);
352    }
353
354    private int getTypedKeycodeGuess(String before, String after) {
355        KeyEvent ev = ImeAdapter.getTypedKeyEventGuess(before, after);
356        if (ev == null) return -1;
357        return ev.getKeyCode();
358    }
359
360    @SmallTest
361    @Feature({"TextInput", "Main"})
362    public void testGuessedKeyCodeFromTyping() throws Throwable {
363        assertEquals(-1, getTypedKeycodeGuess(null, ""));
364        assertEquals(KeyEvent.KEYCODE_X, getTypedKeycodeGuess(null, "x"));
365        assertEquals(-1, getTypedKeycodeGuess(null, "xyz"));
366
367        assertEquals(-1, getTypedKeycodeGuess("abc", "abc"));
368        assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("abc", ""));
369
370        assertEquals(KeyEvent.KEYCODE_H, getTypedKeycodeGuess("", "h"));
371        assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("h", ""));
372        assertEquals(KeyEvent.KEYCODE_E, getTypedKeycodeGuess("h", "he"));
373        assertEquals(KeyEvent.KEYCODE_L, getTypedKeycodeGuess("he", "hel"));
374        assertEquals(KeyEvent.KEYCODE_O, getTypedKeycodeGuess("hel", "helo"));
375        assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("helo", "hel"));
376        assertEquals(KeyEvent.KEYCODE_L, getTypedKeycodeGuess("hel", "hell"));
377        assertEquals(KeyEvent.KEYCODE_L, getTypedKeycodeGuess("hell", "helll"));
378        assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("helll", "hell"));
379        assertEquals(KeyEvent.KEYCODE_O, getTypedKeycodeGuess("hell", "hello"));
380
381        assertEquals(KeyEvent.KEYCODE_X, getTypedKeycodeGuess("xxx", "xxxx"));
382        assertEquals(KeyEvent.KEYCODE_X, getTypedKeycodeGuess("xxx", "xxxxx"));
383        assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("xxx", "xx"));
384        assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("xxx", "x"));
385
386        assertEquals(KeyEvent.KEYCODE_Y, getTypedKeycodeGuess("xxx", "xxxy"));
387        assertEquals(KeyEvent.KEYCODE_Y, getTypedKeycodeGuess("xxx", "xxxxy"));
388        assertEquals(-1, getTypedKeycodeGuess("xxx", "xy"));
389        assertEquals(-1, getTypedKeycodeGuess("xxx", "y"));
390
391        assertEquals(-1, getTypedKeycodeGuess("foo", "bar"));
392        assertEquals(-1, getTypedKeycodeGuess("foo", "bars"));
393        assertEquals(-1, getTypedKeycodeGuess("foo", "ba"));
394
395        // Some characters also require modifiers so we have to check the full event.
396        KeyEvent ev = ImeAdapter.getTypedKeyEventGuess(null, "!");
397        assertEquals(KeyEvent.KEYCODE_1, ev.getKeyCode());
398        assertTrue(ev.isShiftPressed());
399    }
400
401    /*
402    @SmallTest
403    @Feature({"TextInput", "Main"})
404    */
405    @DisabledTest
406    public void testKeyCodesWhileComposingText() throws Throwable {
407        DOMUtils.focusNode(mContentViewCore, "textarea");
408        assertWaitForKeyboardStatus(true);
409
410        // The calls below are a reflection of what the stock Google Keyboard (Android 4.4) sends
411        // when the noted key is touched on screen.  Exercise care when altering to make sure
412        // that the test reflects reality.  If this test breaks, it's possible that code has
413        // changed and different calls need to be made instead.
414        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
415        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
416
417        // H
418        expectUpdateStateCall(mConnection);
419        setComposingText(mConnection, "h", 1);
420        assertEquals(KeyEvent.KEYCODE_H, mImeAdapter.mLastSyntheticKeyCode);
421        assertUpdateStateCall(mConnection, 1000);
422        assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
423
424        // O
425        expectUpdateStateCall(mConnection);
426        setComposingText(mConnection, "ho", 1);
427        assertEquals(KeyEvent.KEYCODE_O, mImeAdapter.mLastSyntheticKeyCode);
428        assertUpdateStateCall(mConnection, 1000);
429        assertEquals("ho", mConnection.getTextBeforeCursor(9, 0));
430
431        // DEL
432        expectUpdateStateCall(mConnection);
433        setComposingText(mConnection, "h", 1);
434        assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
435        assertUpdateStateCall(mConnection, 1000);
436        setComposingRegion(mConnection, 0, 1);  // DEL calls cancelComposition() then restarts
437        setComposingText(mConnection, "h", 1);
438        assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
439
440        // I
441        setComposingText(mConnection, "hi", 1);
442        assertEquals(KeyEvent.KEYCODE_I, mImeAdapter.mLastSyntheticKeyCode);
443        assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
444
445        // SPACE
446        commitText(mConnection, "hi", 1);
447        assertEquals(-1, mImeAdapter.mLastSyntheticKeyCode);
448        commitText(mConnection, " ", 1);
449        assertEquals(KeyEvent.KEYCODE_SPACE, mImeAdapter.mLastSyntheticKeyCode);
450        assertEquals("hi ", mConnection.getTextBeforeCursor(9, 0));
451
452        // DEL
453        deleteSurroundingText(mConnection, 1, 0);
454        assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
455        setComposingRegion(mConnection, 0, 2);
456        assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
457
458        // DEL
459        setComposingText(mConnection, "h", 1);
460        assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
461        assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
462
463        // DEL
464        commitText(mConnection, "", 1);
465        assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
466        assertEquals("", mConnection.getTextBeforeCursor(9, 0));
467
468        // DEL (on empty input)
469        deleteSurroundingText(mConnection, 1, 0);  // DEL on empty still sends 1,0
470        assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
471        assertEquals("", mConnection.getTextBeforeCursor(9, 0));
472    }
473
474    /*
475    @SmallTest
476    @Feature({"TextInput", "Main"})
477    */
478    @DisabledTest
479    public void testKeyCodesWhileSwipingText() throws Throwable {
480        DOMUtils.focusNode(mContentViewCore, "textarea");
481        assertWaitForKeyboardStatus(true);
482
483        // The calls below are a reflection of what the stock Google Keyboard (Android 4.4) sends
484        // when the word is swiped on the soft keyboard.  Exercise care when altering to make sure
485        // that the test reflects reality.  If this test breaks, it's possible that code has
486        // changed and different calls need to be made instead.
487        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
488        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
489
490        // "three"
491        expectUpdateStateCall(mConnection);
492        setComposingText(mConnection, "three", 1);
493        assertEquals(KeyEvent.KEYCODE_UNKNOWN, mImeAdapter.mLastSyntheticKeyCode);
494        assertUpdateStateCall(mConnection, 1000);
495        assertEquals("three", mConnection.getTextBeforeCursor(99, 0));
496
497        // "word"
498        commitText(mConnection, "three", 1);
499        commitText(mConnection, " ", 1);
500        expectUpdateStateCall(mConnection);
501        setComposingText(mConnection, "word", 1);
502        assertEquals(KeyEvent.KEYCODE_UNKNOWN, mImeAdapter.mLastSyntheticKeyCode);
503        assertUpdateStateCall(mConnection, 1000);
504        assertEquals("three word", mConnection.getTextBeforeCursor(99, 0));
505
506        // "test"
507        commitText(mConnection, "word", 1);
508        commitText(mConnection, " ", 1);
509        expectUpdateStateCall(mConnection);
510        setComposingText(mConnection, "test", 1);
511        assertEquals(KeyEvent.KEYCODE_UNKNOWN, mImeAdapter.mLastSyntheticKeyCode);
512        assertUpdateStateCall(mConnection, 1000);
513        assertEquals("three word test", mConnection.getTextBeforeCursor(99, 0));
514    }
515
516    @SmallTest
517    @Feature({"TextInput", "Main"})
518    public void testKeyCodesWhileTypingText() throws Throwable {
519        DOMUtils.focusNode(mContentViewCore, "textarea");
520        assertWaitForKeyboardStatus(true);
521
522        // The calls below are a reflection of what the Hacker's Keyboard sends when the noted
523        // key is touched on screen.  Exercise care when altering to make sure that the test
524        // reflects reality.
525        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
526        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
527
528        // H
529        expectUpdateStateCall(mConnection);
530        commitText(mConnection, "h", 1);
531        assertEquals(KeyEvent.KEYCODE_H, mImeAdapter.mLastSyntheticKeyCode);
532        assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
533        assertUpdateStateCall(mConnection, 1000);
534        assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
535
536        // O
537        expectUpdateStateCall(mConnection);
538        commitText(mConnection, "o", 1);
539        assertEquals(KeyEvent.KEYCODE_O, mImeAdapter.mLastSyntheticKeyCode);
540        assertEquals("ho", mConnection.getTextBeforeCursor(9, 0));
541        assertUpdateStateCall(mConnection, 1000);
542        assertEquals("ho", mConnection.getTextBeforeCursor(9, 0));
543
544        // DEL
545        expectUpdateStateCall(mConnection);
546        deleteSurroundingText(mConnection, 1, 0);
547        assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
548        assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
549        assertUpdateStateCall(mConnection, 1000);
550        assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
551
552        // I
553        expectUpdateStateCall(mConnection);
554        commitText(mConnection, "i", 1);
555        assertEquals(KeyEvent.KEYCODE_I, mImeAdapter.mLastSyntheticKeyCode);
556        assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
557        assertUpdateStateCall(mConnection, 1000);
558        assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
559
560        // SPACE
561        expectUpdateStateCall(mConnection);
562        commitText(mConnection, " ", 1);
563        assertEquals(KeyEvent.KEYCODE_SPACE, mImeAdapter.mLastSyntheticKeyCode);
564        assertEquals("hi ", mConnection.getTextBeforeCursor(9, 0));
565        assertUpdateStateCall(mConnection, 1000);
566        assertEquals("hi ", mConnection.getTextBeforeCursor(9, 0));
567
568        // DEL
569        expectUpdateStateCall(mConnection);
570        deleteSurroundingText(mConnection, 1, 0);
571        assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
572        assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
573        assertUpdateStateCall(mConnection, 1000);
574        assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
575
576        // DEL
577        expectUpdateStateCall(mConnection);
578        deleteSurroundingText(mConnection, 1, 0);
579        assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
580        assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
581        assertUpdateStateCall(mConnection, 1000);
582        assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
583
584        // DEL
585        expectUpdateStateCall(mConnection);
586        deleteSurroundingText(mConnection, 1, 0);
587        assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
588        assertEquals("", mConnection.getTextBeforeCursor(9, 0));
589        assertUpdateStateCall(mConnection, 1000);
590        assertEquals("", mConnection.getTextBeforeCursor(9, 0));
591
592        // DEL (on empty input)
593        deleteSurroundingText(mConnection, 1, 0);  // DEL on empty still sends 1,0
594        assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
595        assertEquals("", mConnection.getTextBeforeCursor(9, 0));
596    }
597
598    @SmallTest
599    @Feature({"TextInput", "Main"})
600    public void testSetComposingRegionOutOfBounds() throws Throwable {
601        DOMUtils.focusNode(mContentViewCore, "textarea");
602        assertWaitForKeyboardStatus(true);
603
604        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
605        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
606        setComposingText(mConnection, "hello", 1);
607
608        setComposingRegion(mConnection, 0, 0);
609        setComposingRegion(mConnection, 0, 9);
610        setComposingRegion(mConnection, 9, 0);
611    }
612
613    /*
614    @SmallTest
615    @Feature({"TextInput", "Main"})
616    */
617    @DisabledTest
618    public void testEnterKeyEventWhileComposingText() throws Throwable {
619        DOMUtils.focusNode(mContentViewCore, "input_radio");
620        assertWaitForKeyboardStatus(false);
621        DOMUtils.focusNode(mContentViewCore, "textarea");
622        assertWaitForKeyboardStatus(true);
623
624        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
625        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
626
627        setComposingText(mConnection, "hello", 1);
628        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5);
629
630        getInstrumentation().runOnMainSync(new Runnable() {
631            @Override
632            public void run() {
633                mConnection.sendKeyEvent(
634                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
635                mConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
636            }
637        });
638
639        // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
640        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 5, 5, -1, -1);
641        // The second new line is not a user visible/editable one, it is a side-effect of Blink
642        // using <br> internally.
643        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hello\n\n", 6, 6, -1, -1);
644    }
645
646    @SmallTest
647    @Feature({"TextInput", "Main"})
648    public void testTransitionsWhileComposingText() throws Throwable {
649        DOMUtils.focusNode(mContentViewCore, "textarea");
650        assertWaitForKeyboardStatus(true);
651
652        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
653        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
654
655        // H
656        expectUpdateStateCall(mConnection);
657        setComposingText(mConnection, "h", 1);
658        assertEquals(KeyEvent.KEYCODE_H, mImeAdapter.mLastSyntheticKeyCode);
659
660        // Simulate switch of input fields.
661        finishComposingText(mConnection);
662
663        // H
664        expectUpdateStateCall(mConnection);
665        setComposingText(mConnection, "h", 1);
666        assertEquals(KeyEvent.KEYCODE_H, mImeAdapter.mLastSyntheticKeyCode);
667    }
668
669    @SmallTest
670    @Feature({"TextInput"})
671    public void testPastePopupShowOnLongPress() throws Throwable {
672        commitText(mConnection, "hello", 1);
673        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
674
675        selectAll(mImeAdapter);
676        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 0, 5, -1, -1);
677
678        cut(mImeAdapter);
679        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
680
681        DOMUtils.longPressNode(this, mContentViewCore, "input_text");
682        final PastePopupMenu pastePopup = mContentViewCore.getPastePopupForTest();
683        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
684            @Override
685            public boolean isSatisfied() {
686                return pastePopup.isShowing();
687            }
688        }));
689    }
690
691    @SmallTest
692    @Feature({"TextInput"})
693    public void testTextHandlesPreservedWithDpadNavigation() throws Throwable {
694        DOMUtils.longPressNode(this, mContentViewCore, "plain_text");
695        assertWaitForSelectActionBarStatus(true);
696        assertTrue(mContentViewCore.hasSelection());
697
698        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
699            @Override
700            public void run() {
701                final KeyEvent downKeyEvent = new KeyEvent(
702                        KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN);
703                mImeAdapter.dispatchKeyEvent(downKeyEvent);
704            }
705        });
706
707        assertWaitForSelectActionBarStatus(true);
708        assertTrue(mContentViewCore.hasSelection());
709    }
710
711    private void performGo(final AdapterInputConnection inputConnection,
712            TestCallbackHelperContainer testCallbackHelperContainer) throws Throwable {
713        handleBlockingCallbackAction(
714                testCallbackHelperContainer.getOnPageFinishedHelper(),
715                new Runnable() {
716                    @Override
717                    public void run() {
718                        inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO);
719                    }
720                });
721    }
722
723    private void assertWaitForKeyboardStatus(final boolean show) throws InterruptedException {
724        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
725            @Override
726            public boolean isSatisfied() {
727                return show == getImeAdapter().mIsShowWithoutHideOutstanding &&
728                        (!show || getAdapterInputConnection() != null);
729            }
730        }));
731    }
732
733    private void assertWaitForSelectActionBarStatus(
734            final boolean show) throws InterruptedException {
735        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
736            @Override
737            public boolean isSatisfied() {
738                return show == mContentViewCore.isSelectActionBarShowing();
739            }
740        }));
741    }
742
743    private void waitAndVerifyEditableCallback(final ArrayList<TestImeState> states,
744            final int index, String text, int selectionStart, int selectionEnd,
745            int compositionStart, int compositionEnd) throws InterruptedException {
746        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
747            @Override
748            public boolean isSatisfied() {
749                return states.size() > index;
750            }
751        }));
752        states.get(index).assertEqualState(
753                text, selectionStart, selectionEnd, compositionStart, compositionEnd);
754    }
755
756    private void expectUpdateStateCall(final TestAdapterInputConnection connection) {
757        connection.mImeUpdateQueue.clear();
758    }
759
760    private void assertUpdateStateCall(final TestAdapterInputConnection connection, int maxms)
761            throws Exception {
762        while (connection.mImeUpdateQueue.size() == 0 && maxms > 0) {
763            try {
764                Thread.sleep(50);
765            } catch (Exception e) {
766                // Not really a problem since we're just going to sleep again.
767            }
768            maxms -= 50;
769        }
770        assertTrue(connection.mImeUpdateQueue.size() > 0);
771    }
772
773    private void assertClipboardContents(final Activity activity, final String expectedContents)
774            throws InterruptedException {
775        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
776            @Override
777            public boolean isSatisfied() {
778                return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
779                    @Override
780                    public Boolean call() throws Exception {
781                        ClipboardManager clipboardManager =
782                                (ClipboardManager) activity.getSystemService(
783                                        Context.CLIPBOARD_SERVICE);
784                        ClipData clip = clipboardManager.getPrimaryClip();
785                        return clip != null && clip.getItemCount() == 1
786                                && TextUtils.equals(clip.getItemAt(0).getText(), expectedContents);
787                    }
788                });
789            }
790        }));
791    }
792
793    private ImeAdapter getImeAdapter() {
794        return mContentViewCore.getImeAdapterForTest();
795    }
796
797    private AdapterInputConnection getAdapterInputConnection() {
798        return mContentViewCore.getInputConnectionForTest();
799    }
800
801    private void copy(final ImeAdapter adapter) {
802        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
803            @Override
804            public void run() {
805                adapter.copy();
806            }
807        });
808    }
809
810    private void cut(final ImeAdapter adapter) {
811        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
812            @Override
813            public void run() {
814                adapter.cut();
815            }
816        });
817    }
818
819    private void paste(final ImeAdapter adapter) {
820        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
821            @Override
822            public void run() {
823                adapter.paste();
824            }
825        });
826    }
827
828    private void selectAll(final ImeAdapter adapter) {
829        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
830            @Override
831            public void run() {
832                adapter.selectAll();
833            }
834        });
835    }
836
837    private void unselect(final ImeAdapter adapter) {
838        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
839            @Override
840            public void run() {
841                adapter.unselect();
842            }
843        });
844    }
845
846    private void commitText(final AdapterInputConnection connection, final CharSequence text,
847            final int newCursorPosition) {
848        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
849            @Override
850            public void run() {
851                connection.commitText(text, newCursorPosition);
852            }
853        });
854    }
855
856    private void setSelection(final AdapterInputConnection connection, final int start,
857            final int end) {
858        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
859            @Override
860            public void run() {
861                connection.setSelection(start, end);
862            }
863        });
864    }
865
866    private void setComposingRegion(final AdapterInputConnection connection, final int start,
867            final int end) {
868        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
869            @Override
870            public void run() {
871                connection.setComposingRegion(start, end);
872            }
873        });
874    }
875
876    private void setComposingText(final AdapterInputConnection connection, final CharSequence text,
877            final int newCursorPosition) {
878        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
879            @Override
880            public void run() {
881                connection.setComposingText(text, newCursorPosition);
882            }
883        });
884    }
885
886    private void finishComposingText(final AdapterInputConnection connection) {
887        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
888            @Override
889            public void run() {
890                connection.finishComposingText();
891            }
892        });
893    }
894
895    private void deleteSurroundingText(final AdapterInputConnection connection, final int before,
896            final int after) {
897        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
898            @Override
899            public void run() {
900                connection.deleteSurroundingText(before, after);
901            }
902        });
903    }
904
905    private static class TestAdapterInputConnectionFactory extends
906            ImeAdapter.AdapterInputConnectionFactory {
907        @Override
908        public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
909                Editable editable, EditorInfo outAttrs) {
910            return new TestAdapterInputConnection(view, imeAdapter, editable, outAttrs);
911        }
912    }
913
914    private static class TestAdapterInputConnection extends AdapterInputConnection {
915        private final ArrayList<TestImeState> mImeUpdateQueue = new ArrayList<TestImeState>();
916
917        public TestAdapterInputConnection(View view, ImeAdapter imeAdapter,
918                Editable editable, EditorInfo outAttrs) {
919            super(view, imeAdapter, editable, outAttrs);
920        }
921
922        @Override
923        public void updateState(String text, int selectionStart, int selectionEnd,
924                int compositionStart, int compositionEnd, boolean requiredAck) {
925            mImeUpdateQueue.add(new TestImeState(text, selectionStart, selectionEnd,
926                    compositionStart, compositionEnd));
927            super.updateState(text, selectionStart, selectionEnd, compositionStart,
928                    compositionEnd, requiredAck);
929        }
930    }
931
932    private static class TestImeState {
933        private final String mText;
934        private final int mSelectionStart;
935        private final int mSelectionEnd;
936        private final int mCompositionStart;
937        private final int mCompositionEnd;
938
939        public TestImeState(String text, int selectionStart, int selectionEnd,
940                int compositionStart, int compositionEnd) {
941            mText = text;
942            mSelectionStart = selectionStart;
943            mSelectionEnd = selectionEnd;
944            mCompositionStart = compositionStart;
945            mCompositionEnd = compositionEnd;
946        }
947
948        public void assertEqualState(String text, int selectionStart, int selectionEnd,
949                int compositionStart, int compositionEnd) {
950            assertEquals("Text did not match", text, mText);
951            assertEquals("Selection start did not match", selectionStart, mSelectionStart);
952            assertEquals("Selection end did not match", selectionEnd, mSelectionEnd);
953            assertEquals("Composition start did not match", compositionStart, mCompositionStart);
954            assertEquals("Composition end did not match", compositionEnd, mCompositionEnd);
955        }
956    }
957}
958