Bug 1456190 - 2. Add evaluateJS API to GV test; r?snorp draft
authorJim Chen <nchen@mozilla.com>
Fri, 27 Apr 2018 11:57:14 -0400
changeset 789105 458706ad3fa505d0e0fe9d2f4fd982b4eb78b941
parent 789104 5688694d8fdf4fe4e7d941f88543ffe147198e5a
child 789106 16256d0f62ab1f19f4ff82e1245ee3503d6365fd
push id108180
push userbmo:nchen@mozilla.com
push dateFri, 27 Apr 2018 15:59:58 +0000
reviewerssnorp
bugs1456190
milestone61.0a1
Bug 1456190 - 2. Add evaluateJS API to GV test; r?snorp Add the GeckoSessionTestRule.evaluateJS() API to evaluate JS expression in a particular session. The API is enabled by the new @WithDevToolsAPI annotation that enables RDP connections to Gecko. MozReview-Commit-ID: 1cuX359t2Wn
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
@@ -95,9 +95,21 @@ open class BaseSessionTest(noErrorCollec
     fun GeckoSession.delegateUntilTestEnd(callback: Any) =
             sessionRule.delegateUntilTestEnd(this, callback)
 
     fun GeckoSession.delegateDuringNextWait(callback: Any) =
             sessionRule.delegateDuringNextWait(this, callback)
 
     fun GeckoSession.synthesizeTap(x: Int, y: Int) =
             sessionRule.synthesizeTap(this, x, y)
-}
+
+    fun GeckoSession.evaluateJS(js: String) =
+            sessionRule.evaluateJS(this, js)
+
+    infix fun Any?.dot(prop: Any): Any? =
+            if (prop is Int) this.asJSList<Any>()[prop] else this.asJSMap<Any>()[prop]
+
+    @Suppress("UNCHECKED_CAST")
+    fun <T> Any?.asJSMap(): Map<String, T> = this as Map<String, T>
+
+    @Suppress("UNCHECKED_CAST")
+    fun <T> Any?.asJSList(): List<T> = this as List<T>
+}
\ No newline at end of file
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
@@ -6,31 +6,34 @@
 package org.mozilla.geckoview.test.rule;
 
 import org.mozilla.gecko.gfx.GeckoDisplay;
 import org.mozilla.geckoview.BuildConfig;
 import org.mozilla.geckoview.GeckoRuntime;
 import org.mozilla.geckoview.GeckoRuntimeSettings;
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
+import org.mozilla.geckoview.test.rdp.RDPConnection;
+import org.mozilla.geckoview.test.rdp.Tab;
 import org.mozilla.geckoview.test.util.Callbacks;
 
 import static org.hamcrest.Matchers.*;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.fail;
 
 import org.hamcrest.Matcher;
 
 import org.junit.rules.ErrorCollector;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
 
 import android.app.Instrumentation;
 import android.graphics.Point;
 import android.graphics.SurfaceTexture;
+import android.net.LocalSocketAddress;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Debug;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.MessageQueue;
 import android.os.SystemClock;
