ImeTest.java revision 7dbb3d5cf0c15f500944d211057644d6a2f37371
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 assertWaitForPageScaleFactor(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 290 291 private void assertWaitForPageScaleFactor(final float scale) throws InterruptedException { 292 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 293 @Override 294 public boolean isSatisfied() { 295 return getContentViewCore().getScale() == scale; 296 } 297 })); 298 } 299 300 private void assertWaitForKeyboardStatus(final boolean show) throws InterruptedException { 301 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 302 @Override 303 public boolean isSatisfied() { 304 return show == getImeAdapter().mIsShowWithoutHideOutstanding && 305 (!show || getAdapterInputConnection() != null); 306 } 307 })); 308 } 309 310 private void waitAndVerifyEditableCallback(final ArrayList<TestImeState> states, 311 final int index, String text, int selectionStart, int selectionEnd, 312 int compositionStart, int compositionEnd) throws InterruptedException { 313 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 314 @Override 315 public boolean isSatisfied() { 316 return states.size() > index; 317 } 318 })); 319 states.get(index).assertEqualState( 320 text, selectionStart, selectionEnd, compositionStart, compositionEnd); 321 } 322 323 private void assertClipboardContents(final Activity activity, final String expectedContents) 324 throws InterruptedException { 325 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 326 @Override 327 public boolean isSatisfied() { 328 return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() { 329 @Override 330 public Boolean call() throws Exception { 331 ClipboardManager clipboardManager = 332 (ClipboardManager) activity.getSystemService( 333 Context.CLIPBOARD_SERVICE); 334 ClipData clip = clipboardManager.getPrimaryClip(); 335 return clip != null && clip.getItemCount() == 1 336 && TextUtils.equals(clip.getItemAt(0).getText(), expectedContents); 337 } 338 }); 339 } 340 })); 341 } 342 343 private void assertWaitForSetIgnoreUpdates(final boolean ignore, 344 final TestAdapterInputConnection connection) throws Throwable { 345 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 346 @Override 347 public boolean isSatisfied() { 348 return ignore == connection.isIgnoringTextInputStateUpdates(); 349 } 350 })); 351 } 352 353 private ImeAdapter getImeAdapter() { 354 return getContentViewCore().getImeAdapterForTest(); 355 } 356 357 private AdapterInputConnection getAdapterInputConnection() { 358 return getContentViewCore().getInputConnectionForTest(); 359 } 360 361 private static class TestAdapterInputConnectionFactory extends 362 ImeAdapter.AdapterInputConnectionFactory { 363 @Override 364 public AdapterInputConnection get(View view, ImeAdapter imeAdapter, 365 EditorInfo outAttrs) { 366 return new TestAdapterInputConnection(view, imeAdapter, outAttrs); 367 } 368 } 369 370 private static class TestAdapterInputConnection extends AdapterInputConnection { 371 private int mUpdateSelectionCounter = 0; 372 private ArrayList<TestImeState> mImeUpdateQueue = new ArrayList<ImeTest.TestImeState>(); 373 374 public TestAdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) { 375 super(view, imeAdapter, outAttrs); 376 } 377 378 @Override 379 public void setEditableText(String text, int selectionStart, int selectionEnd, 380 int compositionStart, int compositionEnd) { 381 mImeUpdateQueue.add(new TestImeState(text, selectionStart, selectionEnd, 382 compositionStart, compositionEnd)); 383 super.setEditableText( 384 text, selectionStart, selectionEnd, compositionStart, compositionEnd); 385 } 386 387 @Override 388 protected void updateSelection( 389 int selectionStart, int selectionEnd, 390 int compositionStart, int compositionEnd) { 391 mUpdateSelectionCounter++; 392 } 393 } 394 395 private static class TestImeState { 396 private final String mText; 397 private final int mSelectionStart; 398 private final int mSelectionEnd; 399 private final int mCompositionStart; 400 private final int mCompositionEnd; 401 402 public TestImeState(String text, int selectionStart, int selectionEnd, 403 int compositionStart, int compositionEnd) { 404 mText = text; 405 mSelectionStart = selectionStart; 406 mSelectionEnd = selectionEnd; 407 mCompositionStart = compositionStart; 408 mCompositionEnd = compositionEnd; 409 } 410 411 public void assertEqualState(String text, int selectionStart, int selectionEnd, 412 int compositionStart, int compositionEnd) { 413 assertEquals("Text did not match", text, mText); 414 assertEquals("Selection start did not match", selectionStart, mSelectionStart); 415 assertEquals("Selection end did not match", selectionEnd, mSelectionEnd); 416 assertEquals("Composition start did not match", compositionStart, mCompositionStart); 417 assertEquals("Composition end did not match", compositionEnd, mCompositionEnd); 418 } 419 } 420} 421