Bug 968409 - Add GeckoViewComponent for UITests; r=mcomella
authorJim Chen <nchen@mozilla.com>
Thu, 27 Feb 2014 16:17:55 -0500
changeset 171097 385d0c08dfe25c75eba936e774f6cd0960b8d6b4
parent 171096 081f28069e48d5ed0d9b318660dfe49e32ae3db0
child 171098 38921bba8197f4f5ce1fd10ef3135ec31f564367
push id26315
push usercbook@mozilla.com
push dateFri, 28 Feb 2014 13:32:24 +0000
treeherdermozilla-central@5acf5aefe27a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcomella
bugs968409
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 968409 - Add GeckoViewComponent for UITests; r=mcomella
mobile/android/base/tests/UITest.java
mobile/android/base/tests/UITestContext.java
mobile/android/base/tests/components/GeckoViewComponent.java
--- a/mobile/android/base/tests/UITest.java
+++ b/mobile/android/base/tests/UITest.java
@@ -54,16 +54,17 @@ abstract class UITest extends ActivityIn
 
     // Base to build hostname URLs
     private String mBaseHostnameUrl;
     // Base to build IP URLs
     private String mBaseIpUrl;
 
     protected AboutHomeComponent mAboutHome;
     protected AppMenuComponent mAppMenu;
+    protected GeckoViewComponent mGeckoView;
     protected ToolbarComponent mToolbar;
 
     static {
         try {
             sLauncherActivityClass = (Class<Activity>) Class.forName(LAUNCHER_ACTIVITY);
         } catch (ClassNotFoundException e) {
             throw new RuntimeException(e);
         }
@@ -117,16 +118,17 @@ abstract class UITest extends ActivityIn
         }
 
         super.tearDown();
     }
 
     private void initComponents() {
         mAboutHome = new AboutHomeComponent(this);
         mAppMenu = new AppMenuComponent(this);
+        mGeckoView = new GeckoViewComponent(this);
         mToolbar = new ToolbarComponent(this);
     }
 
     private void initHelpers() {
         HelperInitializer.init(this);
     }
 
     @Override
@@ -163,16 +165,19 @@ abstract class UITest extends ActivityIn
     public BaseComponent getComponent(final ComponentType type) {
         switch (type) {
             case ABOUTHOME:
                 return mAboutHome;
 
             case APPMENU:
                 return mAppMenu;
 
+            case GECKOVIEW:
+                return mGeckoView;
+
             case TOOLBAR:
                 return mToolbar;
 
             default:
                 fail("Unknown component type, " + type + ".");
                 return null; // Should not reach this statement but required by javac.
         }
     }
