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