ImeTest.java revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
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.TextUtils;
14import android.view.KeyEvent;
15import android.view.View;
16import android.view.inputmethod.EditorInfo;
17
18import org.chromium.base.ThreadUtils;
19import org.chromium.base.test.util.Feature;
20import org.chromium.base.test.util.UrlUtils;
21import org.chromium.content.browser.ContentView;
22import org.chromium.content.browser.test.util.Criteria;
23import org.chromium.content.browser.test.util.CriteriaHelper;
24import org.chromium.content.browser.test.util.DOMUtils;
25import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
26import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper;
27import org.chromium.content_shell_apk.ContentShellTestBase;
28
29import java.util.ArrayList;
30import java.util.concurrent.Callable;
31
32/**
33 * Integration tests for text input using cases based on fixed regressions.
34 */
35public class ImeTest extends ContentShellTestBase {
36
37    private static final String DATA_URL = UrlUtils.encodeHtmlDataUri(
38            "<html><head><meta name=\"viewport\"" +
39            "content=\"width=device-width, initial-scale=2.0, maximum-scale=2.0\" /></head>" +
40            "<body><form action=\"about:blank\">" +
41            "<input id=\"input_text\" type=\"text\" /><br/>" +
42            "<input id=\"input_radio\" type=\"radio\" style=\"width:50px;height:50px\" />" +
43            "<br/><textarea id=\"textarea\" rows=\"4\" cols=\"20\"></textarea>" +
44            "</form></body></html>");
45
46    private TestAdapterInputConnection mConnection;
47    private ImeAdapter mImeAdapter;
48    private ContentView mContentView;
49    private TestCallbackHelperContainer mCallbackContainer;
50    private TestInputMethodManagerWrapper mInputMethodManagerWrapper;
51
52    @Override
53    public void setUp() throws Exception {
54        super.setUp();
55
56        launchContentShellWithUrl(DATA_URL);
57        assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
58
59        mInputMethodManagerWrapper = new TestInputMethodManagerWrapper(getContentViewCore());
60        getImeAdapter().setInputMethodManagerWrapper(mInputMethodManagerWrapper);
61        assertEquals(0, mInputMethodManagerWrapper.getShowSoftInputCounter());
62        getContentViewCore().setAdapterInputConnectionFactory(
63                new TestAdapterInputConnectionFactory());
64
65        mContentView = getActivity().getActiveContentView();
66        mCallbackContainer = new TestCallbackHelperContainer(mContentView);
67        // TODO(aurimas) remove this wait once crbug.com/179511 is fixed.
68        assertWaitForPageScaleFactorMatch(2);
69        assertTrue(
70                DOMUtils.waitForNonZeroNodeBounds(mContentView, mCallbackContainer, "input_text"));
71        DOMUtils.clickNode(this, mContentView, mCallbackContainer, "input_text");
72        assertWaitForKeyboardStatus(true);
73
74        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
75        mImeAdapter = getImeAdapter();
76
77        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
78        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
79        assertEquals(0, mInputMethodManagerWrapper.getEditorInfo().initialSelStart);
80        assertEquals(0, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd);
81    }
82
83    @MediumTest
84    @Feature({"TextInput", "Main"})
85    public void testKeyboardDismissedAfterClickingGo() throws Throwable {
86        setComposingText(mConnection, "hello", 1);
87        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5);
88
89        performGo(getAdapterInputConnection(), mCallbackContainer);
90
91        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "", 0, 0, -1, -1);
92        assertWaitForKeyboardStatus(false);
93    }
94
95    @SmallTest
96    @Feature({"TextInput", "Main"})
97    public void testGetTextUpdatesAfterEnteringText() throws Throwable {
98        setComposingText(mConnection, "h", 1);
99        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "h", 1, 1, 0, 1);
100        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
101
102        setComposingText(mConnection, "he", 1);
103        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "he", 2, 2, 0, 2);
104        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
105
106        setComposingText(mConnection, "hel", 1);
107        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hel", 3, 3, 0, 3);
108        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
109
110        commitText(mConnection, "hel", 1);
111        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hel", 3, 3, -1, -1);
112        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
113    }
114
115    @SmallTest
116    @Feature({"TextInput"})
117    public void testImeCopy() throws Exception {
118        commitText(mConnection, "hello", 1);
119        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
120
121        setSelection(mConnection, 2, 5);
122        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 2, 5, -1, -1);
123
124        copy(mImeAdapter);
125        assertClipboardContents(getActivity(), "llo");
126    }
127
128    @SmallTest
129    @Feature({"TextInput"})
130    public void testEnterTextAndRefocus() throws Exception {
131        commitText(mConnection, "hello", 1);
132        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
133
134        DOMUtils.clickNode(this, mContentView, mCallbackContainer, "input_radio");
135        assertWaitForKeyboardStatus(false);
136
137        DOMUtils.clickNode(this, mContentView, mCallbackContainer, "input_text");
138        assertWaitForKeyboardStatus(true);
139        assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelStart);
140        assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd);
141    }
142
143    @SmallTest
144    @Feature({"TextInput"})
145    public void testImeCut() throws Exception {
146        commitText(mConnection, "snarful", 1);
147        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "snarful", 7, 7, -1, -1);
148
149        setSelection(mConnection, 1, 5);
150        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "snarful", 1, 5, -1, -1);
151
152        cut(mImeAdapter);
153        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "sul", 1, 1, -1, -1);
154
155        assertClipboardContents(getActivity(), "narf");
156    }
157
158    @SmallTest
159    @Feature({"TextInput"})
160    public void testImePaste() throws Exception {
161        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
162            @Override
163            public void run() {
164                ClipboardManager clipboardManager =
165                        (ClipboardManager) getActivity().getSystemService(
166                                Context.CLIPBOARD_SERVICE);
167                clipboardManager.setPrimaryClip(ClipData.newPlainText("blarg", "blarg"));
168            }
169        });
170
171        paste(mImeAdapter);
172        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "blarg", 5, 5, -1, -1);
173
174        setSelection(mConnection, 3, 5);
175        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "blarg", 3, 5, -1, -1);
176
177        paste(mImeAdapter);
178        // Paste is a two step process when there is a non-zero selection.
179        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "bla", 3, 3, -1, -1);
180        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "blablarg", 8, 8, -1, -1);
181
182        paste(mImeAdapter);
183        waitAndVerifyEditableCallback(
184                mConnection.mImeUpdateQueue, 5, "blablargblarg", 13, 13, -1, -1);
185    }
186
187    @SmallTest
188    @Feature({"TextInput"})
189    public void testImeSelectAndUnSelectAll() throws Exception {
190        commitText(mConnection, "hello", 1);
191        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
192
193        selectAll(mImeAdapter);
194        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 0, 5, -1, -1);
195
196        unselect(mImeAdapter);
197        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "", 0, 0, -1, -1);
198
199        assertWaitForKeyboardStatus(false);
200    }
201
202    @SmallTest
203    @Feature({"TextInput", "Main"})
204    public void testShowImeIfNeeded() throws Throwable {
205        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_radio");
206        assertWaitForKeyboardStatus(false);
207
208        performShowImeIfNeeded();
209        assertWaitForKeyboardStatus(false);
210
211        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_text");
212        assertWaitForKeyboardStatus(false);
213
214        performShowImeIfNeeded();
215        assertWaitForKeyboardStatus(true);
216    }
217
218    @SmallTest
219    @Feature({"TextInput", "Main"})
220    public void testFinishComposingText() throws Throwable {
221        // Focus the textarea. We need to do the following steps because we are focusing using JS.
222        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_radio");
223        assertWaitForKeyboardStatus(false);
224        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "textarea");
225        assertWaitForKeyboardStatus(false);
226        performShowImeIfNeeded();
227        assertWaitForKeyboardStatus(true);
228
229        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
230        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
231
232        commitText(mConnection, "hllo", 1);
233        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hllo", 4, 4, -1, -1);
234
235        commitText(mConnection, " ", 1);
236        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hllo ", 5, 5, -1, -1);
237
238        setSelection(mConnection, 1, 1);
239        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hllo ", 1, 1, -1, -1);
240
241        setComposingRegion(mConnection, 0, 4);
242        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hllo ", 1, 1, 0, 4);
243
244        finishComposingText(mConnection);
245        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 5, "hllo ", 1, 1, -1, -1);
246
247        commitText(mConnection, "\n", 1);
248        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 6, "h\nllo ", 2, 2, -1, -1);
249    }
250
251    @SmallTest
252    @Feature({"TextInput", "Main"})
253    public void testEnterKeyEventWhileComposingText() throws Throwable {
254        // Focus the textarea. We need to do the following steps because we are focusing using JS.
255        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_radio");
256        assertWaitForKeyboardStatus(false);
257        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "textarea");
258        assertWaitForKeyboardStatus(false);
259        performShowImeIfNeeded();
260        assertWaitForKeyboardStatus(true);
261
262        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
263        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
264
265        setComposingText(mConnection, "hello", 1);
266        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5);
267
268        getInstrumentation().runOnMainSync(new Runnable() {
269            @Override
270            public void run() {
271                mConnection.sendKeyEvent(
272                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
273                mConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
274            }
275        });
276
277        // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
278        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 5, 5, -1, -1);
279        // The second new line is not a user visible/editable one, it is a side-effect of Blink
280        // using <br> internally.
281        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hello\n\n", 6, 6, -1, -1);
282    }
283
284    private void performShowImeIfNeeded() {
285        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
286            @Override
287            public void run() {
288                mContentView.getContentViewCore().showImeIfNeeded();
289            }
290        });
291    }
292
293    private void performGo(final AdapterInputConnection inputConnection,
294            TestCallbackHelperContainer testCallbackHelperContainer) throws Throwable {
295        handleBlockingCallbackAction(
296                testCallbackHelperContainer.getOnPageFinishedHelper(),
297                new Runnable() {
298                    @Override
299                    public void run() {
300                        inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO);
301                    }
302                });
303    }
304
305    private void assertWaitForKeyboardStatus(final boolean show) throws InterruptedException {
306        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
307            @Override
308            public boolean isSatisfied() {
309                return show == getImeAdapter().mIsShowWithoutHideOutstanding &&
310                        (!show || getAdapterInputConnection() != null);
311            }
312        }));
313    }
314
315    private void waitAndVerifyEditableCallback(final ArrayList<TestImeState> states,
316            final int index, String text, int selectionStart, int selectionEnd,
317            int compositionStart, int compositionEnd) throws InterruptedException {
318        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
319            @Override
320            public boolean isSatisfied() {
321                return states.size() > index;
322            }
323        }));
324        states.get(index).assertEqualState(
325                text, selectionStart, selectionEnd, compositionStart, compositionEnd);
326    }
327
328    private void assertClipboardContents(final Activity activity, final String expectedContents)
329            throws InterruptedException {
330        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
331            @Override
332            public boolean isSatisfied() {
333                return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
334                    @Override
335                    public Boolean call() throws Exception {
336                        ClipboardManager clipboardManager =
337                                (ClipboardManager) activity.getSystemService(
338                                        Context.CLIPBOARD_SERVICE);
339                        ClipData clip = clipboardManager.getPrimaryClip();
340                        return clip != null && clip.getItemCount() == 1
341                                && TextUtils.equals(clip.getItemAt(0).getText(), expectedContents);
342                    }
343                });
344            }
345        }));
346    }
347
348    private ImeAdapter getImeAdapter() {
349        return getContentViewCore().getImeAdapterForTest();
350    }
351
352    private AdapterInputConnection getAdapterInputConnection() {
353        return getContentViewCore().getInputConnectionForTest();
354    }
355
356    private void copy(final ImeAdapter adapter) {
357        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
358            @Override
359            public void run() {
360                adapter.copy();
361            }
362        });
363    }
364
365    private void cut(final ImeAdapter adapter) {
366        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
367            @Override
368            public void run() {
369                adapter.cut();
370            }
371        });
372    }
373
374    private void paste(final ImeAdapter adapter) {
375        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
376            @Override
377            public void run() {
378                adapter.paste();
379            }
380        });
381    }
382
383    private void selectAll(final ImeAdapter adapter) {
384        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
385            @Override
386            public void run() {
387                adapter.selectAll();
388            }
389        });
390    }
391
392    private void unselect(final ImeAdapter adapter) {
393        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
394            @Override
395            public void run() {
396                adapter.unselect();
397            }
398        });
399    }
400
401    private void commitText(final AdapterInputConnection connection, final CharSequence text,
402            final int newCursorPosition) {
403        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
404            @Override
405            public void run() {
406                connection.commitText(text, newCursorPosition);
407            }
408        });
409    }
410
411    private void setSelection(final AdapterInputConnection connection, final int start,
412            final int end) {
413        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
414            @Override
415            public void run() {
416                connection.setSelection(start, end);
417            }
418        });
419    }
420
421    private void setComposingRegion(final AdapterInputConnection connection, final int start,
422            final int end) {
423        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
424            @Override
425            public void run() {
426                connection.setComposingRegion(start, end);
427            }
428        });
429    }
430
431    private void setComposingText(final AdapterInputConnection connection, final CharSequence text,
432            final int newCursorPosition) {
433        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
434            @Override
435            public void run() {
436                connection.setComposingText(text, newCursorPosition);
437            }
438        });
439    }
440
441    private void finishComposingText(final AdapterInputConnection connection) {
442        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
443            @Override
444            public void run() {
445                connection.finishComposingText();
446            }
447        });
448    }
449
450    private static class TestAdapterInputConnectionFactory extends
451            ImeAdapter.AdapterInputConnectionFactory {
452        @Override
453        public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
454                EditorInfo outAttrs) {
455            return new TestAdapterInputConnection(view, imeAdapter, outAttrs);
456        }
457    }
458
459    private static class TestAdapterInputConnection extends AdapterInputConnection {
460        private final ArrayList<TestImeState> mImeUpdateQueue = new ArrayList<TestImeState>();
461
462        public TestAdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
463            super(view, imeAdapter, outAttrs);
464        }
465
466        @Override
467        public void updateState(String text, int selectionStart, int selectionEnd,
468                int compositionStart, int compositionEnd, boolean requiredAck) {
469            mImeUpdateQueue.add(new TestImeState(text, selectionStart, selectionEnd,
470                    compositionStart, compositionEnd));
471            super.updateState(text, selectionStart, selectionEnd, compositionStart,
472                    compositionEnd, requiredAck);
473        }
474    }
475
476    private static class TestImeState {
477        private final String mText;
478        private final int mSelectionStart;
479        private final int mSelectionEnd;
480        private final int mCompositionStart;
481        private final int mCompositionEnd;
482
483        public TestImeState(String text, int selectionStart, int selectionEnd,
484                int compositionStart, int compositionEnd) {
485            mText = text;
486            mSelectionStart = selectionStart;
487            mSelectionEnd = selectionEnd;
488            mCompositionStart = compositionStart;
489            mCompositionEnd = compositionEnd;
490        }
491
492        public void assertEqualState(String text, int selectionStart, int selectionEnd,
493                int compositionStart, int compositionEnd) {
494            assertEquals("Text did not match", text, mText);
495            assertEquals("Selection start did not match", selectionStart, mSelectionStart);
496            assertEquals("Selection end did not match", selectionEnd, mSelectionEnd);
497            assertEquals("Composition start did not match", compositionStart, mCompositionStart);
498            assertEquals("Composition end did not match", compositionEnd, mCompositionEnd);
499        }
500    }
501}
502