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