ImeTest.java revision 0529e5d033099cbfc42635f6f6183833b09dff6e
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.TextUtils;
15import android.view.KeyEvent;
16import android.view.View;
17import android.view.inputmethod.EditorInfo;
18
19import org.chromium.base.ThreadUtils;
20import org.chromium.base.test.util.Feature;
21import org.chromium.base.test.util.UrlUtils;
22import org.chromium.content.browser.ContentViewCore;
23import org.chromium.content.browser.test.util.Criteria;
24import org.chromium.content.browser.test.util.CriteriaHelper;
25import org.chromium.content.browser.test.util.DOMUtils;
26import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
27import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper;
28import org.chromium.content_shell_apk.ContentShellTestBase;
29
30import java.util.ArrayList;
31import java.util.concurrent.Callable;
32
33/**
34 * Integration tests for text input using cases based on fixed regressions.
35 */
36public class ImeTest extends ContentShellTestBase {
37
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>");
46
47    private TestAdapterInputConnection mConnection;
48    private ImeAdapter mImeAdapter;
49
50    private ContentViewCore mContentViewCore;
51    private TestCallbackHelperContainer mCallbackContainer;
52    private TestInputMethodManagerWrapper mInputMethodManagerWrapper;
53
54    @Override
55    public void setUp() throws Exception {
56        super.setUp();
57
58        launchContentShellWithUrl(DATA_URL);
59        assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
60        mContentViewCore = getContentViewCore();
61
62        mInputMethodManagerWrapper = new TestInputMethodManagerWrapper(mContentViewCore);
63        getImeAdapter().setInputMethodManagerWrapper(mInputMethodManagerWrapper);
64        assertEquals(0, mInputMethodManagerWrapper.getShowSoftInputCounter());
65        mContentViewCore.setAdapterInputConnectionFactory(
66                new TestAdapterInputConnectionFactory());
67
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);
75
76        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
77        mImeAdapter = getImeAdapter();
78
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);
83    }
84
85    @MediumTest
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);
90
91        performGo(getAdapterInputConnection(), mCallbackContainer);
92
93        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "", 0, 0, -1, -1);
94        assertWaitForKeyboardStatus(false);
95    }
96
97    @SmallTest
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());
103
104        setComposingText(mConnection, "he", 1);
105        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "he", 2, 2, 0, 2);
106        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
107
108        setComposingText(mConnection, "hel", 1);
109        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hel", 3, 3, 0, 3);
110        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
111
112        commitText(mConnection, "hel", 1);
113        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hel", 3, 3, -1, -1);
114        assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter());
115    }
116
117    @SmallTest
118    @Feature({"TextInput"})
119    public void testImeCopy() throws Exception {
120        commitText(mConnection, "hello", 1);
121        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
122
123        setSelection(mConnection, 2, 5);
124        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 2, 5, -1, -1);
125
126        copy(mImeAdapter);
127        assertClipboardContents(getActivity(), "llo");
128    }
129
130    @SmallTest
131    @Feature({"TextInput"})
132    public void testEnterTextAndRefocus() throws Exception {
133        commitText(mConnection, "hello", 1);
134        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
135
136        DOMUtils.clickNode(this, mContentViewCore, "input_radio");
137        assertWaitForKeyboardStatus(false);
138
139        DOMUtils.clickNode(this, mContentViewCore, "input_text");
140        assertWaitForKeyboardStatus(true);
141        assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelStart);
142        assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd);
143    }
144
145    @SmallTest
146    @Feature({"TextInput"})
147    public void testImeCut() throws Exception {
148        commitText(mConnection, "snarful", 1);
149        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "snarful", 7, 7, -1, -1);
150
151        setSelection(mConnection, 1, 5);
152        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "snarful", 1, 5, -1, -1);
153
154        cut(mImeAdapter);
155        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "sul", 1, 1, -1, -1);
156
157        assertClipboardContents(getActivity(), "narf");
158    }
159
160    @SmallTest
161    @Feature({"TextInput"})
162    public void testImePaste() throws Exception {
163        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
164            @Override
165            public void run() {
166                ClipboardManager clipboardManager =
167                        (ClipboardManager) getActivity().getSystemService(
168                                Context.CLIPBOARD_SERVICE);
169                clipboardManager.setPrimaryClip(ClipData.newPlainText("blarg", "blarg"));
170            }
171        });
172
173        paste(mImeAdapter);
174        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "blarg", 5, 5, -1, -1);
175
176        setSelection(mConnection, 3, 5);
177        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "blarg", 3, 5, -1, -1);
178
179        paste(mImeAdapter);
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);
183
184        paste(mImeAdapter);
185        waitAndVerifyEditableCallback(
186                mConnection.mImeUpdateQueue, 5, "blablargblarg", 13, 13, -1, -1);
187    }
188
189    @SmallTest
190    @Feature({"TextInput"})
191    public void testImeSelectAndUnSelectAll() throws Exception {
192        commitText(mConnection, "hello", 1);
193        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
194
195        selectAll(mImeAdapter);
196        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 0, 5, -1, -1);
197
198        unselect(mImeAdapter);
199        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "", 0, 0, -1, -1);
200
201        assertWaitForKeyboardStatus(false);
202    }
203
204    @SmallTest
205    @Feature({"TextInput", "Main"})
206    public void testShowImeIfNeeded() throws Throwable {
207        DOMUtils.focusNode(mContentViewCore, "input_radio");
208        assertWaitForKeyboardStatus(false);
209
210        performShowImeIfNeeded();
211        assertWaitForKeyboardStatus(false);
212
213        DOMUtils.focusNode(mContentViewCore, "input_text");
214        assertWaitForKeyboardStatus(false);
215
216        performShowImeIfNeeded();
217        assertWaitForKeyboardStatus(true);
218    }
219
220    @SmallTest
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);
230
231        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
232        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
233
234        commitText(mConnection, "hllo", 1);
235        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hllo", 4, 4, -1, -1);
236
237        commitText(mConnection, " ", 1);
238        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hllo ", 5, 5, -1, -1);
239
240        setSelection(mConnection, 1, 1);
241        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hllo ", 1, 1, -1, -1);
242
243        setComposingRegion(mConnection, 0, 4);
244        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hllo ", 1, 1, 0, 4);
245
246        finishComposingText(mConnection);
247        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 5, "hllo ", 1, 1, -1, -1);
248
249        commitText(mConnection, "\n", 1);
250        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 6, "h\nllo ", 2, 2, -1, -1);
251    }
252
253    @SmallTest
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);
263
264        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
265        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
266
267        setComposingText(mConnection, "hello", 1);
268        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5);
269
270        getInstrumentation().runOnMainSync(new Runnable() {
271            @Override
272            public void run() {
273                mConnection.sendKeyEvent(
274                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
275                mConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
276            }
277        });
278
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);
284    }
285
286    private void performShowImeIfNeeded() {
287        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
288            @Override
289            public void run() {
290                mContentViewCore.showImeIfNeeded();
291            }
292        });
293    }
294
295    private void performGo(final AdapterInputConnection inputConnection,
296            TestCallbackHelperContainer testCallbackHelperContainer) throws Throwable {
297        handleBlockingCallbackAction(
298                testCallbackHelperContainer.getOnPageFinishedHelper(),
299                new Runnable() {
300                    @Override
301                    public void run() {
302                        inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO);
303                    }
304                });
305    }
306
307    private void assertWaitForKeyboardStatus(final boolean show) throws InterruptedException {
308        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
309            @Override
310            public boolean isSatisfied() {
311                return show == getImeAdapter().mIsShowWithoutHideOutstanding &&
312                        (!show || getAdapterInputConnection() != null);
313            }
314        }));
315    }
316
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() {
321            @Override
322            public boolean isSatisfied() {
323                return states.size() > index;
324            }
325        }));
326        states.get(index).assertEqualState(
327                text, selectionStart, selectionEnd, compositionStart, compositionEnd);
328    }
329
330    private void assertClipboardContents(final Activity activity, final String expectedContents)
331            throws InterruptedException {
332        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
333            @Override
334            public boolean isSatisfied() {
335                return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
336                    @Override
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);
344                    }
345                });
346            }
347        }));
348    }
349
350    private ImeAdapter getImeAdapter() {
351        return mContentViewCore.getImeAdapterForTest();
352    }
353
354    private AdapterInputConnection getAdapterInputConnection() {
355        return mContentViewCore.getInputConnectionForTest();
356    }
357
358    private void copy(final ImeAdapter adapter) {
359        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
360            @Override
361            public void run() {
362                adapter.copy();
363            }
364        });
365    }
366
367    private void cut(final ImeAdapter adapter) {
368        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
369            @Override
370            public void run() {
371                adapter.cut();
372            }
373        });
374    }
375
376    private void paste(final ImeAdapter adapter) {
377        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
378            @Override
379            public void run() {
380                adapter.paste();
381            }
382        });
383    }
384
385    private void selectAll(final ImeAdapter adapter) {
386        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
387            @Override
388            public void run() {
389                adapter.selectAll();
390            }
391        });
392    }
393
394    private void unselect(final ImeAdapter adapter) {
395        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
396            @Override
397            public void run() {
398                adapter.unselect();
399            }
400        });
401    }
402
403    private void commitText(final AdapterInputConnection connection, final CharSequence text,
404            final int newCursorPosition) {
405        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
406            @Override
407            public void run() {
408                connection.commitText(text, newCursorPosition);
409            }
410        });
411    }
412
413    private void setSelection(final AdapterInputConnection connection, final int start,
414            final int end) {
415        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
416            @Override
417            public void run() {
418                connection.setSelection(start, end);
419            }
420        });
421    }
422
423    private void setComposingRegion(final AdapterInputConnection connection, final int start,
424            final int end) {
425        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
426            @Override
427            public void run() {
428                connection.setComposingRegion(start, end);
429            }
430        });
431    }
432
433    private void setComposingText(final AdapterInputConnection connection, final CharSequence text,
434            final int newCursorPosition) {
435        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
436            @Override
437            public void run() {
438                connection.setComposingText(text, newCursorPosition);
439            }
440        });
441    }
442
443    private void finishComposingText(final AdapterInputConnection connection) {
444        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
445            @Override
446            public void run() {
447                connection.finishComposingText();
448            }
449        });
450    }
451
452    private static class TestAdapterInputConnectionFactory extends
453            ImeAdapter.AdapterInputConnectionFactory {
454        @Override
455        public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
456                Editable editable, EditorInfo outAttrs) {
457            return new TestAdapterInputConnection(view, imeAdapter, editable, outAttrs);
458        }
459    }
460
461    private static class TestAdapterInputConnection extends AdapterInputConnection {
462        private final ArrayList<TestImeState> mImeUpdateQueue = new ArrayList<TestImeState>();
463
464        public TestAdapterInputConnection(View view, ImeAdapter imeAdapter,
465                Editable editable, EditorInfo outAttrs) {
466            super(view, imeAdapter, editable, outAttrs);
467        }
468
469        @Override
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);
476        }
477    }
478
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;
485
486        public TestImeState(String text, int selectionStart, int selectionEnd,
487                int compositionStart, int compositionEnd) {
488            mText = text;
489            mSelectionStart = selectionStart;
490            mSelectionEnd = selectionEnd;
491            mCompositionStart = compositionStart;
492            mCompositionEnd = compositionEnd;
493        }
494
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);
502        }
503    }
504}
505