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