Index: content/public/android/javatests/src/org/chromium/content/browser/input/ImeActivityTestRule.java |
diff --git a/content/public/android/javatests/src/org/chromium/content/browser/input/ImeActivityTestRule.java b/content/public/android/javatests/src/org/chromium/content/browser/input/ImeActivityTestRule.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..8ec3f26ae7f5d27085c2e60c2afce103d4181d34 |
--- /dev/null |
+++ b/content/public/android/javatests/src/org/chromium/content/browser/input/ImeActivityTestRule.java |
@@ -0,0 +1,616 @@ |
+// Copyright 2017 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.content.browser.input; |
+ |
+import android.annotation.TargetApi; |
+import android.app.Activity; |
+import android.content.ClipData; |
+import android.content.ClipboardManager; |
+import android.content.Context; |
+import android.os.Handler; |
+import android.text.TextUtils; |
+import android.util.Pair; |
+import android.view.KeyEvent; |
+import android.view.View; |
+import android.view.inputmethod.EditorInfo; |
+import android.view.inputmethod.InputConnection; |
+ |
+import org.junit.Assert; |
+ |
+import org.chromium.base.ThreadUtils; |
+import org.chromium.content.browser.ContentViewCore; |
+import org.chromium.content.browser.SelectionPopupController; |
+import org.chromium.content.browser.test.util.Criteria; |
+import org.chromium.content.browser.test.util.CriteriaHelper; |
+import org.chromium.content.browser.test.util.DOMUtils; |
+import org.chromium.content.browser.test.util.JavaScriptUtils; |
+import org.chromium.content.browser.test.util.TestCallbackHelperContainer; |
+import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper; |
+import org.chromium.content_public.browser.WebContents; |
+import org.chromium.content_shell_apk.ContentShellActivityTestRule; |
+import org.chromium.ui.base.ime.TextInputType; |
+ |
+import java.util.ArrayList; |
+import java.util.Arrays; |
+import java.util.List; |
+import java.util.concurrent.Callable; |
+import java.util.concurrent.ExecutionException; |
+import java.util.concurrent.TimeoutException; |
+ |
+/** |
+ * Integration tests for text input for Android L (or above) features. |
+ */ |
+class ImeActivityTestRule extends ContentShellActivityTestRule { |
+ private ChromiumBaseInputConnection mConnection; |
+ private TestInputConnectionFactory mConnectionFactory; |
+ private ImeAdapter mImeAdapter; |
+ |
+ static final String INPUT_FORM_HTML = "content/test/data/android/input/input_forms.html"; |
+ |
+ private ContentViewCore mContentViewCore; |
+ private SelectionPopupController mSelectionPopupController; |
+ private TestCallbackHelperContainer mCallbackContainer; |
+ private TestInputMethodManagerWrapper mInputMethodManagerWrapper; |
+ |
+ public void setUp() throws Exception { |
+ launchContentShellWithUrlSync(INPUT_FORM_HTML); |
+ mContentViewCore = getContentViewCore(); |
+ mSelectionPopupController = mContentViewCore.getSelectionPopupControllerForTesting(); |
+ mInputMethodManagerWrapper = new TestInputMethodManagerWrapper(mContentViewCore) { |
+ private boolean mExpectsSelectionOutsideComposition; |
+ |
+ @Override |
+ public void expectsSelectionOutsideComposition() { |
+ mExpectsSelectionOutsideComposition = true; |
+ } |
+ |
+ @Override |
+ public void onUpdateSelection( |
+ Range oldSel, Range oldComp, Range newSel, Range newComp) { |
+ // We expect that selection will be outside composition in some cases. Keyboard |
+ // app will not finish composition in this case. |
+ if (mExpectsSelectionOutsideComposition) { |
+ mExpectsSelectionOutsideComposition = false; |
+ return; |
+ } |
+ if (oldComp == null || oldComp.start() == oldComp.end() |
+ || newComp.start() == newComp.end()) { |
+ return; |
+ } |
+ // This emulates keyboard app's behavior that finishes composition when |
+ // selection is outside composition. |
+ if (!newSel.intersects(newComp)) { |
+ try { |
+ finishComposingText(); |
+ } catch (Exception e) { |
+ e.printStackTrace(); |
+ Assert.fail(); |
+ } |
+ } |
+ } |
+ }; |
+ getImeAdapter().setInputMethodManagerWrapperForTest(mInputMethodManagerWrapper); |
+ Assert.assertEquals(0, mInputMethodManagerWrapper.getShowSoftInputCounter()); |
+ mConnectionFactory = |
+ new TestInputConnectionFactory(getImeAdapter().getInputConnectionFactoryForTest()); |
+ getImeAdapter().setInputConnectionFactory(mConnectionFactory); |
+ |
+ mCallbackContainer = new TestCallbackHelperContainer(mContentViewCore); |
+ DOMUtils.waitForNonZeroNodeBounds(getWebContents(), "input_text"); |
+ boolean result = DOMUtils.clickNode(mContentViewCore, "input_text"); |
+ |
+ Assert.assertEquals("Failed to dispatch touch event.", true, result); |
+ assertWaitForKeyboardStatus(true); |
+ |
+ mConnection = getInputConnection(); |
+ mImeAdapter = getImeAdapter(); |
+ |
+ waitForKeyboardStates(1, 0, 1, new Integer[] {TextInputType.TEXT}); |
+ Assert.assertEquals(0, mConnectionFactory.getOutAttrs().initialSelStart); |
+ Assert.assertEquals(0, mConnectionFactory.getOutAttrs().initialSelEnd); |
+ |
+ waitForEventLogs("selectionchange"); |
+ clearEventLogs(); |
+ |
+ waitAndVerifyUpdateSelection(0, 0, 0, -1, -1); |
+ resetAllStates(); |
+ } |
+ |
+ SelectionPopupController getSelectionPopupController() { |
+ return mSelectionPopupController; |
+ } |
+ |
+ TestCallbackHelperContainer getTestCallBackHelperContainer() { |
+ return mCallbackContainer; |
+ } |
+ |
+ ChromiumBaseInputConnection getConnection() { |
+ return mConnection; |
+ } |
+ |
+ TestInputMethodManagerWrapper getInputMethodManagerWrapper() { |
+ return mInputMethodManagerWrapper; |
+ } |
+ |
+ TestInputConnectionFactory getConnectionFactory() { |
+ return mConnectionFactory; |
+ } |
+ |
+ void fullyLoadUrl(final String url) throws Throwable { |
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
+ @Override |
+ public void run() { |
+ getActivity().getActiveShell().loadUrl(url); |
+ } |
+ }); |
+ waitForActiveShellToBeDoneLoading(); |
+ } |
+ |
+ void clearEventLogs() throws Exception { |
+ final String code = "clearEventLogs()"; |
+ JavaScriptUtils.executeJavaScriptAndWaitForResult( |
+ getContentViewCore().getWebContents(), code); |
+ } |
+ |
+ void waitForEventLogs(String expectedLogs) throws Exception { |
+ final String code = "getEventLogs()"; |
+ final String sanitizedExpectedLogs = "\"" + expectedLogs + "\""; |
+ Assert.assertEquals(sanitizedExpectedLogs, |
+ JavaScriptUtils.executeJavaScriptAndWaitForResult( |
+ getContentViewCore().getWebContents(), code)); |
+ } |
+ |
+ void assertTextsAroundCursor(CharSequence before, CharSequence selected, CharSequence after) |
+ throws Exception { |
+ Assert.assertEquals(before, getTextBeforeCursor(100, 0)); |
+ Assert.assertEquals(selected, getSelectedText(0)); |
+ Assert.assertEquals(after, getTextAfterCursor(100, 0)); |
+ } |
+ |
+ void waitForKeyboardStates(int show, int hide, int restart, Integer[] history) { |
+ final String expected = stringifyKeyboardStates(show, hide, restart, history); |
+ CriteriaHelper.pollUiThread(Criteria.equals(expected, new Callable<String>() { |
+ @Override |
+ public String call() { |
+ return getKeyboardStates(); |
+ } |
+ })); |
+ } |
+ |
+ void resetAllStates() { |
+ mInputMethodManagerWrapper.reset(); |
+ mConnectionFactory.clearTextInputTypeHistory(); |
+ } |
+ |
+ String getKeyboardStates() { |
+ int showCount = mInputMethodManagerWrapper.getShowSoftInputCounter(); |
+ int hideCount = mInputMethodManagerWrapper.getHideSoftInputCounter(); |
+ int restartCount = mInputMethodManagerWrapper.getRestartInputCounter(); |
+ Integer[] history = mConnectionFactory.getTextInputTypeHistory(); |
+ return stringifyKeyboardStates(showCount, hideCount, restartCount, history); |
+ } |
+ |
+ String stringifyKeyboardStates(int show, int hide, int restart, Integer[] history) { |
+ return "show count: " + show + ", hide count: " + hide + ", restart count: " + restart |
+ + ", input type history: " + Arrays.deepToString(history); |
+ } |
+ |
+ void performGo(TestCallbackHelperContainer testCallbackHelperContainer) throws Throwable { |
+ final InputConnection inputConnection = mConnection; |
+ final Callable<Void> callable = new Callable<Void>() { |
+ @Override |
+ public Void call() throws Exception { |
+ inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO); |
+ return null; |
+ } |
+ }; |
+ |
+ handleBlockingCallbackAction( |
+ testCallbackHelperContainer.getOnPageFinishedHelper(), new Runnable() { |
+ @Override |
+ public void run() { |
+ try { |
+ runBlockingOnImeThread(callable); |
+ } catch (Exception e) { |
+ e.printStackTrace(); |
+ Assert.fail(); |
+ } |
+ } |
+ }); |
+ } |
+ |
+ void assertWaitForKeyboardStatus(final boolean show) { |
+ CriteriaHelper.pollUiThread(new Criteria() { |
+ @Override |
+ public boolean isSatisfied() { |
+ // We do not check the other way around: in some cases we need to keep |
+ // input connection even when the last known status is 'hidden'. |
+ if (show && getInputConnection() == null) { |
+ updateFailureReason("input connection should not be null."); |
+ return false; |
+ } |
+ updateFailureReason("expected show: " + show); |
+ return show == mInputMethodManagerWrapper.isShowWithoutHideOutstanding(); |
+ } |
+ }); |
+ } |
+ |
+ void assertWaitForSelectActionBarStatus(final boolean show) { |
+ CriteriaHelper.pollUiThread(Criteria.equals(show, new Callable<Boolean>() { |
+ @Override |
+ public Boolean call() { |
+ return mContentViewCore.isSelectActionBarShowing(); |
+ } |
+ })); |
+ } |
+ |
+ void waitAndVerifyUpdateSelection(final int index, final int selectionStart, |
+ final int selectionEnd, final int compositionStart, final int compositionEnd) { |
+ final List<Pair<Range, Range>> states = mInputMethodManagerWrapper.getUpdateSelectionList(); |
+ CriteriaHelper.pollUiThread(new Criteria() { |
+ @Override |
+ public boolean isSatisfied() { |
+ return states.size() > index; |
+ } |
+ }); |
+ Pair<Range, Range> selection = states.get(index); |
+ Assert.assertEquals(selectionStart, selection.first.start()); |
+ Assert.assertEquals(selectionEnd, selection.first.end()); |
+ Assert.assertEquals(compositionStart, selection.second.start()); |
+ Assert.assertEquals(compositionEnd, selection.second.end()); |
+ } |
+ |
+ void resetUpdateSelectionList() { |
+ mInputMethodManagerWrapper.getUpdateSelectionList().clear(); |
+ } |
+ |
+ void assertClipboardContents(final Activity activity, final String expectedContents) { |
+ CriteriaHelper.pollUiThread(new Criteria() { |
+ @Override |
+ public boolean isSatisfied() { |
+ ClipboardManager clipboardManager = |
+ (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); |
+ ClipData clip = clipboardManager.getPrimaryClip(); |
+ return clip != null && clip.getItemCount() == 1 |
+ && TextUtils.equals(clip.getItemAt(0).getText(), expectedContents); |
+ } |
+ }); |
+ } |
+ |
+ ImeAdapter getImeAdapter() { |
+ return mContentViewCore.getImeAdapterForTest(); |
+ } |
+ |
+ ChromiumBaseInputConnection getInputConnection() { |
+ try { |
+ return ThreadUtils.runOnUiThreadBlocking(new Callable<ChromiumBaseInputConnection>() { |
+ @Override |
+ public ChromiumBaseInputConnection call() { |
+ return mContentViewCore.getImeAdapterForTest().getInputConnectionForTest(); |
+ } |
+ }); |
+ } catch (ExecutionException e) { |
+ e.printStackTrace(); |
+ Assert.fail(); |
+ return null; |
+ } |
+ } |
+ |
+ void restartInput() { |
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
+ @Override |
+ public void run() { |
+ mImeAdapter.restartInput(); |
+ } |
+ }); |
+ } |
+ |
+ // After calling this method, we should call assertClipboardContents() to wait for the clipboard |
+ // to get updated. See cubug.com/621046 |
+ void copy() { |
+ final WebContents webContents = getWebContents(); |
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
+ @Override |
+ public void run() { |
+ webContents.copy(); |
+ } |
+ }); |
+ } |
+ |
+ void cut() { |
+ final WebContents webContents = getWebContents(); |
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
+ @Override |
+ public void run() { |
+ webContents.cut(); |
+ } |
+ }); |
+ } |
+ |
+ void setClip(final CharSequence text) { |
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
+ @Override |
+ public void run() { |
+ final ClipboardManager clipboardManager = |
+ (ClipboardManager) getActivity().getSystemService( |
+ Context.CLIPBOARD_SERVICE); |
+ clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text)); |
+ } |
+ }); |
+ } |
+ |
+ void paste() { |
+ final WebContents webContents = getWebContents(); |
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
+ @Override |
+ public void run() { |
+ webContents.paste(); |
+ } |
+ }); |
+ } |
+ |
+ void selectAll() { |
+ final WebContents webContents = getWebContents(); |
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
+ @Override |
+ public void run() { |
+ webContents.selectAll(); |
+ } |
+ }); |
+ } |
+ |
+ void collapseSelection() { |
+ final WebContents webContents = getWebContents(); |
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
+ @Override |
+ public void run() { |
+ webContents.collapseSelection(); |
+ } |
+ }); |
+ } |
+ |
+ /** |
+ * Run the {@Callable} on IME thread (or UI thread if not applicable). |
+ * @param c The callable |
+ * @return The result from running the callable. |
+ */ |
+ <T> T runBlockingOnImeThread(Callable<T> c) throws Exception { |
+ return ImeTestUtils.runBlockingOnHandler(mConnectionFactory.getHandler(), c); |
+ } |
+ |
+ boolean beginBatchEdit() throws Exception { |
+ final ChromiumBaseInputConnection connection = mConnection; |
+ return runBlockingOnImeThread(new Callable<Boolean>() { |
+ @Override |
+ public Boolean call() { |
+ return connection.beginBatchEdit(); |
+ } |
+ }); |
+ } |
+ |
+ boolean endBatchEdit() throws Exception { |
+ final ChromiumBaseInputConnection connection = mConnection; |
+ return runBlockingOnImeThread(new Callable<Boolean>() { |
+ @Override |
+ public Boolean call() { |
+ return connection.endBatchEdit(); |
+ } |
+ }); |
+ } |
+ |
+ boolean commitText(final CharSequence text, final int newCursorPosition) throws Exception { |
+ final ChromiumBaseInputConnection connection = mConnection; |
+ return runBlockingOnImeThread(new Callable<Boolean>() { |
+ @Override |
+ public Boolean call() { |
+ return connection.commitText(text, newCursorPosition); |
+ } |
+ }); |
+ } |
+ |
+ boolean setSelection(final int start, final int end) throws Exception { |
+ final ChromiumBaseInputConnection connection = mConnection; |
+ return runBlockingOnImeThread(new Callable<Boolean>() { |
+ @Override |
+ public Boolean call() { |
+ return connection.setSelection(start, end); |
+ } |
+ }); |
+ } |
+ |
+ boolean setComposingRegion(final int start, final int end) throws Exception { |
+ final ChromiumBaseInputConnection connection = mConnection; |
+ return runBlockingOnImeThread(new Callable<Boolean>() { |
+ @Override |
+ public Boolean call() { |
+ return connection.setComposingRegion(start, end); |
+ } |
+ }); |
+ } |
+ |
+ protected boolean setComposingText(final CharSequence text, final int newCursorPosition) |
+ throws Exception { |
+ final ChromiumBaseInputConnection connection = mConnection; |
+ return runBlockingOnImeThread(new Callable<Boolean>() { |
+ @Override |
+ public Boolean call() { |
+ return connection.setComposingText(text, newCursorPosition); |
+ } |
+ }); |
+ } |
+ |
+ boolean finishComposingText() throws Exception { |
+ final ChromiumBaseInputConnection connection = mConnection; |
+ return runBlockingOnImeThread(new Callable<Boolean>() { |
+ @Override |
+ public Boolean call() { |
+ return connection.finishComposingText(); |
+ } |
+ }); |
+ } |
+ |
+ boolean deleteSurroundingText(final int before, final int after) throws Exception { |
+ final ChromiumBaseInputConnection connection = mConnection; |
+ return runBlockingOnImeThread(new Callable<Boolean>() { |
+ @Override |
+ public Boolean call() { |
+ return connection.deleteSurroundingText(before, after); |
+ } |
+ }); |
+ } |
+ |
+ // Note that deleteSurroundingTextInCodePoints() was introduced in Android N (Api level 24), but |
+ // the Android repository used in Chrome is behind that (level 23). So this function can't be |
+ // called by keyboard apps currently. |
+ @TargetApi(24) |
+ boolean deleteSurroundingTextInCodePoints(final int before, final int after) throws Exception { |
+ final ThreadedInputConnection connection = (ThreadedInputConnection) mConnection; |
+ return runBlockingOnImeThread(new Callable<Boolean>() { |
+ @Override |
+ public Boolean call() { |
+ return connection.deleteSurroundingTextInCodePoints(before, after); |
+ } |
+ }); |
+ } |
+ |
+ CharSequence getTextBeforeCursor(final int length, final int flags) throws Exception { |
+ final ChromiumBaseInputConnection connection = mConnection; |
+ return runBlockingOnImeThread(new Callable<CharSequence>() { |
+ @Override |
+ public CharSequence call() { |
+ return connection.getTextBeforeCursor(length, flags); |
+ } |
+ }); |
+ } |
+ |
+ CharSequence getSelectedText(final int flags) throws Exception { |
+ final ChromiumBaseInputConnection connection = mConnection; |
+ return runBlockingOnImeThread(new Callable<CharSequence>() { |
+ @Override |
+ public CharSequence call() { |
+ return connection.getSelectedText(flags); |
+ } |
+ }); |
+ } |
+ |
+ CharSequence getTextAfterCursor(final int length, final int flags) throws Exception { |
+ final ChromiumBaseInputConnection connection = mConnection; |
+ return runBlockingOnImeThread(new Callable<CharSequence>() { |
+ @Override |
+ public CharSequence call() { |
+ return connection.getTextAfterCursor(length, flags); |
+ } |
+ }); |
+ } |
+ |
+ int getCursorCapsMode(final int reqModes) throws Throwable { |
+ final ChromiumBaseInputConnection connection = mConnection; |
+ return runBlockingOnImeThread(new Callable<Integer>() { |
+ @Override |
+ public Integer call() { |
+ return connection.getCursorCapsMode(reqModes); |
+ } |
+ }); |
+ } |
+ |
+ void dispatchKeyEvent(final KeyEvent event) { |
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
+ @Override |
+ public void run() { |
+ mImeAdapter.dispatchKeyEvent(event); |
+ } |
+ }); |
+ } |
+ |
+ /** |
+ * Focus element, wait for a single state update, reset state update list. |
+ * @param id ID of the element to focus. |
+ */ |
+ void focusElementAndWaitForStateUpdate(String id) |
+ throws InterruptedException, TimeoutException { |
+ resetUpdateSelectionList(); |
+ focusElement(id); |
+ waitAndVerifyUpdateSelection(0, 0, 0, -1, -1); |
+ resetUpdateSelectionList(); |
+ } |
+ |
+ void focusElement(final String id) throws InterruptedException, TimeoutException { |
+ focusElement(id, true); |
+ } |
+ |
+ void focusElement(final String id, boolean shouldShowKeyboard) |
+ throws InterruptedException, TimeoutException { |
+ DOMUtils.focusNode(getWebContents(), id); |
+ assertWaitForKeyboardStatus(shouldShowKeyboard); |
+ CriteriaHelper.pollInstrumentationThread(Criteria.equals(id, new Callable<String>() { |
+ @Override |
+ public String call() throws Exception { |
+ return DOMUtils.getFocusedNode(getWebContents()); |
+ } |
+ })); |
+ // When we focus another element, the connection may be recreated. |
+ mConnection = getInputConnection(); |
+ } |
+ |
+ static class TestInputConnectionFactory implements ChromiumBaseInputConnection.Factory { |
+ private final ChromiumBaseInputConnection.Factory mFactory; |
+ |
+ private final List<Integer> mTextInputTypeList = new ArrayList<>(); |
+ private EditorInfo mOutAttrs; |
+ |
+ public TestInputConnectionFactory(ChromiumBaseInputConnection.Factory factory) { |
+ mFactory = factory; |
+ } |
+ |
+ @Override |
+ public ChromiumBaseInputConnection initializeAndGet(View view, ImeAdapter imeAdapter, |
+ int inputType, int inputFlags, int inputMode, int selectionStart, int selectionEnd, |
+ EditorInfo outAttrs) { |
+ mTextInputTypeList.add(inputType); |
+ mOutAttrs = outAttrs; |
+ return mFactory.initializeAndGet(view, imeAdapter, inputType, inputMode, inputFlags, |
+ selectionStart, selectionEnd, outAttrs); |
+ } |
+ |
+ @Override |
+ public Handler getHandler() { |
+ return mFactory.getHandler(); |
+ } |
+ |
+ public Integer[] getTextInputTypeHistory() { |
+ Integer[] result = new Integer[mTextInputTypeList.size()]; |
+ mTextInputTypeList.toArray(result); |
+ return result; |
+ } |
+ |
+ public void clearTextInputTypeHistory() { |
+ mTextInputTypeList.clear(); |
+ } |
+ |
+ public EditorInfo getOutAttrs() { |
+ return mOutAttrs; |
+ } |
+ |
+ @Override |
+ public void onWindowFocusChanged(boolean gainFocus) { |
+ mFactory.onWindowFocusChanged(gainFocus); |
+ } |
+ |
+ @Override |
+ public void onViewFocusChanged(boolean gainFocus) { |
+ mFactory.onViewFocusChanged(gainFocus); |
+ } |
+ |
+ @Override |
+ public void onViewAttachedToWindow() { |
+ mFactory.onViewAttachedToWindow(); |
+ } |
+ |
+ @Override |
+ public void onViewDetachedFromWindow() { |
+ mFactory.onViewDetachedFromWindow(); |
+ } |
+ } |
+} |