1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/tests/components/GeckoViewComponent.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,202 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.tests.components; 1.9 + 1.10 +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull; 1.11 +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotSame; 1.12 +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; 1.13 + 1.14 +import org.mozilla.gecko.R; 1.15 +import org.mozilla.gecko.tests.UITestContext; 1.16 +import org.mozilla.gecko.tests.helpers.FrameworkHelper; 1.17 +import org.mozilla.gecko.tests.helpers.WaitHelper; 1.18 + 1.19 +import android.content.Context; 1.20 +import android.content.ContextWrapper; 1.21 +import android.os.Handler; 1.22 +import android.os.Looper; 1.23 +import android.view.View; 1.24 +import android.view.inputmethod.EditorInfo; 1.25 +import android.view.inputmethod.InputConnection; 1.26 +import android.view.inputmethod.InputMethodManager; 1.27 + 1.28 +import com.jayway.android.robotium.solo.Condition; 1.29 + 1.30 +/** 1.31 + * A class representing any interactions that take place on GeckoView. 1.32 + */ 1.33 +public class GeckoViewComponent extends BaseComponent { 1.34 + 1.35 + public interface InputConnectionTest { 1.36 + public void test(InputConnection ic, EditorInfo info); 1.37 + } 1.38 + 1.39 + public final TextInput mTextInput; 1.40 + 1.41 + public GeckoViewComponent(final UITestContext testContext) { 1.42 + super(testContext); 1.43 + mTextInput = new TextInput(); 1.44 + } 1.45 + 1.46 + /** 1.47 + * Returns the GeckoView. 1.48 + */ 1.49 + private View getView() { 1.50 + // Solo.getView asserts returning a valid View 1.51 + return mSolo.getView(R.id.layer_view); 1.52 + } 1.53 + 1.54 + private void setContext(final Context newContext) { 1.55 + final View geckoView = getView(); 1.56 + // Switch to a no-InputMethodManager context to avoid interference 1.57 + mTestContext.getInstrumentation().runOnMainSync(new Runnable() { 1.58 + @Override 1.59 + public void run() { 1.60 + FrameworkHelper.setViewContext(geckoView, newContext); 1.61 + } 1.62 + }); 1.63 + } 1.64 + 1.65 + public class TextInput { 1.66 + private TextInput() { 1.67 + } 1.68 + 1.69 + private InputMethodManager getInputMethodManager() { 1.70 + final InputMethodManager imm = (InputMethodManager) 1.71 + mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); 1.72 + fAssertNotNull("Must have an InputMethodManager", imm); 1.73 + return imm; 1.74 + } 1.75 + 1.76 + /** 1.77 + * Returns whether text input is being directed to the GeckoView. 1.78 + */ 1.79 + private boolean isActive() { 1.80 + return getInputMethodManager().isActive(getView()); 1.81 + } 1.82 + 1.83 + public TextInput assertActive() { 1.84 + fAssertTrue("Current view should be the active input view", isActive()); 1.85 + return this; 1.86 + } 1.87 + 1.88 + public TextInput waitForActive() { 1.89 + WaitHelper.waitFor("current view to become the active input view", new Condition() { 1.90 + @Override 1.91 + public boolean isSatisfied() { 1.92 + return isActive(); 1.93 + } 1.94 + }); 1.95 + return this; 1.96 + } 1.97 + 1.98 + /** 1.99 + * Returns whether an InputConnection is avaiable. 1.100 + * An InputConnection is available when text input is being directed to the 1.101 + * GeckoView, and a text field (input, textarea, contentEditable, etc.) is 1.102 + * currently focused inside the GeckoView. 1.103 + */ 1.104 + private boolean hasInputConnection() { 1.105 + final InputMethodManager imm = getInputMethodManager(); 1.106 + return imm.isActive(getView()) && imm.isAcceptingText(); 1.107 + } 1.108 + 1.109 + public TextInput assertInputConnection() { 1.110 + fAssertTrue("Current view should have an active InputConnection", hasInputConnection()); 1.111 + return this; 1.112 + } 1.113 + 1.114 + public TextInput waitForInputConnection() { 1.115 + WaitHelper.waitFor("current view to have an active InputConnection", new Condition() { 1.116 + @Override 1.117 + public boolean isSatisfied() { 1.118 + return hasInputConnection(); 1.119 + } 1.120 + }); 1.121 + return this; 1.122 + } 1.123 + 1.124 + /** 1.125 + * Starts an InputConnectionTest. An InputConnectionTest must run on the 1.126 + * InputConnection thread which may or may not be the main UI thread. Also, 1.127 + * during an InputConnectionTest, the system InputMethodManager service must 1.128 + * be temporarily disabled to prevent the system IME from interfering with our 1.129 + * tests. We disable the service by override the GeckoView's context with one 1.130 + * that returns a null InputMethodManager service. 1.131 + * 1.132 + * @param test Test to run 1.133 + */ 1.134 + public TextInput testInputConnection(final InputConnectionTest test) { 1.135 + 1.136 + fAssertNotNull("Test must not be null", test); 1.137 + assertInputConnection(); 1.138 + 1.139 + // GeckoInputConnection can run on another thread than the main thread, 1.140 + // so we need to be testing it on that same thread it's running on 1.141 + final View geckoView = getView(); 1.142 + final Handler inputConnectionHandler = geckoView.getHandler(); 1.143 + final Context oldGeckoViewContext = FrameworkHelper.getViewContext(geckoView); 1.144 + 1.145 + setContext(new ContextWrapper(oldGeckoViewContext) { 1.146 + @Override 1.147 + public Object getSystemService(String name) { 1.148 + if (Context.INPUT_METHOD_SERVICE.equals(name)) { 1.149 + return null; 1.150 + } 1.151 + return super.getSystemService(name); 1.152 + } 1.153 + }); 1.154 + 1.155 + (new InputConnectionTestRunner(test)).runOnHandler(inputConnectionHandler); 1.156 + 1.157 + setContext(oldGeckoViewContext); 1.158 + return this; 1.159 + } 1.160 + 1.161 + private class InputConnectionTestRunner implements Runnable { 1.162 + private final InputConnectionTest mTest; 1.163 + private boolean mDone; 1.164 + 1.165 + public InputConnectionTestRunner(final InputConnectionTest test) { 1.166 + mTest = test; 1.167 + } 1.168 + 1.169 + public synchronized void runOnHandler(final Handler inputConnectionHandler) { 1.170 + // Below, we are blocking the instrumentation thread to wait on the 1.171 + // InputConnection thread. Therefore, the InputConnection thread must not be 1.172 + // the same as the instrumentation thread to avoid a deadlock. This should 1.173 + // always be the case and we perform a sanity check to make sure. 1.174 + fAssertNotSame("InputConnection should not be running on instrumentation thread", 1.175 + Looper.myLooper(), inputConnectionHandler.getLooper()); 1.176 + 1.177 + mDone = false; 1.178 + inputConnectionHandler.post(this); 1.179 + do { 1.180 + try { 1.181 + wait(); 1.182 + } catch (InterruptedException e) { 1.183 + // Ignore interrupts 1.184 + } 1.185 + } while (!mDone); 1.186 + } 1.187 + 1.188 + @Override 1.189 + public void run() { 1.190 + final EditorInfo info = new EditorInfo(); 1.191 + final InputConnection ic = getView().onCreateInputConnection(info); 1.192 + fAssertNotNull("Must have an InputConnection", ic); 1.193 + // Restore the IC to a clean state 1.194 + ic.clearMetaKeyStates(-1); 1.195 + ic.finishComposingText(); 1.196 + mTest.test(ic, info); 1.197 + synchronized (this) { 1.198 + // Test finished; return from runOnHandler 1.199 + mDone = true; 1.200 + notify(); 1.201 + } 1.202 + } 1.203 + } 1.204 + } 1.205 +}