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.Editable; 14import android.text.Selection; 15import android.text.TextUtils; 16import android.view.KeyEvent; 17import android.view.View; 18import android.view.inputmethod.EditorInfo; 19 20import org.chromium.base.ThreadUtils; 21import org.chromium.base.test.util.DisabledTest; 22import org.chromium.base.test.util.Feature; 23import org.chromium.base.test.util.UrlUtils; 24import org.chromium.content.browser.ContentViewCore; 25import org.chromium.content.browser.test.util.Criteria; 26import org.chromium.content.browser.test.util.CriteriaHelper; 27import org.chromium.content.browser.test.util.DOMUtils; 28import org.chromium.content.browser.test.util.TestCallbackHelperContainer; 29import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper; 30import org.chromium.content_shell_apk.ContentShellTestBase; 31 32import java.util.ArrayList; 33import java.util.concurrent.Callable; 34 35/** 36 * Integration tests for text input using cases based on fixed regressions. 37 */ 38public class ImeTest extends ContentShellTestBase { 39 40 private static final String DATA_URL = UrlUtils.encodeHtmlDataUri( 41 "<html><head><meta name=\"viewport\"" + 42 "content=\"width=device-width, initial-scale=2.0, maximum-scale=2.0\" /></head>" + 43 "<body><form action=\"about:blank\">" + 44 "<input id=\"input_text\" type=\"text\" /><br/>" + 45 "<input id=\"input_radio\" type=\"radio\" style=\"width:50px;height:50px\" />" + 46 "<br/><textarea id=\"textarea\" rows=\"4\" cols=\"20\"></textarea>" + 47 "<br/><p><span id=\"plain_text\">This is Plain Text One</span></p>" + 48 "</form></body></html>"); 49 50 private TestAdapterInputConnection mConnection; 51 private ImeAdapter mImeAdapter; 52 53 private ContentViewCore mContentViewCore; 54 private TestCallbackHelperContainer mCallbackContainer; 55 private TestInputMethodManagerWrapper mInputMethodManagerWrapper; 56 57 @Override 58 public void setUp() throws Exception { 59 super.setUp(); 60 61 launchContentShellWithUrl(DATA_URL); 62 assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading()); 63 mContentViewCore = getContentViewCore(); 64 65 mInputMethodManagerWrapper = new TestInputMethodManagerWrapper(mContentViewCore); 66 getImeAdapter().setInputMethodManagerWrapper(mInputMethodManagerWrapper); 67 assertEquals(0, mInputMethodManagerWrapper.getShowSoftInputCounter()); 68 mContentViewCore.setAdapterInputConnectionFactory( 69 new TestAdapterInputConnectionFactory()); 70 71 mCallbackContainer = new TestCallbackHelperContainer(mContentViewCore); 72 // TODO(aurimas) remove this wait once crbug.com/179511 is fixed. 73 assertWaitForPageScaleFactorMatch(2); 74 assertTrue(DOMUtils.waitForNonZeroNodeBounds( 75 mContentViewCore, "input_text")); 76 DOMUtils.clickNode(this, mContentViewCore, "input_text"); 77 assertWaitForKeyboardStatus(true); 78 79 mConnection = (TestAdapterInputConnection) getAdapterInputConnection(); 80 mImeAdapter = getImeAdapter(); 81 82 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1); 83 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter()); 84 assertEquals(0, mInputMethodManagerWrapper.getEditorInfo().initialSelStart); 85 assertEquals(0, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd); 86 } 87 88 @MediumTest 89 @Feature({"TextInput", "Main"}) 90 public void testKeyboardDismissedAfterClickingGo() throws Throwable { 91 setComposingText(mConnection, "hello", 1); 92 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5); 93 94 performGo(getAdapterInputConnection(), mCallbackContainer); 95 96 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "", 0, 0, -1, -1); 97 assertWaitForKeyboardStatus(false); 98 } 99 100 @SmallTest 101 @Feature({"TextInput", "Main"}) 102 @RerunWithUpdatedContainerView 103 public void testGetTextUpdatesAfterEnteringText() throws Throwable { 104 setComposingText(mConnection, "h", 1); 105 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "h", 1, 1, 0, 1); 106 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter()); 107 108 setComposingText(mConnection, "he", 1); 109 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "he", 2, 2, 0, 2); 110 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter()); 111 112 setComposingText(mConnection, "hel", 1); 113 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hel", 3, 3, 0, 3); 114 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter()); 115 116 commitText(mConnection, "hel", 1); 117 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hel", 3, 3, -1, -1); 118 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter()); 119 } 120 121 @SmallTest 122 @Feature({"TextInput"}) 123 @RerunWithUpdatedContainerView 124 public void testImeCopy() throws Exception { 125 commitText(mConnection, "hello", 1); 126 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1); 127 128 setSelection(mConnection, 2, 5); 129 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 2, 5, -1, -1); 130 131 copy(mImeAdapter); 132 assertClipboardContents(getActivity(), "llo"); 133 } 134 135 @SmallTest 136 @Feature({"TextInput"}) 137 public void testEnterTextAndRefocus() throws Exception { 138 commitText(mConnection, "hello", 1); 139 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1); 140 141 DOMUtils.clickNode(this, mContentViewCore, "input_radio"); 142 assertWaitForKeyboardStatus(false); 143 144 DOMUtils.clickNode(this, mContentViewCore, "input_text"); 145 assertWaitForKeyboardStatus(true); 146 assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelStart); 147 assertEquals(5, mInputMethodManagerWrapper.getEditorInfo().initialSelEnd); 148 } 149 150 @SmallTest 151 @Feature({"TextInput"}) 152 public void testKeyboardNotDismissedAfterCopySelection() throws Exception { 153 commitText(mConnection, "Sample Text", 1); 154 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, 155 "Sample Text", 11, 11, -1, -1); 156 DOMUtils.clickNode(this, mContentViewCore, "input_text"); 157 assertWaitForKeyboardStatus(true); 158 DOMUtils.longPressNode(this, mContentViewCore, "input_text"); 159 selectAll(mImeAdapter); 160 copy(mImeAdapter); 161 assertWaitForKeyboardStatus(true); 162 assertEquals(11, Selection.getSelectionEnd(mContentViewCore.getEditableForTest())); 163 } 164 165 @SmallTest 166 @Feature({"TextInput"}) 167 public void testImeNotDismissedAfterCutSelection() throws Exception { 168 commitText(mConnection, "Sample Text", 1); 169 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, 170 "Sample Text", 11, 11, -1, -1); 171 DOMUtils.longPressNode(this, mContentViewCore, "input_text"); 172 assertWaitForSelectActionBarStatus(true); 173 assertWaitForKeyboardStatus(true); 174 cut(mImeAdapter); 175 assertWaitForKeyboardStatus(true); 176 assertWaitForSelectActionBarStatus(false); 177 } 178 179 @SmallTest 180 @Feature({"TextInput"}) 181 public void testImeNotShownOnLongPressingEmptyInput() throws Exception { 182 DOMUtils.focusNode(mContentViewCore, "input_radio"); 183 DOMUtils.longPressNode(this, mContentViewCore, "input_text"); 184 assertWaitForKeyboardStatus(false); 185 commitText(mConnection, "Sample Text", 1); 186 DOMUtils.longPressNode(this, mContentViewCore, "input_text"); 187 assertWaitForKeyboardStatus(true); 188 } 189 190 @SmallTest 191 @Feature({"TextInput"}) 192 public void testSelectActionBarShownOnLongPressingInput() throws Exception { 193 DOMUtils.longPressNode(this, mContentViewCore, "input_text"); 194 assertWaitForSelectActionBarStatus(false); 195 commitText(mConnection, "Sample Text", 1); 196 DOMUtils.longPressNode(this, mContentViewCore, "input_text"); 197 assertWaitForSelectActionBarStatus(true); 198 } 199 200 /* 201 @SmallTest 202 @Feature({"TextInput"}) 203 */ 204 @DisabledTest 205 public void testSelectActionBarClearedOnTappingInput() throws Exception { 206 commitText(mConnection, "Sample Text", 1); 207 DOMUtils.longPressNode(this, mContentViewCore, "input_text"); 208 assertWaitForKeyboardStatus(true); 209 assertWaitForSelectActionBarStatus(true); 210 DOMUtils.clickNode(this, mContentViewCore, "input_text"); 211 assertWaitForSelectActionBarStatus(false); 212 } 213 214 @SmallTest 215 @Feature({"TextInput"}) 216 public void testSelectActionBarClearedOnTappingOutsideInput() throws Exception { 217 commitText(mConnection, "Sample Text", 1); 218 DOMUtils.longPressNode(this, mContentViewCore, "input_text"); 219 assertWaitForKeyboardStatus(true); 220 assertWaitForSelectActionBarStatus(true); 221 DOMUtils.clickNode(this, mContentViewCore, "input_radio"); 222 assertWaitForKeyboardStatus(false); 223 assertWaitForSelectActionBarStatus(false); 224 } 225 226 @SmallTest 227 @Feature({"TextInput"}) 228 public void testImeNotShownOnLongPressingDifferentEmptyInputs() throws Exception { 229 DOMUtils.focusNode(mContentViewCore, "input_radio"); 230 DOMUtils.longPressNode(this, mContentViewCore, "input_text"); 231 assertWaitForKeyboardStatus(false); 232 DOMUtils.longPressNode(this, mContentViewCore, "textarea"); 233 assertWaitForKeyboardStatus(false); 234 } 235 236 @SmallTest 237 @Feature({"TextInput"}) 238 public void testImeStaysOnLongPressingDifferentNonEmptyInputs() throws Exception { 239 DOMUtils.focusNode(mContentViewCore, "input_text"); 240 assertWaitForKeyboardStatus(true); 241 commitText(mConnection, "Sample Text", 1); 242 DOMUtils.focusNode(mContentViewCore, "textarea"); 243 commitText(mConnection, "Sample Text", 1); 244 DOMUtils.longPressNode(this, mContentViewCore, "input_text"); 245 assertWaitForKeyboardStatus(true); 246 DOMUtils.longPressNode(this, mContentViewCore, "textarea"); 247 assertWaitForKeyboardStatus(true); 248 } 249 250 @SmallTest 251 @Feature({"TextInput"}) 252 public void testImeCut() throws Exception { 253 commitText(mConnection, "snarful", 1); 254 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "snarful", 7, 7, -1, -1); 255 256 setSelection(mConnection, 1, 5); 257 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "snarful", 1, 5, -1, -1); 258 259 cut(mImeAdapter); 260 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "sul", 1, 1, -1, -1); 261 262 assertClipboardContents(getActivity(), "narf"); 263 } 264 265 @SmallTest 266 @Feature({"TextInput"}) 267 public void testImePaste() throws Exception { 268 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 269 @Override 270 public void run() { 271 ClipboardManager clipboardManager = 272 (ClipboardManager) getActivity().getSystemService( 273 Context.CLIPBOARD_SERVICE); 274 clipboardManager.setPrimaryClip(ClipData.newPlainText("blarg", "blarg")); 275 } 276 }); 277 278 paste(mImeAdapter); 279 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "blarg", 5, 5, -1, -1); 280 281 setSelection(mConnection, 3, 5); 282 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "blarg", 3, 5, -1, -1); 283 284 paste(mImeAdapter); 285 // Paste is a two step process when there is a non-zero selection. 286 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "bla", 3, 3, -1, -1); 287 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "blablarg", 8, 8, -1, -1); 288 289 paste(mImeAdapter); 290 waitAndVerifyEditableCallback( 291 mConnection.mImeUpdateQueue, 5, "blablargblarg", 13, 13, -1, -1); 292 } 293 294 @SmallTest 295 @Feature({"TextInput"}) 296 public void testImeSelectAndUnSelectAll() throws Exception { 297 commitText(mConnection, "hello", 1); 298 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1); 299 300 selectAll(mImeAdapter); 301 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 0, 5, -1, -1); 302 303 unselect(mImeAdapter); 304 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "", 0, 0, -1, -1); 305 306 assertWaitForKeyboardStatus(false); 307 } 308 309 @SmallTest 310 @Feature({"TextInput", "Main"}) 311 public void testShowImeIfNeeded() throws Throwable { 312 // showImeIfNeeded() is now implicitly called by the updated focus 313 // heuristic so no need to call explicitly. http://crbug.com/371927 314 DOMUtils.focusNode(mContentViewCore, "input_radio"); 315 assertWaitForKeyboardStatus(false); 316 317 DOMUtils.focusNode(mContentViewCore, "input_text"); 318 assertWaitForKeyboardStatus(true); 319 } 320 321 /* 322 @SmallTest 323 @Feature({"TextInput", "Main"}) 324 */ 325 @DisabledTest 326 public void testFinishComposingText() throws Throwable { 327 DOMUtils.focusNode(mContentViewCore, "input_radio"); 328 assertWaitForKeyboardStatus(false); 329 DOMUtils.focusNode(mContentViewCore, "textarea"); 330 assertWaitForKeyboardStatus(true); 331 332 mConnection = (TestAdapterInputConnection) getAdapterInputConnection(); 333 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1); 334 335 commitText(mConnection, "hllo", 1); 336 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hllo", 4, 4, -1, -1); 337 338 commitText(mConnection, " ", 1); 339 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hllo ", 5, 5, -1, -1); 340 341 setSelection(mConnection, 1, 1); 342 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hllo ", 1, 1, -1, -1); 343 344 setComposingRegion(mConnection, 0, 4); 345 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hllo ", 1, 1, 0, 4); 346 347 finishComposingText(mConnection); 348 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 5, "hllo ", 1, 1, -1, -1); 349 350 commitText(mConnection, "\n", 1); 351 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 6, "h\nllo ", 2, 2, -1, -1); 352 } 353 354 private int getTypedKeycodeGuess(String before, String after) { 355 KeyEvent ev = ImeAdapter.getTypedKeyEventGuess(before, after); 356 if (ev == null) return -1; 357 return ev.getKeyCode(); 358 } 359 360 @SmallTest 361 @Feature({"TextInput", "Main"}) 362 public void testGuessedKeyCodeFromTyping() throws Throwable { 363 assertEquals(-1, getTypedKeycodeGuess(null, "")); 364 assertEquals(KeyEvent.KEYCODE_X, getTypedKeycodeGuess(null, "x")); 365 assertEquals(-1, getTypedKeycodeGuess(null, "xyz")); 366 367 assertEquals(-1, getTypedKeycodeGuess("abc", "abc")); 368 assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("abc", "")); 369 370 assertEquals(KeyEvent.KEYCODE_H, getTypedKeycodeGuess("", "h")); 371 assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("h", "")); 372 assertEquals(KeyEvent.KEYCODE_E, getTypedKeycodeGuess("h", "he")); 373 assertEquals(KeyEvent.KEYCODE_L, getTypedKeycodeGuess("he", "hel")); 374 assertEquals(KeyEvent.KEYCODE_O, getTypedKeycodeGuess("hel", "helo")); 375 assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("helo", "hel")); 376 assertEquals(KeyEvent.KEYCODE_L, getTypedKeycodeGuess("hel", "hell")); 377 assertEquals(KeyEvent.KEYCODE_L, getTypedKeycodeGuess("hell", "helll")); 378 assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("helll", "hell")); 379 assertEquals(KeyEvent.KEYCODE_O, getTypedKeycodeGuess("hell", "hello")); 380 381 assertEquals(KeyEvent.KEYCODE_X, getTypedKeycodeGuess("xxx", "xxxx")); 382 assertEquals(KeyEvent.KEYCODE_X, getTypedKeycodeGuess("xxx", "xxxxx")); 383 assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("xxx", "xx")); 384 assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("xxx", "x")); 385 386 assertEquals(KeyEvent.KEYCODE_Y, getTypedKeycodeGuess("xxx", "xxxy")); 387 assertEquals(KeyEvent.KEYCODE_Y, getTypedKeycodeGuess("xxx", "xxxxy")); 388 assertEquals(-1, getTypedKeycodeGuess("xxx", "xy")); 389 assertEquals(-1, getTypedKeycodeGuess("xxx", "y")); 390 391 assertEquals(-1, getTypedKeycodeGuess("foo", "bar")); 392 assertEquals(-1, getTypedKeycodeGuess("foo", "bars")); 393 assertEquals(-1, getTypedKeycodeGuess("foo", "ba")); 394 395 // Some characters also require modifiers so we have to check the full event. 396 KeyEvent ev = ImeAdapter.getTypedKeyEventGuess(null, "!"); 397 assertEquals(KeyEvent.KEYCODE_1, ev.getKeyCode()); 398 assertTrue(ev.isShiftPressed()); 399 } 400 401 /* 402 @SmallTest 403 @Feature({"TextInput", "Main"}) 404 */ 405 @DisabledTest 406 public void testKeyCodesWhileComposingText() throws Throwable { 407 DOMUtils.focusNode(mContentViewCore, "textarea"); 408 assertWaitForKeyboardStatus(true); 409 410 // The calls below are a reflection of what the stock Google Keyboard (Android 4.4) sends 411 // when the noted key is touched on screen. Exercise care when altering to make sure 412 // that the test reflects reality. If this test breaks, it's possible that code has 413 // changed and different calls need to be made instead. 414 mConnection = (TestAdapterInputConnection) getAdapterInputConnection(); 415 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1); 416 417 // H 418 expectUpdateStateCall(mConnection); 419 setComposingText(mConnection, "h", 1); 420 assertEquals(KeyEvent.KEYCODE_H, mImeAdapter.mLastSyntheticKeyCode); 421 assertUpdateStateCall(mConnection, 1000); 422 assertEquals("h", mConnection.getTextBeforeCursor(9, 0)); 423 424 // O 425 expectUpdateStateCall(mConnection); 426 setComposingText(mConnection, "ho", 1); 427 assertEquals(KeyEvent.KEYCODE_O, mImeAdapter.mLastSyntheticKeyCode); 428 assertUpdateStateCall(mConnection, 1000); 429 assertEquals("ho", mConnection.getTextBeforeCursor(9, 0)); 430 431 // DEL 432 expectUpdateStateCall(mConnection); 433 setComposingText(mConnection, "h", 1); 434 assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode); 435 assertUpdateStateCall(mConnection, 1000); 436 setComposingRegion(mConnection, 0, 1); // DEL calls cancelComposition() then restarts 437 setComposingText(mConnection, "h", 1); 438 assertEquals("h", mConnection.getTextBeforeCursor(9, 0)); 439 440 // I 441 setComposingText(mConnection, "hi", 1); 442 assertEquals(KeyEvent.KEYCODE_I, mImeAdapter.mLastSyntheticKeyCode); 443 assertEquals("hi", mConnection.getTextBeforeCursor(9, 0)); 444 445 // SPACE 446 commitText(mConnection, "hi", 1); 447 assertEquals(-1, mImeAdapter.mLastSyntheticKeyCode); 448 commitText(mConnection, " ", 1); 449 assertEquals(KeyEvent.KEYCODE_SPACE, mImeAdapter.mLastSyntheticKeyCode); 450 assertEquals("hi ", mConnection.getTextBeforeCursor(9, 0)); 451 452 // DEL 453 deleteSurroundingText(mConnection, 1, 0); 454 assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode); 455 setComposingRegion(mConnection, 0, 2); 456 assertEquals("hi", mConnection.getTextBeforeCursor(9, 0)); 457 458 // DEL 459 setComposingText(mConnection, "h", 1); 460 assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode); 461 assertEquals("h", mConnection.getTextBeforeCursor(9, 0)); 462 463 // DEL 464 commitText(mConnection, "", 1); 465 assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode); 466 assertEquals("", mConnection.getTextBeforeCursor(9, 0)); 467 468 // DEL (on empty input) 469 deleteSurroundingText(mConnection, 1, 0); // DEL on empty still sends 1,0 470 assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode); 471 assertEquals("", mConnection.getTextBeforeCursor(9, 0)); 472 } 473 474 /* 475 @SmallTest 476 @Feature({"TextInput", "Main"}) 477 */ 478 @DisabledTest 479 public void testKeyCodesWhileSwipingText() throws Throwable { 480 DOMUtils.focusNode(mContentViewCore, "textarea"); 481 assertWaitForKeyboardStatus(true); 482 483 // The calls below are a reflection of what the stock Google Keyboard (Android 4.4) sends 484 // when the word is swiped on the soft keyboard. Exercise care when altering to make sure 485 // that the test reflects reality. If this test breaks, it's possible that code has 486 // changed and different calls need to be made instead. 487 mConnection = (TestAdapterInputConnection) getAdapterInputConnection(); 488 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1); 489 490 // "three" 491 expectUpdateStateCall(mConnection); 492 setComposingText(mConnection, "three", 1); 493 assertEquals(KeyEvent.KEYCODE_UNKNOWN, mImeAdapter.mLastSyntheticKeyCode); 494 assertUpdateStateCall(mConnection, 1000); 495 assertEquals("three", mConnection.getTextBeforeCursor(99, 0)); 496 497 // "word" 498 commitText(mConnection, "three", 1); 499 commitText(mConnection, " ", 1); 500 expectUpdateStateCall(mConnection); 501 setComposingText(mConnection, "word", 1); 502 assertEquals(KeyEvent.KEYCODE_UNKNOWN, mImeAdapter.mLastSyntheticKeyCode); 503 assertUpdateStateCall(mConnection, 1000); 504 assertEquals("three word", mConnection.getTextBeforeCursor(99, 0)); 505 506 // "test" 507 commitText(mConnection, "word", 1); 508 commitText(mConnection, " ", 1); 509 expectUpdateStateCall(mConnection); 510 setComposingText(mConnection, "test", 1); 511 assertEquals(KeyEvent.KEYCODE_UNKNOWN, mImeAdapter.mLastSyntheticKeyCode); 512 assertUpdateStateCall(mConnection, 1000); 513 assertEquals("three word test", mConnection.getTextBeforeCursor(99, 0)); 514 } 515 516 @SmallTest 517 @Feature({"TextInput", "Main"}) 518 public void testKeyCodesWhileTypingText() throws Throwable { 519 DOMUtils.focusNode(mContentViewCore, "textarea"); 520 assertWaitForKeyboardStatus(true); 521 522 // The calls below are a reflection of what the Hacker's Keyboard sends when the noted 523 // key is touched on screen. Exercise care when altering to make sure that the test 524 // reflects reality. 525 mConnection = (TestAdapterInputConnection) getAdapterInputConnection(); 526 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1); 527 528 // H 529 expectUpdateStateCall(mConnection); 530 commitText(mConnection, "h", 1); 531 assertEquals(KeyEvent.KEYCODE_H, mImeAdapter.mLastSyntheticKeyCode); 532 assertEquals("h", mConnection.getTextBeforeCursor(9, 0)); 533 assertUpdateStateCall(mConnection, 1000); 534 assertEquals("h", mConnection.getTextBeforeCursor(9, 0)); 535 536 // O 537 expectUpdateStateCall(mConnection); 538 commitText(mConnection, "o", 1); 539 assertEquals(KeyEvent.KEYCODE_O, mImeAdapter.mLastSyntheticKeyCode); 540 assertEquals("ho", mConnection.getTextBeforeCursor(9, 0)); 541 assertUpdateStateCall(mConnection, 1000); 542 assertEquals("ho", mConnection.getTextBeforeCursor(9, 0)); 543 544 // DEL 545 expectUpdateStateCall(mConnection); 546 deleteSurroundingText(mConnection, 1, 0); 547 assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode); 548 assertEquals("h", mConnection.getTextBeforeCursor(9, 0)); 549 assertUpdateStateCall(mConnection, 1000); 550 assertEquals("h", mConnection.getTextBeforeCursor(9, 0)); 551 552 // I 553 expectUpdateStateCall(mConnection); 554 commitText(mConnection, "i", 1); 555 assertEquals(KeyEvent.KEYCODE_I, mImeAdapter.mLastSyntheticKeyCode); 556 assertEquals("hi", mConnection.getTextBeforeCursor(9, 0)); 557 assertUpdateStateCall(mConnection, 1000); 558 assertEquals("hi", mConnection.getTextBeforeCursor(9, 0)); 559 560 // SPACE 561 expectUpdateStateCall(mConnection); 562 commitText(mConnection, " ", 1); 563 assertEquals(KeyEvent.KEYCODE_SPACE, mImeAdapter.mLastSyntheticKeyCode); 564 assertEquals("hi ", mConnection.getTextBeforeCursor(9, 0)); 565 assertUpdateStateCall(mConnection, 1000); 566 assertEquals("hi ", mConnection.getTextBeforeCursor(9, 0)); 567 568 // DEL 569 expectUpdateStateCall(mConnection); 570 deleteSurroundingText(mConnection, 1, 0); 571 assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode); 572 assertEquals("hi", mConnection.getTextBeforeCursor(9, 0)); 573 assertUpdateStateCall(mConnection, 1000); 574 assertEquals("hi", mConnection.getTextBeforeCursor(9, 0)); 575 576 // DEL 577 expectUpdateStateCall(mConnection); 578 deleteSurroundingText(mConnection, 1, 0); 579 assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode); 580 assertEquals("h", mConnection.getTextBeforeCursor(9, 0)); 581 assertUpdateStateCall(mConnection, 1000); 582 assertEquals("h", mConnection.getTextBeforeCursor(9, 0)); 583 584 // DEL 585 expectUpdateStateCall(mConnection); 586 deleteSurroundingText(mConnection, 1, 0); 587 assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode); 588 assertEquals("", mConnection.getTextBeforeCursor(9, 0)); 589 assertUpdateStateCall(mConnection, 1000); 590 assertEquals("", mConnection.getTextBeforeCursor(9, 0)); 591 592 // DEL (on empty input) 593 deleteSurroundingText(mConnection, 1, 0); // DEL on empty still sends 1,0 594 assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode); 595 assertEquals("", mConnection.getTextBeforeCursor(9, 0)); 596 } 597 598 @SmallTest 599 @Feature({"TextInput", "Main"}) 600 public void testSetComposingRegionOutOfBounds() throws Throwable { 601 DOMUtils.focusNode(mContentViewCore, "textarea"); 602 assertWaitForKeyboardStatus(true); 603 604 mConnection = (TestAdapterInputConnection) getAdapterInputConnection(); 605 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1); 606 setComposingText(mConnection, "hello", 1); 607 608 setComposingRegion(mConnection, 0, 0); 609 setComposingRegion(mConnection, 0, 9); 610 setComposingRegion(mConnection, 9, 0); 611 } 612 613 /* 614 @SmallTest 615 @Feature({"TextInput", "Main"}) 616 */ 617 @DisabledTest 618 public void testEnterKeyEventWhileComposingText() throws Throwable { 619 DOMUtils.focusNode(mContentViewCore, "input_radio"); 620 assertWaitForKeyboardStatus(false); 621 DOMUtils.focusNode(mContentViewCore, "textarea"); 622 assertWaitForKeyboardStatus(true); 623 624 mConnection = (TestAdapterInputConnection) getAdapterInputConnection(); 625 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1); 626 627 setComposingText(mConnection, "hello", 1); 628 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5); 629 630 getInstrumentation().runOnMainSync(new Runnable() { 631 @Override 632 public void run() { 633 mConnection.sendKeyEvent( 634 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); 635 mConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER)); 636 } 637 }); 638 639 // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed. 640 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 5, 5, -1, -1); 641 // The second new line is not a user visible/editable one, it is a side-effect of Blink 642 // using <br> internally. 643 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hello\n\n", 6, 6, -1, -1); 644 } 645 646 @SmallTest 647 @Feature({"TextInput", "Main"}) 648 public void testTransitionsWhileComposingText() throws Throwable { 649 DOMUtils.focusNode(mContentViewCore, "textarea"); 650 assertWaitForKeyboardStatus(true); 651 652 mConnection = (TestAdapterInputConnection) getAdapterInputConnection(); 653 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1); 654 655 // H 656 expectUpdateStateCall(mConnection); 657 setComposingText(mConnection, "h", 1); 658 assertEquals(KeyEvent.KEYCODE_H, mImeAdapter.mLastSyntheticKeyCode); 659 660 // Simulate switch of input fields. 661 finishComposingText(mConnection); 662 663 // H 664 expectUpdateStateCall(mConnection); 665 setComposingText(mConnection, "h", 1); 666 assertEquals(KeyEvent.KEYCODE_H, mImeAdapter.mLastSyntheticKeyCode); 667 } 668 669 @SmallTest 670 @Feature({"TextInput"}) 671 public void testPastePopupShowOnLongPress() throws Throwable { 672 commitText(mConnection, "hello", 1); 673 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1); 674 675 selectAll(mImeAdapter); 676 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 0, 5, -1, -1); 677 678 cut(mImeAdapter); 679 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1); 680 681 DOMUtils.longPressNode(this, mContentViewCore, "input_text"); 682 final PastePopupMenu pastePopup = mContentViewCore.getPastePopupForTest(); 683 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 684 @Override 685 public boolean isSatisfied() { 686 return pastePopup.isShowing(); 687 } 688 })); 689 } 690 691 @SmallTest 692 @Feature({"TextInput"}) 693 public void testTextHandlesPreservedWithDpadNavigation() throws Throwable { 694 DOMUtils.longPressNode(this, mContentViewCore, "plain_text"); 695 assertWaitForSelectActionBarStatus(true); 696 assertTrue(mContentViewCore.hasSelection()); 697 698 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 699 @Override 700 public void run() { 701 final KeyEvent downKeyEvent = new KeyEvent( 702 KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN); 703 mImeAdapter.dispatchKeyEvent(downKeyEvent); 704 } 705 }); 706 707 assertWaitForSelectActionBarStatus(true); 708 assertTrue(mContentViewCore.hasSelection()); 709 } 710 711 private void performGo(final AdapterInputConnection inputConnection, 712 TestCallbackHelperContainer testCallbackHelperContainer) throws Throwable { 713 handleBlockingCallbackAction( 714 testCallbackHelperContainer.getOnPageFinishedHelper(), 715 new Runnable() { 716 @Override 717 public void run() { 718 inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO); 719 } 720 }); 721 } 722 723 private void assertWaitForKeyboardStatus(final boolean show) throws InterruptedException { 724 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 725 @Override 726 public boolean isSatisfied() { 727 return show == getImeAdapter().mIsShowWithoutHideOutstanding && 728 (!show || getAdapterInputConnection() != null); 729 } 730 })); 731 } 732 733 private void assertWaitForSelectActionBarStatus( 734 final boolean show) throws InterruptedException { 735 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 736 @Override 737 public boolean isSatisfied() { 738 return show == mContentViewCore.isSelectActionBarShowing(); 739 } 740 })); 741 } 742 743 private void waitAndVerifyEditableCallback(final ArrayList<TestImeState> states, 744 final int index, String text, int selectionStart, int selectionEnd, 745 int compositionStart, int compositionEnd) throws InterruptedException { 746 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 747 @Override 748 public boolean isSatisfied() { 749 return states.size() > index; 750 } 751 })); 752 states.get(index).assertEqualState( 753 text, selectionStart, selectionEnd, compositionStart, compositionEnd); 754 } 755 756 private void expectUpdateStateCall(final TestAdapterInputConnection connection) { 757 connection.mImeUpdateQueue.clear(); 758 } 759 760 private void assertUpdateStateCall(final TestAdapterInputConnection connection, int maxms) 761 throws Exception { 762 while (connection.mImeUpdateQueue.size() == 0 && maxms > 0) { 763 try { 764 Thread.sleep(50); 765 } catch (Exception e) { 766 // Not really a problem since we're just going to sleep again. 767 } 768 maxms -= 50; 769 } 770 assertTrue(connection.mImeUpdateQueue.size() > 0); 771 } 772 773 private void assertClipboardContents(final Activity activity, final String expectedContents) 774 throws InterruptedException { 775 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 776 @Override 777 public boolean isSatisfied() { 778 return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() { 779 @Override 780 public Boolean call() throws Exception { 781 ClipboardManager clipboardManager = 782 (ClipboardManager) activity.getSystemService( 783 Context.CLIPBOARD_SERVICE); 784 ClipData clip = clipboardManager.getPrimaryClip(); 785 return clip != null && clip.getItemCount() == 1 786 && TextUtils.equals(clip.getItemAt(0).getText(), expectedContents); 787 } 788 }); 789 } 790 })); 791 } 792 793 private ImeAdapter getImeAdapter() { 794 return mContentViewCore.getImeAdapterForTest(); 795 } 796 797 private AdapterInputConnection getAdapterInputConnection() { 798 return mContentViewCore.getInputConnectionForTest(); 799 } 800 801 private void copy(final ImeAdapter adapter) { 802 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 803 @Override 804 public void run() { 805 adapter.copy(); 806 } 807 }); 808 } 809 810 private void cut(final ImeAdapter adapter) { 811 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 812 @Override 813 public void run() { 814 adapter.cut(); 815 } 816 }); 817 } 818 819 private void paste(final ImeAdapter adapter) { 820 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 821 @Override 822 public void run() { 823 adapter.paste(); 824 } 825 }); 826 } 827 828 private void selectAll(final ImeAdapter adapter) { 829 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 830 @Override 831 public void run() { 832 adapter.selectAll(); 833 } 834 }); 835 } 836 837 private void unselect(final ImeAdapter adapter) { 838 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 839 @Override 840 public void run() { 841 adapter.unselect(); 842 } 843 }); 844 } 845 846 private void commitText(final AdapterInputConnection connection, final CharSequence text, 847 final int newCursorPosition) { 848 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 849 @Override 850 public void run() { 851 connection.commitText(text, newCursorPosition); 852 } 853 }); 854 } 855 856 private void setSelection(final AdapterInputConnection connection, final int start, 857 final int end) { 858 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 859 @Override 860 public void run() { 861 connection.setSelection(start, end); 862 } 863 }); 864 } 865 866 private void setComposingRegion(final AdapterInputConnection connection, final int start, 867 final int end) { 868 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 869 @Override 870 public void run() { 871 connection.setComposingRegion(start, end); 872 } 873 }); 874 } 875 876 private void setComposingText(final AdapterInputConnection connection, final CharSequence text, 877 final int newCursorPosition) { 878 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 879 @Override 880 public void run() { 881 connection.setComposingText(text, newCursorPosition); 882 } 883 }); 884 } 885 886 private void finishComposingText(final AdapterInputConnection connection) { 887 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 888 @Override 889 public void run() { 890 connection.finishComposingText(); 891 } 892 }); 893 } 894 895 private void deleteSurroundingText(final AdapterInputConnection connection, final int before, 896 final int after) { 897 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 898 @Override 899 public void run() { 900 connection.deleteSurroundingText(before, after); 901 } 902 }); 903 } 904 905 private static class TestAdapterInputConnectionFactory extends 906 ImeAdapter.AdapterInputConnectionFactory { 907 @Override 908 public AdapterInputConnection get(View view, ImeAdapter imeAdapter, 909 Editable editable, EditorInfo outAttrs) { 910 return new TestAdapterInputConnection(view, imeAdapter, editable, outAttrs); 911 } 912 } 913 914 private static class TestAdapterInputConnection extends AdapterInputConnection { 915 private final ArrayList<TestImeState> mImeUpdateQueue = new ArrayList<TestImeState>(); 916 917 public TestAdapterInputConnection(View view, ImeAdapter imeAdapter, 918 Editable editable, EditorInfo outAttrs) { 919 super(view, imeAdapter, editable, outAttrs); 920 } 921 922 @Override 923 public void updateState(String text, int selectionStart, int selectionEnd, 924 int compositionStart, int compositionEnd, boolean requiredAck) { 925 mImeUpdateQueue.add(new TestImeState(text, selectionStart, selectionEnd, 926 compositionStart, compositionEnd)); 927 super.updateState(text, selectionStart, selectionEnd, compositionStart, 928 compositionEnd, requiredAck); 929 } 930 } 931 932 private static class TestImeState { 933 private final String mText; 934 private final int mSelectionStart; 935 private final int mSelectionEnd; 936 private final int mCompositionStart; 937 private final int mCompositionEnd; 938 939 public TestImeState(String text, int selectionStart, int selectionEnd, 940 int compositionStart, int compositionEnd) { 941 mText = text; 942 mSelectionStart = selectionStart; 943 mSelectionEnd = selectionEnd; 944 mCompositionStart = compositionStart; 945 mCompositionEnd = compositionEnd; 946 } 947 948 public void assertEqualState(String text, int selectionStart, int selectionEnd, 949 int compositionStart, int compositionEnd) { 950 assertEquals("Text did not match", text, mText); 951 assertEquals("Selection start did not match", selectionStart, mSelectionStart); 952 assertEquals("Selection end did not match", selectionEnd, mSelectionEnd); 953 assertEquals("Composition start did not match", compositionStart, mCompositionStart); 954 assertEquals("Composition end did not match", compositionEnd, mCompositionEnd); 955 } 956 } 957} 958