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