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