ImeTest.java revision a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7
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 mConnection.setComposingText("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 mConnection.setComposingText("h", 1); 99 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "h", 1, 1, 0, 1); 100 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter()); 101 102 mConnection.setComposingText("he", 1); 103 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "he", 2, 2, 0, 2); 104 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter()); 105 106 mConnection.setComposingText("hel", 1); 107 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hel", 3, 3, 0, 3); 108 assertEquals(1, mInputMethodManagerWrapper.getShowSoftInputCounter()); 109 110 mConnection.commitText("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 mConnection.commitText("hello", 1); 119 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1); 120 121 mConnection.setSelection(2, 5); 122 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 2, 5, -1, -1); 123 124 mImeAdapter.copy(); 125 assertClipboardContents(getActivity(), "llo"); 126 } 127 128 @SmallTest 129 @Feature({"TextInput"}) 130 public void testEnterTextAndRefocus() throws Exception { 131 mConnection.commitText("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 mConnection.commitText("snarful", 1); 147 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "snarful", 7, 7, -1, -1); 148 149 mConnection.setSelection(1, 5); 150 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "snarful", 1, 5, -1, -1); 151 152 mImeAdapter.cut(); 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 mImeAdapter.paste(); 172 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "blarg", 5, 5, -1, -1); 173 174 mConnection.setSelection(3, 5); 175 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "blarg", 3, 5, -1, -1); 176 177 mImeAdapter.paste(); 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 mImeAdapter.paste(); 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 mConnection.commitText("hello", 1); 191 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1); 192 193 mImeAdapter.selectAll(); 194 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 0, 5, -1, -1); 195 196 mImeAdapter.unselect(); 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 mConnection.commitText("hllo", 1); 233 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hllo", 4, 4, -1, -1); 234 235 mConnection.commitText(" ", 1); 236 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hllo ", 5, 5, -1, -1); 237 238 mConnection.setSelection(1, 1); 239 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hllo ", 1, 1, -1, -1); 240 241 mConnection.setComposingRegion(0, 4); 242 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hllo ", 1, 1, 0, 4); 243 244 mConnection.finishComposingText(); 245 waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 5, "hllo ", 1, 1, -1, -1); 246 247 mConnection.commitText("\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 mConnection.setComposingText("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 static class TestAdapterInputConnectionFactory extends 357 ImeAdapter.AdapterInputConnectionFactory { 358 @Override 359 public AdapterInputConnection get(View view, ImeAdapter imeAdapter, 360 EditorInfo outAttrs) { 361 return new TestAdapterInputConnection(view, imeAdapter, outAttrs); 362 } 363 } 364 365 private static class TestAdapterInputConnection extends AdapterInputConnection { 366 private final ArrayList<TestImeState> mImeUpdateQueue = new ArrayList<TestImeState>(); 367 368 public TestAdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) { 369 super(view, imeAdapter, outAttrs); 370 } 371 372 @Override 373 public void updateState(String text, int selectionStart, int selectionEnd, 374 int compositionStart, int compositionEnd, boolean requiredAck) { 375 mImeUpdateQueue.add(new TestImeState(text, selectionStart, selectionEnd, 376 compositionStart, compositionEnd)); 377 super.updateState(text, selectionStart, selectionEnd, compositionStart, 378 compositionEnd, requiredAck); 379 } 380 } 381 382 private static class TestImeState { 383 private final String mText; 384 private final int mSelectionStart; 385 private final int mSelectionEnd; 386 private final int mCompositionStart; 387 private final int mCompositionEnd; 388 389 public TestImeState(String text, int selectionStart, int selectionEnd, 390 int compositionStart, int compositionEnd) { 391 mText = text; 392 mSelectionStart = selectionStart; 393 mSelectionEnd = selectionEnd; 394 mCompositionStart = compositionStart; 395 mCompositionEnd = compositionEnd; 396 } 397 398 public void assertEqualState(String text, int selectionStart, int selectionEnd, 399 int compositionStart, int compositionEnd) { 400 assertEquals("Text did not match", text, mText); 401 assertEquals("Selection start did not match", selectionStart, mSelectionStart); 402 assertEquals("Selection end did not match", selectionEnd, mSelectionEnd); 403 assertEquals("Composition start did not match", compositionStart, mCompositionStart); 404 assertEquals("Composition end did not match", compositionEnd, mCompositionEnd); 405 } 406 } 407} 408