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
--- 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>
* @Setting.List(@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);
+ }
}