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