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