@@ -143,16 +146,25 @@ public class GeckoSessionTestRule extend
         @Target({ElementType.METHOD, ElementType.TYPE})
         @Retention(RetentionPolicy.RUNTIME)
         @interface List {
             NullDelegate[] value();
         }
     }
 
     /**
+     * Specify that the test uses DevTools-enabled APIs, such as {@link #evaluateJS}.
+     */
+    @Target({ElementType.METHOD, ElementType.TYPE})
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface WithDevToolsAPI {
+        boolean value() default true;
+    }
+
+    /**
      * Specify a list of GeckoSession settings to be applied to the GeckoSession object
      * under test. Can be used on classes or methods. Note that the settings values must
      * be string literals regardless of the type of the settings.
      * <p>
      * Disable e10s for a particular test:
      * <pre>
      * &#64;Setting.List(&#64;Setting(key = Setting.Key.USE_MULTIPROCESS,
      *                        value = "false"))
@@ -554,17 +566,19 @@ public class GeckoSessionTestRule extend
             addCallbackClasses(list, ifce);
         }
 
         final HashSet<Class<?>> set = new HashSet<>(list);
         return set.toArray(new Class<?>[set.size()]);
     }
 
     private static final List<Class<?>> CALLBACK_CLASSES = Arrays.asList(getCallbackClasses());
+
     private static GeckoRuntime sRuntime;
+    private static RDPConnection sRDPConnection;
     private static long sLongestWait;
 
     public final Environment env = new Environment();
 
     protected final Instrumentation mInstrumentation =
             InstrumentationRegistry.getInstrumentation();
     protected final GeckoSessionSettings mDefaultSettings;
     protected final Set<GeckoSession> mSubSessions = new HashSet<>();
@@ -581,16 +595,18 @@ public class GeckoSessionTestRule extend
     protected int mLastWaitEnd;
     protected MethodCall mCurrentMethodCall;
     protected long mTimeoutMillis;
     protected Point mDisplaySize;
     protected SurfaceTexture mDisplayTexture;
     protected Surface mDisplaySurface;
     protected GeckoDisplay mDisplay;
     protected boolean mClosedSession;
+    protected boolean mWithDevTools;
+    protected Map<GeckoSession, Tab> mRDPTabs;
 
     public GeckoSessionTestRule() {
         mDefaultSettings = new GeckoSessionSettings();
         mDefaultSettings.setBoolean(GeckoSessionSettings.USE_MULTIPROCESS, env.isMultiprocess());
     }
 
     /**
      * Set an ErrorCollector for assertion errors, or null to not use one.
@@ -718,16 +734,18 @@ public class GeckoSessionTestRule extend
                 for (final NullDelegate nullDelegate : ((NullDelegate.List) annotation).value()) {
                     addNullDelegate(nullDelegate.value());
                 }
             } else if (WithDisplay.class.equals(annotation.annotationType())) {
                 final WithDisplay displaySize = (WithDisplay)annotation;
                 mDisplaySize = new Point(displaySize.width(), displaySize.height());
             } else if (ClosedSessionAtStart.class.equals(annotation.annotationType())) {
                 mClosedSession = ((ClosedSessionAtStart) annotation).value();
+            } else if (WithDevToolsAPI.class.equals(annotation.annotationType())) {
+                mWithDevTools = ((WithDevToolsAPI) annotation).value();
             }
         }
     }
 
     private static RuntimeException unwrapRuntimeException(final Throwable e) {
         final Throwable cause = e.getCause();
         if (cause != null && cause instanceof RuntimeException) {
             return (RuntimeException) cause;
@@ -748,28 +766,32 @@ public class GeckoSessionTestRule extend
     }
 
     protected void prepareStatement(final Description description) throws Throwable {
         final GeckoSessionSettings settings = new GeckoSessionSettings(mDefaultSettings);
         mTimeoutMillis = env.isDebugging() ? DEFAULT_IDE_DEBUG_TIMEOUT_MILLIS
                                            : getDefaultTimeoutMillis();
         mNullDelegates = new HashSet<>();
         mClosedSession = false;
+        mWithDevTools = false;
 
         applyAnnotations(Arrays.asList(description.getTestClass().getAnnotations()), settings);
         applyAnnotations(description.getAnnotations(), settings);
 
         final List<CallRecord> records = new ArrayList<>();
         final CallbackDelegates waitDelegates = new CallbackDelegates();
         final CallbackDelegates testDelegates = new CallbackDelegates();
         mCallRecords = records;
         mWaitScopeDelegates = waitDelegates;
         mTestScopeDelegates = testDelegates;
         mLastWaitStart = 0;
         mLastWaitEnd = 0;
+        if (mWithDevTools) {
+            mRDPTabs = new HashMap<>();
+        }
 
         final InvocationHandler recorder = new InvocationHandler() {
             @Override
             public Object invoke(final Object proxy, final Method method,
                                  final Object[] args) {
                 boolean ignore = false;
                 MethodCall call = null;
 
@@ -817,16 +839,18 @@ public class GeckoSessionTestRule extend
                 new GeckoRuntimeSettings.Builder();
             runtimeSettingsBuilder.arguments(new String[] { "-purgecaches" })
                                   .extras(InstrumentationRegistry.getArguments());
             sRuntime = GeckoRuntime.create(
                 InstrumentationRegistry.getTargetContext(),
                 runtimeSettingsBuilder.build());
         }
 
+        sRuntime.getSettings().setRemoteDebuggingEnabled(mWithDevTools);
+
         mMainSession = new GeckoSession(settings);
         prepareSession(mMainSession);
 
         if (mDisplaySize != null) {
             mDisplayTexture = new SurfaceTexture(0);
             mDisplaySurface = new Surface(mDisplayTexture);
             mDisplay = mMainSession.acquireDisplay();
             mDisplay.surfaceChanged(mDisplaySurface, mDisplaySize.x, mDisplaySize.y);
@@ -849,16 +873,32 @@ public class GeckoSessionTestRule extend
      * Call open() on a session, and ensure it's ready for use by the test. In particular,
      * remove any extra calls recorded as part of opening the session.
      *
      * @param session Session to open.
      */
     public void openSession(final GeckoSession session) {
         session.open(sRuntime);
         waitForInitialLoad(session);
+
+        if (mWithDevTools) {
+            if (sRDPConnection == null) {
+                final String dataDir = InstrumentationRegistry.getTargetContext()
+                                                              .getApplicationInfo().dataDir;
+                final LocalSocketAddress address = new LocalSocketAddress(
+                        dataDir + "/firefox-debugger-socket",
+                        LocalSocketAddress.Namespace.FILESYSTEM);
+                sRDPConnection = new RDPConnection(address);
+                sRDPConnection.setTimeout((int) Math.min(DEFAULT_TIMEOUT_MILLIS,
+                                                         Integer.MAX_VALUE));
+            }
+            final Tab tab = sRDPConnection.getMostRecentTab();
+            tab.attach();
+            mRDPTabs.put(session, tab);
+        }
     }
 
     private void waitForInitialLoad(final GeckoSession session) {
         // We receive an initial about:blank load; don't expose that to the test.
         // The about:blank load is bounded by onLocationChange and onPageStop calls,
         // so find the first about:blank onLocationChange, then the next onPageStop,
         // and ignore everything in-between from that session.
 
@@ -904,16 +944,21 @@ public class GeckoSessionTestRule extend
      * Internal method to perform callback checks at the end of a test.
      */
     public void performTestEndCheck() {
         mWaitScopeDelegates.clear();
         mTestScopeDelegates.clear();
     }
 
     protected void cleanupSession(final GeckoSession session) {
+        final Tab tab = (mRDPTabs != null) ? mRDPTabs.get(session) : null;
+        if (tab != null) {
+            tab.detach();
+            mRDPTabs.remove(session);
+        }
         if (session.isOpen()) {
             session.close();
         }
     }
 
     protected void cleanupStatement() throws Throwable {
         for (final GeckoSession session : mSubSessions) {
             cleanupSession(session);
@@ -934,16 +979,17 @@ public class GeckoSessionTestRule extend
         mCallbackProxy = null;
         mNullDelegates = null;
         mCallRecords = null;
         mWaitScopeDelegates = null;
         mTestScopeDelegates = null;
         mLastWaitStart = 0;
         mLastWaitEnd = 0;
         mTimeoutMillis = 0;
+        mRDPTabs = null;
     }
 
     @Override
     public Statement apply(final Statement base, final Description description) {
         return super.apply(new Statement() {
             @Override
             public void evaluate() throws Throwable {
                 try {
@@ -1559,9 +1605,27 @@ public class GeckoSessionTestRule extend
      * @param values Input array
      * @return Value from input array indexed by the current call counter.
      */
     @SafeVarargs
     public final <T> T forEachCall(T... values) {
         assertThat("Should be in a method call", mCurrentMethodCall, notNullValue());
         return values[Math.min(mCurrentMethodCall.getCurrentCount(), values.length) - 1];
     }
+
+    /**
+     * Evaluate a JavaScript expression in the context of the target page and return the result.
+     * RDP must be enabled first using the {@link WithDevToolsAPI} annotation. String, number, and
+     * boolean results are converted to Java values. Undefined and null results are returned as
+     * null. Objects are returned as Map instances. Arrays are returned as Object[] instances.
+     *
+     * @param session Session containing the target page.
+     * @param js JavaScript expression.
+     * @return Result of evaluating the expression.
+     */
+    public Object evaluateJS(final @NonNull GeckoSession session, final @NonNull String js) {
+        assertThat("Must enable RDP using @WithDevToolsAPI", mRDPTabs, notNullValue());
+
+        final Tab tab = mRDPTabs.get(session);
+        assertThat("Session should have tab object", tab, notNullValue());
+        return tab.getConsole().evaluateJS(js);
+    }
 }