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