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