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