--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
@@ -683,9 +683,78 @@ class GeckoSessionTestRuleTest {
assertThat("Wait delegate should be used", waitCounter, equalTo(2))
sessionRule.session.reload()
sessionRule.waitForPageStop()
assertThat("Test delegate should be used", testCounter, equalTo(6))
assertThat("Wait delegate should be cleared", waitCounter, equalTo(2))
}
-}
\ No newline at end of file
+
+ @Test fun wrapSession() {
+ val session = sessionRule.wrapSession(GeckoSession(sessionRule.session.settings))
+ sessionRule.openSession(session)
+ session.reload()
+ session.waitForPageStop()
+ }
+
+ @Test fun createOpenSession() {
+ val newSession = sessionRule.createOpenSession()
+ assertThat("Can create session", newSession, notNullValue())
+ assertThat("New session is open", newSession.isOpen, equalTo(true))
+ assertThat("New session has same settings",
+ newSession.settings, equalTo(sessionRule.session.settings))
+ }
+
+ @Test fun createOpenSession_withSettings() {
+ val settings = GeckoSessionSettings(sessionRule.session.settings)
+ settings.setBoolean(GeckoSessionSettings.USE_PRIVATE_MODE, true)
+
+ val newSession = sessionRule.createOpenSession(settings)
+ assertThat("New session has same settings", newSession.settings, equalTo(settings))
+ }
+
+ @Test fun createOpenSession_canInterleaveOtherCalls() {
+ sessionRule.session.loadTestPath(HELLO_HTML_PATH)
+
+ val newSession = sessionRule.createOpenSession()
+ sessionRule.session.loadTestPath(HELLO_HTML_PATH)
+ sessionRule.waitForPageStops(2)
+
+ newSession.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
+ @AssertCalled(false)
+ override fun onPageStop(session: GeckoSession, success: Boolean) {
+ }
+ })
+
+ sessionRule.session.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
+ @AssertCalled(count = 2)
+ override fun onPageStop(session: GeckoSession, success: Boolean) {
+ }
+ })
+ }
+
+ @Test fun createClosedSession() {
+ val newSession = sessionRule.createClosedSession()
+ assertThat("Can create session", newSession, notNullValue())
+ assertThat("New session is open", newSession.isOpen, equalTo(false))
+ assertThat("New session has same settings",
+ newSession.settings, equalTo(sessionRule.session.settings))
+ }
+
+ @Test fun createClosedSession_withSettings() {
+ val settings = GeckoSessionSettings(sessionRule.session.settings)
+ settings.setBoolean(GeckoSessionSettings.USE_PRIVATE_MODE, true)
+
+ val newSession = sessionRule.createClosedSession(settings)
+ assertThat("New session has same settings", newSession.settings, equalTo(settings))
+ }
+
+ @Test(expected = AssertionError::class)
+ @TimeoutMillis(1000)
+ @LargeTest
+ @GeckoSessionTestRule.ClosedSessionAtStart
+ fun noPendingCallbacks_withSpecificSession() {
+ sessionRule.createOpenSession()
+ // Make sure we don't have unexpected pending callbacks after opening a session.
+ sessionRule.waitUntilCalled(object : Callbacks.All {})
+ }
+}
--- 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
@@ -47,16 +47,17 @@ import java.lang.reflect.ParameterizedTy
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.regex.Pattern;
import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KClass;
/**
* TestRule that, for each test, sets up a GeckoSession, runs the test on the UI thread,
* and tears down the GeckoSession at the end of the test. The rule also provides methods
@@ -64,16 +65,30 @@ import kotlin.reflect.KClass;
* callbacks are called in the proper order.
*/
public class GeckoSessionTestRule extends UiThreadTestRule {
private static final long DEFAULT_TIMEOUT_MILLIS = 10000;
private static final long DEFAULT_DEBUG_TIMEOUT_MILLIS = 86400000;
public static final String APK_URI_PREFIX = "resource://android/";
+ private static final Method sOnLocationChange;
+ private static final Method sOnPageStop;
+
+ static {
+ try {
+ sOnLocationChange = GeckoSession.NavigationDelegate.class.getMethod(
+ "onLocationChange", GeckoSession.class, String.class);
+ sOnPageStop = GeckoSession.ProgressDelegate.class.getMethod(
+ "onPageStop", GeckoSession.class, boolean.class);
+ } catch (final NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
/**
* Specify the timeout for any of the wait methods, in milliseconds. Can be used
* on classes or methods.
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeoutMillis {
long value();
@@ -347,16 +362,20 @@ public class GeckoSessionTestRule extend
public CallRecord(final Method method, final Object[] args) {
this.method = method;
this.methodCall = new MethodCall(method, /* requirement */ null);
this.args = args;
}
}
+ protected interface CallRecordHandler {
+ boolean handleCall(Method method, Object[] args);
+ }
+
public class Environment {
/* package */ Environment() {
}
private String getEnvVar(final String name) {
final int nameLen = name.length();
final Bundle args = InstrumentationRegistry.getArguments();
String env = args.getString("env0", null);
@@ -371,17 +390,17 @@ public class GeckoSessionTestRule extend
return "";
}
public boolean isAutomation() {
return !getEnvVar("MOZ_IN_AUTOMATION").isEmpty();
}
public boolean isE10s() {
- return mSession.getSettings().getBoolean(
+ return mMainSession.getSettings().getBoolean(
GeckoSessionSettings.USE_MULTIPROCESS);
}
public boolean isDebugging() {
return Debug.isDebuggerConnected();
}
}
@@ -476,28 +495,30 @@ public class GeckoSessionTestRule extend
private static final List<Class<?>> CALLBACK_CLASSES = Arrays.asList(getCallbackClasses());
public final Environment env = new Environment();
protected final Instrumentation mInstrumentation =
InstrumentationRegistry.getInstrumentation();
protected final GeckoSessionSettings mDefaultSettings;
+ protected final Set<GeckoSession> mSubSessions = new HashSet<>();
protected ErrorCollector mErrorCollector;
- protected GeckoSession mSession;
- protected Point mDisplaySize;
+ protected GeckoSession mMainSession;
protected Object mCallbackProxy;
protected List<CallRecord> mCallRecords;
+ protected CallRecordHandler mCallRecordHandler;
protected CallbackDelegates mWaitScopeDelegates;
protected CallbackDelegates mTestScopeDelegates;
protected int mLastWaitStart;
protected int mLastWaitEnd;
protected MethodCall mCurrentMethodCall;
protected long mTimeoutMillis;
+ protected Point mDisplaySize;
protected SurfaceTexture mDisplayTexture;
protected Surface mDisplaySurface;
protected GeckoDisplay mDisplay;
protected boolean mClosedSession;
public GeckoSessionTestRule() {
mDefaultSettings = new GeckoSessionSettings();
}
@@ -563,17 +584,17 @@ public class GeckoSessionTestRule extend
}
/**
* Get the session set up for the current test.
*
* @return GeckoSession object.
*/
public @NonNull GeckoSession getSession() {
- return mSession;
+ return mMainSession;
}
protected static Method getCallbackSetter(final @NonNull Class<?> cls)
throws NoSuchMethodException {
return GeckoSession.class.getMethod("set" + cls.getSimpleName(), cls);
}
protected static Method getCallbackGetter(final @NonNull Class<?> cls)
@@ -596,26 +617,28 @@ public class GeckoSessionTestRule extend
final WithDisplay displaySize = (WithDisplay)annotation;
mDisplaySize = new Point(displaySize.width(), displaySize.height());
} else if (ClosedSessionAtStart.class.equals(annotation.annotationType())) {
mClosedSession = ((ClosedSessionAtStart) annotation).value();
}
}
}
- private static RuntimeException unwrapRuntimeException(Throwable e) {
+ private static RuntimeException unwrapRuntimeException(final Throwable e) {
final Throwable cause = e.getCause();
if (cause != null && cause instanceof RuntimeException) {
- return (RuntimeException)cause;
+ return (RuntimeException) cause;
+ } else if (e instanceof RuntimeException) {
+ return (RuntimeException) e;
}
- return new RuntimeException(cause);
+ return new RuntimeException(cause != null ? cause : e);
}
- protected void prepareSession(final Description description) throws Throwable {
+ protected void prepareStatement(final Description description) throws Throwable {
final GeckoSessionSettings settings = new GeckoSessionSettings(mDefaultSettings);
mTimeoutMillis = !env.isDebugging() ? DEFAULT_TIMEOUT_MILLIS
: DEFAULT_DEBUG_TIMEOUT_MILLIS;
mClosedSession = false;
applyAnnotations(Arrays.asList(description.getTestClass().getAnnotations()), settings);
applyAnnotations(description.getAnnotations(), settings);
@@ -627,24 +650,37 @@ public class GeckoSessionTestRule extend
mTestScopeDelegates = testDelegates;
mLastWaitStart = 0;
mLastWaitEnd = 0;
final InvocationHandler recorder = new InvocationHandler() {
@Override
public Object invoke(final Object proxy, final Method method,
final Object[] args) {
- assertThat("Callbacks must be on UI thread",
- Looper.myLooper(), equalTo(Looper.getMainLooper()));
+ boolean ignore = false;
+ MethodCall call = null;
+
+ if (Object.class.equals(method.getDeclaringClass())) {
+ ignore = true;
+ } else if (mCallRecordHandler != null) {
+ ignore = mCallRecordHandler.handleCall(method, args);
+ }
- records.add(new CallRecord(method, args));
+ if (!ignore) {
+ assertThat("Callbacks must be on UI thread",
+ Looper.myLooper(), equalTo(Looper.getMainLooper()));
+ assertThat("Callback first argument must be session object",
+ args[0], instanceOf(GeckoSession.class));
- MethodCall call = waitDelegates.prepareMethodCall(method);
- if (call == null) {
- call = testDelegates.prepareMethodCall(method);
+ records.add(new CallRecord(method, args));
+
+ call = waitDelegates.prepareMethodCall(method);
+ if (call == null) {
+ call = testDelegates.prepareMethodCall(method);
+ }
}
try {
mCurrentMethodCall = call;
return method.invoke((call != null) ? call.target
: Callbacks.Default.INSTANCE, args);
} catch (final IllegalAccessException | InvocationTargetException e) {
throw unwrapRuntimeException(e);
@@ -653,87 +689,145 @@ public class GeckoSessionTestRule extend
}
}
};
final Class<?>[] classes = CALLBACK_CLASSES.toArray(new Class<?>[CALLBACK_CLASSES.size()]);
mCallbackProxy = Proxy.newProxyInstance(GeckoSession.class.getClassLoader(),
classes, recorder);
- mSession = new GeckoSession(settings);
+ mMainSession = new GeckoSession(settings);
+ prepareSession(mMainSession);
if (mDisplaySize != null) {
mDisplayTexture = new SurfaceTexture(0);
mDisplaySurface = new Surface(mDisplayTexture);
- mDisplay = mSession.acquireDisplay();
+ mDisplay = mMainSession.acquireDisplay();
mDisplay.surfaceChanged(mDisplaySurface, mDisplaySize.x, mDisplaySize.y);
}
+ if (!mClosedSession) {
+ openSession(mMainSession);
+ }
+ }
+
+ protected void prepareSession(final GeckoSession session) throws Throwable {
for (final Class<?> cls : CALLBACK_CLASSES) {
if (cls != null) {
- getCallbackSetter(cls).invoke(mSession, mCallbackProxy);
+ getCallbackSetter(cls).invoke(session, mCallbackProxy);
}
}
+ }
- if (mClosedSession) {
+ /**
+ * Call openWindow() 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) {
+ final boolean e10s = session.getSettings().getBoolean(
+ GeckoSessionSettings.USE_MULTIPROCESS);
+
+ if (e10s) {
+ // Give any pending calls a chance to catch up.
+ loopUntilIdle(/* timeout */ 0);
+ }
+
+ session.openWindow(mInstrumentation.getTargetContext());
+
+ if (!e10s) {
return;
}
- mSession.openWindow(mInstrumentation.getTargetContext());
+ // Under e10s, 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.
+
+ try {
+ mCallRecordHandler = new CallRecordHandler() {
+ private boolean mFoundStart = false;
- if (settings.getBoolean(GeckoSessionSettings.USE_MULTIPROCESS)) {
- // Under e10s, we receive an initial about:blank load; don't expose that to the test.
- waitForPageStop();
+ @Override
+ public boolean handleCall(final Method method, final Object[] args) {
+ if (!mFoundStart && sOnLocationChange.equals(method) &&
+ session.equals(args[0]) && "about:blank".equals(args[1])) {
+ mFoundStart = true;
+ return true;
+ } else if (mFoundStart && session.equals(args[0])) {
+ if (sOnPageStop.equals(method)) {
+ mCallRecordHandler = null;
+ }
+ return true;
+ }
+ return false;
+ }
+ };
+
+ do {
+ loopUntilIdle(mTimeoutMillis);
+ } while (mCallRecordHandler != null);
+
+ } finally {
+ mCallRecordHandler = null;
}
}
/**
* Internal method to perform callback checks at the end of a test.
*/
public void performTestEndCheck() {
mWaitScopeDelegates.clear();
mTestScopeDelegates.clear();
}
- protected void cleanupSession() throws Throwable {
- if (mSession.isOpen()) {
- mSession.closeWindow();
+ protected void cleanupSession(final GeckoSession session) {
+ if (session.isOpen()) {
+ session.closeWindow();
}
+ }
+
+ protected void cleanupStatement() throws Throwable {
+ for (final GeckoSession session : mSubSessions) {
+ cleanupSession(session);
+ }
+ cleanupSession(mMainSession);
if (mDisplay != null) {
mDisplay.surfaceDestroyed();
- mSession.releaseDisplay(mDisplay);
+ mMainSession.releaseDisplay(mDisplay);
+ mDisplay = null;
mDisplaySurface.release();
+ mDisplaySurface = null;
mDisplayTexture.release();
- mDisplay = null;
mDisplayTexture = null;
- mDisplaySurface = null;
}
- mSession = null;
+ mMainSession = null;
mCallbackProxy = null;
mCallRecords = null;
mWaitScopeDelegates = null;
mTestScopeDelegates = null;
mLastWaitStart = 0;
mLastWaitEnd = 0;
mTimeoutMillis = 0;
}
@Override
public Statement apply(final Statement base, final Description description) {
return super.apply(new Statement() {
@Override
public void evaluate() throws Throwable {
try {
- prepareSession(description);
+ prepareStatement(description);
base.evaluate();
performTestEndCheck();
} finally {
- cleanupSession();
+ cleanupStatement();
}
}
}, description);
}
@Override
protected boolean shouldRunOnUiThread(final Description description) {
return true;
@@ -821,26 +915,18 @@ public class GeckoSessionTestRule extend
/**
* Wait until a page load has finished. The session must have started a page load since
* the last wait, or this method will wait indefinitely.
*
* @param count Number of page loads to wait for.
*/
public void waitForPageStops(final int count) {
- final Method onPageStop;
- try {
- onPageStop = GeckoSession.ProgressDelegate.class.getMethod(
- "onPageStop", GeckoSession.class, boolean.class);
- } catch (final NoSuchMethodException e) {
- throw new RuntimeException(e);
- }
-
final List<MethodCall> methodCalls = new ArrayList<>(1);
- methodCalls.add(new MethodCall(onPageStop,
+ methodCalls.add(new MethodCall(sOnPageStop,
new CallRequirement(/* allowed */ true, count, null)));
waitUntilCalled(GeckoSession.ProgressDelegate.class, methodCalls);
}
/**
* Wait until the specified methods have been called on the specified callback
* interface. If no methods are specified, wait until any method has been called.
@@ -1078,20 +1164,90 @@ public class GeckoSessionTestRule extend
* {@link #delegateUntilTestEnd}.
*
* @param callback Callback object, or null to clear all previously-set delegates.
*/
public void delegateDuringNextWait(final Object callback) {
mWaitScopeDelegates.delegate(callback);
}
- public void synthesizeTap(int x, int y) {
+ /**
+ * Synthesize a tap event at the specified location using the main session.
+ * The session must have been created with a display.
+ *
+ * @param x X coordinate
+ * @param y Y coordinate
+ */
+ public void synthesizeTap(final int x, final int y) {
final long downTime = SystemClock.uptimeMillis();
- final MotionEvent down = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
- MotionEvent.ACTION_DOWN, x, y, 0);
-
+ final MotionEvent down = MotionEvent.obtain(
+ downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, x, y, 0);
mSession.getPanZoomController().onTouchEvent(down);
- final MotionEvent up = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
- MotionEvent.ACTION_UP, x, y, 0);
+ final MotionEvent up = MotionEvent.obtain(
+ downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0);
mSession.getPanZoomController().onTouchEvent(up);
}
+
+ /**
+ * Initialize and keep track of the specified session within the test rule. The
+ * session is automatically cleaned up at the end of the test.
+ *
+ * @param session Session to keep track of.
+ * @return Same session
+ */
+ public GeckoSession wrapSession(final GeckoSession session) {
+ try {
+ mSubSessions.add(session);
+ prepareSession(session);
+ } catch (final Throwable e) {
+ throw unwrapRuntimeException(e);
+ }
+ return session;
+ }
+
+ private GeckoSession createSession(final GeckoSessionSettings settings,
+ final boolean open) {
+ final GeckoSession session = wrapSession(new GeckoSession(settings));
+ if (open) {
+ openSession(session);
+ }
+ return session;
+ }
+
+ /**
+ * Create a new, opened session using the main session settings.
+ *
+ * @return New session.
+ */
+ public GeckoSession createOpenSession() {
+ return createSession(mMainSession.getSettings(), /* open */ true);
+ }
+
+ /**
+ * Create a new, opened session using the specified settings.
+ *
+ * @param settings Settings for the new session.
+ * @return New session.
+ */
+ public GeckoSession createOpenSession(final GeckoSessionSettings settings) {
+ return createSession(settings, /* open */ true);
+ }
+
+ /**
+ * Create a new, closed session using the specified settings.
+ *
+ * @return New session.
+ */
+ public GeckoSession createClosedSession() {
+ return createSession(mMainSession.getSettings(), /* open */ false);
+ }
+
+ /**
+ * Create a new, closed session using the specified settings.
+ *
+ * @param settings Settings for the new session.
+ * @return New session.
+ */
+ public GeckoSession createClosedSession(final GeckoSessionSettings settings) {
+ return createSession(settings, /* open */ false);
+ }
}