--- a/mobile/android/base/tests/UITestContext.java
+++ b/mobile/android/base/tests/UITestContext.java
@@ -17,16 +17,17 @@ import android.app.Instrumentation;
 /**
  * Interface to the global information about a UITest environment.
  */
 public interface UITestContext {
 
     public static enum ComponentType {
         ABOUTHOME,
         APPMENU,
+        GECKOVIEW,
         TOOLBAR
     }
 
     public Activity getActivity();
     public Solo getSolo();
     public Assert getAsserter();
     public Driver getDriver();
     public Actions getActions();
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/components/GeckoViewComponent.java
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.components;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+import org.mozilla.gecko.tests.helpers.*;
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.R;
+
+import com.jayway.android.robotium.solo.Condition;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+
+/**
+ * A class representing any interactions that take place on GeckoView.
+ */
+public class GeckoViewComponent extends BaseComponent {
+
+    public interface InputConnectionTest {
+        public void test(InputConnection ic, EditorInfo info);
+    }
+
+    public final TextInput mTextInput;
+
+    public GeckoViewComponent(final UITestContext testContext) {
+        super(testContext);
+        mTextInput = new TextInput();
+    }
+
+    /**
+     * Returns the GeckoView.
+     */
+    private View getView() {
+        // Solo.getView asserts returning a valid View
+        return mSolo.getView(R.id.layer_view);
+    }
+
+    private void setContext(final Context newContext) {
+        final View geckoView = getView();
+        // Switch to a no-InputMethodManager context to avoid interference
+        mTestContext.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                FrameworkHelper.setViewContext(geckoView, newContext);
+            }
+        });
+    }
+
+    public class TextInput {
+        private TextInput() {
+        }
+
+        private InputMethodManager getInputMethodManager() {
+            final InputMethodManager imm = (InputMethodManager)
+                mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
+            assertNotNull("Must have an InputMethodManager", imm);
+            return imm;
+        }
+
+        /**
+         * Returns whether text input is being directed to the GeckoView.
+         */
+        private boolean isActive() {
+            return getInputMethodManager().isActive(getView());
+        }
+
+        public TextInput assertActive() {
+            assertTrue("Current view should be the active input view", isActive());
+            return this;
+        }
+
+        public TextInput waitForActive() {
+            WaitHelper.waitFor("current view to become the active input view", new Condition() {
+                @Override
+                public boolean isSatisfied() {
+                    return isActive();
+                }
+            });
+            return this;
+        }
+
+        /**
+         * Returns whether an InputConnection is avaiable.
+         * An InputConnection is available when text input is being directed to the
+         * GeckoView, and a text field (input, textarea, contentEditable, etc.) is
+         * currently focused inside the GeckoView.
+         */
+        private boolean hasInputConnection() {
+            final InputMethodManager imm = getInputMethodManager();
+            return imm.isActive(getView()) && imm.isAcceptingText();
+        }
+
+        public TextInput assertInputConnection() {
+            assertTrue("Current view should have an active InputConnection", hasInputConnection());
+            return this;
+        }
+
+        public TextInput waitForInputConnection() {
+            WaitHelper.waitFor("current view to have an active InputConnection", new Condition() {
+                @Override
+                public boolean isSatisfied() {
+                    return hasInputConnection();
+                }
+            });
+            return this;
+        }
+
+        /**
+         * Starts an InputConnectionTest. An InputConnectionTest must run on the
+         * InputConnection thread which may or may not be the main UI thread. Also,
+         * during an InputConnectionTest, the system InputMethodManager service must
+         * be temporarily disabled to prevent the system IME from interfering with our
+         * tests. We disable the service by override the GeckoView's context with one
+         * that returns a null InputMethodManager service.
+         *
+         * @param test Test to run
+         */
+        public TextInput testInputConnection(final InputConnectionTest test) {
+
+            assertNotNull("Test must not be null", test);
+            assertInputConnection();
+
+            // GeckoInputConnection can run on another thread than the main thread,
+            // so we need to be testing it on that same thread it's running on
+            final View geckoView = getView();
+            final Handler inputConnectionHandler = geckoView.getHandler();
+            final Context oldGeckoViewContext = FrameworkHelper.getViewContext(geckoView);
+
+            setContext(new ContextWrapper(oldGeckoViewContext) {
+                @Override
+                public Object getSystemService(String name) {
+                    if (Context.INPUT_METHOD_SERVICE.equals(name)) {
+                        return null;
+                    }
+                    return super.getSystemService(name);
+                }
+            });
+
+            (new InputConnectionTestRunner(test)).runOnHandler(inputConnectionHandler);
+
+            setContext(oldGeckoViewContext);
+            return this;
+        }
+
+        private class InputConnectionTestRunner implements Runnable {
+            private final InputConnectionTest mTest;
+            private boolean mDone;
+
+            public InputConnectionTestRunner(final InputConnectionTest test) {
+                mTest = test;
+            }
+
+            public synchronized void runOnHandler(final Handler inputConnectionHandler) {
+                // Below, we are blocking the instrumentation thread to wait on the
+                // InputConnection thread. Therefore, the InputConnection thread must not be
+                // the same as the instrumentation thread to avoid a deadlock. This should
+                // always be the case and we perform a sanity check to make sure.
+                assertNotSame("InputConnection should not be running on instrumentation thread",
+                    Looper.myLooper(), inputConnectionHandler.getLooper());
+
+                mDone = false;
+                inputConnectionHandler.post(this);
+                do {
+                    try {
+                        wait();
+                    } catch (InterruptedException e) {
+                        // Ignore interrupts
+                    }
+                } while (!mDone);
+            }
+
+            @Override
+            public void run() {
+                final EditorInfo info = new EditorInfo();
+                final InputConnection ic = getView().onCreateInputConnection(info);
+                assertNotNull("Must have an InputConnection", ic);
+                // Restore the IC to a clean state
+                ic.clearMetaKeyStates(-1);
+                ic.finishComposingText();
+                mTest.test(ic, info);
+                synchronized (this) {
+                    // Test finished; return from runOnHandler
+                    mDone = true;
+                    notify();
+                }
+            }
+        }
+    }
+}