ImeTest.java revision 7dbb3d5cf0c15f500944d211057644d6a2f37371
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        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 5, "hllo ", 1, 1, -1, -1);
263
264        mConnection.commitText("\n", 1);
265        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 6, "h\nllo ", 2, 2, -1, -1);
266    }
267
268    private void performShowImeIfNeeded() {
269        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
270            @Override
271            public void run() {
272                mContentView.getContentViewCore().showImeIfNeeded();
273            }
274        });
275    }
276
277    private void performGo(final AdapterInputConnection inputConnection,
278            TestCallbackHelperContainer testCallbackHelperContainer) throws Throwable {
279        handleBlockingCallbackAction(
280                testCallbackHelperContainer.getOnPageFinishedHelper(),
281                new Runnable() {
282                    @Override
283                    public void run() {
284                        inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO);
285                    }
286                });
287    }
288
289
290
291    private void assertWaitForPageScaleFactor(final float scale) throws InterruptedException {
292        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
293            @Override
294            public boolean isSatisfied() {
295                return getContentViewCore().getScale() == scale;
296            }
297        }));
298    }
299
300    private void assertWaitForKeyboardStatus(final boolean show) throws InterruptedException {
301        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
302            @Override
303            public boolean isSatisfied() {
304                return show == getImeAdapter().mIsShowWithoutHideOutstanding &&
305                        (!show || getAdapterInputConnection() != null);
306            }
307        }));
308    }
309
310    private void waitAndVerifyEditableCallback(final ArrayList<TestImeState> states,
311            final int index, String text, int selectionStart, int selectionEnd,
312            int compositionStart, int compositionEnd) throws InterruptedException {
313        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
314            @Override
315            public boolean isSatisfied() {
316                return states.size() > index;
317            }
318        }));
319        states.get(index).assertEqualState(
320                text, selectionStart, selectionEnd, compositionStart, compositionEnd);
321    }
322
323    private void assertClipboardContents(final Activity activity, final String expectedContents)
324            throws InterruptedException {
325        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
326            @Override
327            public boolean isSatisfied() {
328                return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
329                    @Override
330                    public Boolean call() throws Exception {
331                        ClipboardManager clipboardManager =
332                                (ClipboardManager) activity.getSystemService(
333                                        Context.CLIPBOARD_SERVICE);
334                        ClipData clip = clipboardManager.getPrimaryClip();
335                        return clip != null && clip.getItemCount() == 1
336                                && TextUtils.equals(clip.getItemAt(0).getText(), expectedContents);
337                    }
338                });
339            }
340        }));
341    }
342
343    private void assertWaitForSetIgnoreUpdates(final boolean ignore,
344            final TestAdapterInputConnection connection) throws Throwable {
345        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
346            @Override
347            public boolean isSatisfied() {
348                return ignore == connection.isIgnoringTextInputStateUpdates();
349            }
350        }));
351    }
352
353    private ImeAdapter getImeAdapter() {
354        return getContentViewCore().getImeAdapterForTest();
355    }
356
357    private AdapterInputConnection getAdapterInputConnection() {
358        return getContentViewCore().getInputConnectionForTest();
359    }
360
361    private static class TestAdapterInputConnectionFactory extends
362            ImeAdapter.AdapterInputConnectionFactory {
363        @Override
364        public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
365                EditorInfo outAttrs) {
366            return new TestAdapterInputConnection(view, imeAdapter, outAttrs);
367        }
368    }
369
370    private static class TestAdapterInputConnection extends AdapterInputConnection {
371        private int mUpdateSelectionCounter = 0;
372        private ArrayList<TestImeState> mImeUpdateQueue = new ArrayList<ImeTest.TestImeState>();
373
374        public TestAdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
375            super(view, imeAdapter, outAttrs);
376        }
377
378        @Override
379        public void setEditableText(String text, int selectionStart, int selectionEnd,
380                int compositionStart, int compositionEnd) {
381            mImeUpdateQueue.add(new TestImeState(text, selectionStart, selectionEnd,
382                    compositionStart, compositionEnd));
383            super.setEditableText(
384                    text, selectionStart, selectionEnd, compositionStart, compositionEnd);
385        }
386
387        @Override
388        protected void updateSelection(
389                int selectionStart, int selectionEnd,
390                int compositionStart, int compositionEnd) {
391            mUpdateSelectionCounter++;
392        }
393    }
394
395    private static class TestImeState {
396        private final String mText;
397        private final int mSelectionStart;
398        private final int mSelectionEnd;
399        private final int mCompositionStart;
400        private final int mCompositionEnd;
401
402        public TestImeState(String text, int selectionStart, int selectionEnd,
403                int compositionStart, int compositionEnd) {
404            mText = text;
405            mSelectionStart = selectionStart;
406            mSelectionEnd = selectionEnd;
407            mCompositionStart = compositionStart;
408            mCompositionEnd = compositionEnd;
409        }
410
411        public void assertEqualState(String text, int selectionStart, int selectionEnd,
412                int compositionStart, int compositionEnd) {
413            assertEquals("Text did not match", text, mText);
414            assertEquals("Selection start did not match", selectionStart, mSelectionStart);
415            assertEquals("Selection end did not match", selectionEnd, mSelectionEnd);
416            assertEquals("Composition start did not match", compositionStart, mCompositionStart);
417            assertEquals("Composition end did not match", compositionEnd, mCompositionEnd);
418        }
419    }
420}
421