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