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