ImeTest.java revision 4e180b6a0b4720a9b8e9e959a882386f690f08ff
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.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\" style=\"width:50px;height:50px\" />" +
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 testShowImeIfNeeded() throws Throwable {
200        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_radio");
201        assertWaitForKeyboardStatus(false);
202
203        performShowImeIfNeeded();
204        assertWaitForKeyboardStatus(false);
205
206        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_text");
207        assertWaitForKeyboardStatus(false);
208
209        performShowImeIfNeeded();
210        assertWaitForKeyboardStatus(true);
211    }
212
213    @SmallTest
214    @Feature({"TextInput", "Main"})
215    public void testFinishComposingText() throws Throwable {
216        // Focus the textarea. We need to do the following steps because we are focusing using JS.
217        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_radio");
218        assertWaitForKeyboardStatus(false);
219        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "textarea");
220        assertWaitForKeyboardStatus(false);
221        performShowImeIfNeeded();
222        assertWaitForKeyboardStatus(true);
223
224        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
225        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
226
227        mConnection.commitText("hllo", 1);
228        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hllo", 4, 4, -1, -1);
229
230        mConnection.commitText(" ", 1);
231        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hllo ", 5, 5, -1, -1);
232
233        mConnection.setSelection(1, 1);
234        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hllo ", 1, 1, -1, -1);
235
236        mConnection.setComposingRegion(0, 4);
237        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hllo ", 1, 1, 0, 4);
238
239        mConnection.finishComposingText();
240        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 5, "hllo ", 1, 1, -1, -1);
241
242        mConnection.commitText("\n", 1);
243        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 6, "h\nllo ", 2, 2, -1, -1);
244    }
245
246    @SmallTest
247    @Feature({"TextInput", "Main"})
248    public void testEnterKeyEventWhileComposingText() throws Throwable {
249        // Focus the textarea. We need to do the following steps because we are focusing using JS.
250        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_radio");
251        assertWaitForKeyboardStatus(false);
252        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "textarea");
253        assertWaitForKeyboardStatus(false);
254        performShowImeIfNeeded();
255        assertWaitForKeyboardStatus(true);
256
257        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
258        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
259
260        mConnection.setComposingText("hello", 1);
261        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5);
262
263        getInstrumentation().runOnMainSync(new Runnable() {
264            @Override
265            public void run() {
266                mConnection.sendKeyEvent(
267                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
268                mConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
269            }
270        });
271
272        // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
273        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 5, 5, -1, -1);
274        // The second new line is not a user visible/editable one, it is a side-effect of Blink
275        // using <br> internally.
276        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hello\n\n", 6, 6, -1, -1);
277    }
278
279    private void performShowImeIfNeeded() {
280        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
281            @Override
282            public void run() {
283                mContentView.getContentViewCore().showImeIfNeeded();
284            }
285        });
286    }
287
288    private void performGo(final AdapterInputConnection inputConnection,
289            TestCallbackHelperContainer testCallbackHelperContainer) throws Throwable {
290        handleBlockingCallbackAction(
291                testCallbackHelperContainer.getOnPageFinishedHelper(),
292                new Runnable() {
293                    @Override
294                    public void run() {
295                        inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO);
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 ImeAdapter getImeAdapter() {
344        return getContentViewCore().getImeAdapterForTest();
345    }
346
347    private AdapterInputConnection getAdapterInputConnection() {
348        return getContentViewCore().getInputConnectionForTest();
349    }
350
351    private static class TestAdapterInputConnectionFactory extends
352            ImeAdapter.AdapterInputConnectionFactory {
353        @Override
354        public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
355                EditorInfo outAttrs) {
356            return new TestAdapterInputConnection(view, imeAdapter, outAttrs);
357        }
358    }
359
360    private static class TestAdapterInputConnection extends AdapterInputConnection {
361        private final ArrayList<TestImeState> mImeUpdateQueue = new ArrayList<TestImeState>();
362
363        public TestAdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
364            super(view, imeAdapter, outAttrs);
365        }
366
367        @Override
368        public void updateState(String text, int selectionStart, int selectionEnd,
369                int compositionStart, int compositionEnd, boolean requiredAck) {
370            mImeUpdateQueue.add(new TestImeState(text, selectionStart, selectionEnd,
371                    compositionStart, compositionEnd));
372            super.updateState(text, selectionStart, selectionEnd, compositionStart,
373                    compositionEnd, requiredAck);
374        }
375    }
376
377    private static class TestImeState {
378        private final String mText;
379        private final int mSelectionStart;
380        private final int mSelectionEnd;
381        private final int mCompositionStart;
382        private final int mCompositionEnd;
383
384        public TestImeState(String text, int selectionStart, int selectionEnd,
385                int compositionStart, int compositionEnd) {
386            mText = text;
387            mSelectionStart = selectionStart;
388            mSelectionEnd = selectionEnd;
389            mCompositionStart = compositionStart;
390            mCompositionEnd = compositionEnd;
391        }
392
393        public void assertEqualState(String text, int selectionStart, int selectionEnd,
394                int compositionStart, int compositionEnd) {
395            assertEquals("Text did not match", text, mText);
396            assertEquals("Selection start did not match", selectionStart, mSelectionStart);
397            assertEquals("Selection end did not match", selectionEnd, mSelectionEnd);
398            assertEquals("Composition start did not match", compositionStart, mCompositionStart);
399            assertEquals("Composition end did not match", compositionEnd, mCompositionEnd);
400        }
401    }
402}
403