mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-21 17:25:36 +00:00
Bug 1439410 - 4. Add JUnit4 rule for testing GeckoSession; r=snorp
Add a rule for setting up a GeckoSession for a JUnit4 test and letting the test wait for listener invocations and to verify listener behavior. MozReview-Commit-ID: 20ij409yY1Z --HG-- extra : rebase_source : 50f8a01aad41938910710421b97d5dcc97594a06
This commit is contained in:
parent
448584018f
commit
e59ef3e43d
@ -0,0 +1,810 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.geckoview.test.rule;
|
||||
|
||||
import org.mozilla.gecko.GeckoSession;
|
||||
import org.mozilla.gecko.GeckoSessionSettings;
|
||||
import org.mozilla.geckoview.test.util.Callbacks;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.MessageQueue;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.rule.UiThreadTestRule;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* for waiting on particular callbacks to be called, and methods for asserting that
|
||||
* callbacks are called in the proper order.
|
||||
*/
|
||||
public class GeckoSessionTestRule extends UiThreadTestRule {
|
||||
|
||||
private static final long DEFAULT_TIMEOUT_MILLIS = 10000;
|
||||
public static final String APK_URI_PREFIX = "resource://android";
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"))
|
||||
* @Test public void test() { ... }
|
||||
* </pre>
|
||||
* <p>
|
||||
* Use multiple settings:
|
||||
* <pre>
|
||||
* @Setting.List({@Setting(key = Setting.Key.USE_MULTIPROCESS,
|
||||
* value = "false"),
|
||||
* @Setting(key = Setting.Key.USE_TRACKING_PROTECTION,
|
||||
* value = "true")})
|
||||
* </pre>
|
||||
*/
|
||||
@Target(ElementType.ANNOTATION_TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Setting {
|
||||
enum Key {
|
||||
CHROME_URI,
|
||||
DISPLAY_MODE,
|
||||
SCREEN_ID,
|
||||
USE_MULTIPROCESS,
|
||||
USE_PRIVATE_MODE,
|
||||
USE_REMOTE_DEBUGGER,
|
||||
USE_TRACKING_PROTECTION;
|
||||
|
||||
private final GeckoSessionSettings.Key<?> mKey;
|
||||
private final Class<?> mType;
|
||||
|
||||
Key() {
|
||||
final Field field;
|
||||
try {
|
||||
field = GeckoSessionSettings.class.getField(name());
|
||||
mKey = (GeckoSessionSettings.Key<?>) field.get(null);
|
||||
} catch (final NoSuchFieldException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
final ParameterizedType genericType = (ParameterizedType) field.getGenericType();
|
||||
mType = (Class<?>) genericType.getActualTypeArguments()[0];
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void set(final GeckoSessionSettings settings, final String value) {
|
||||
if (boolean.class.equals(mType) || Boolean.class.equals(mType)) {
|
||||
settings.setBoolean((GeckoSessionSettings.Key<Boolean>) mKey,
|
||||
Boolean.valueOf(value));
|
||||
} else if (int.class.equals(mType) || Integer.class.equals(mType)) {
|
||||
try {
|
||||
settings.setInt((GeckoSessionSettings.Key<Integer>) mKey,
|
||||
(Integer) GeckoSessionSettings.class.getField(value)
|
||||
.get(null));
|
||||
return;
|
||||
} catch (final NoSuchFieldException | IllegalAccessException |
|
||||
ClassCastException e) {
|
||||
}
|
||||
settings.setInt((GeckoSessionSettings.Key<Integer>) mKey,
|
||||
Integer.valueOf(value));
|
||||
} else if (String.class.equals(mType)) {
|
||||
settings.setString((GeckoSessionSettings.Key<String>) mKey, value);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported type: " +
|
||||
mType.getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface List {
|
||||
Setting[] value();
|
||||
}
|
||||
|
||||
Key key();
|
||||
String value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a method is called or not called, and if called, the order and number
|
||||
* of times it is called. The order number is a monotonically increasing integer; if
|
||||
* an called method's order number is less than the current order number, an exception
|
||||
* is raised for out-of-order call.
|
||||
* <p>
|
||||
* {@code @AssertCalled} asserts the method must be called at least once.
|
||||
* <p>
|
||||
* {@code @AssertCalled(false)} asserts the method must not be called.
|
||||
* <p>
|
||||
* {@code @AssertCalled(order = 2)} asserts the method must be called once and
|
||||
* after any other method with order number less than 2.
|
||||
* <p>
|
||||
* {@code @AssertCalled(order = {2, 4})} asserts order number 2 for first
|
||||
* call and order number 4 for any subsequent calls.
|
||||
* <p>
|
||||
* {@code @AssertCalled(count = 2)} asserts two calls total in any order
|
||||
* with respect to other calls.
|
||||
* <p>
|
||||
* {@code @AssertCalled(count = 2, order = 2)} asserts two calls, both with
|
||||
* order number 2.
|
||||
* <p>
|
||||
* {@code @AssertCalled(count = 2, order = {2, 4, 6})} asserts two calls
|
||||
* total: the first with order number 2 and the second with order number 4.
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface AssertCalled {
|
||||
/**
|
||||
* @return True if the method must be called,
|
||||
* or false if the method must not be called.
|
||||
*/
|
||||
boolean value() default true;
|
||||
|
||||
/**
|
||||
* @return If called, the number of calls called, or 0 to allow any number > 0.
|
||||
*/
|
||||
int count() default 0;
|
||||
|
||||
/**
|
||||
* @return If called, the order number for each call, or 0 to allow arbitrary
|
||||
* order. If order's length is more than count, extra elements are not used;
|
||||
* if order's length is less than count, the last element is repeated.
|
||||
*/
|
||||
int[] order() default 0;
|
||||
}
|
||||
|
||||
public static class CallRequirement {
|
||||
public final boolean allowed;
|
||||
public final int count;
|
||||
public final int[] order;
|
||||
|
||||
public CallRequirement(final boolean allowed, final int count, final int[] order) {
|
||||
this.allowed = allowed;
|
||||
this.count = count;
|
||||
this.order = order;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CallInfo {
|
||||
public final int counter;
|
||||
public final int order;
|
||||
|
||||
/* package */ CallInfo(final int counter, final int order) {
|
||||
this.counter = counter;
|
||||
this.order = order;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MethodCall {
|
||||
public final Method method;
|
||||
public final CallRequirement requirement;
|
||||
private int currentCount;
|
||||
|
||||
/* package */ MethodCall(final Method method) {
|
||||
this(method, (CallRequirement) null);
|
||||
}
|
||||
|
||||
/* package */ MethodCall(final Method method,
|
||||
final AssertCalled requirement) {
|
||||
this(method, requirement != null ? new CallRequirement(
|
||||
requirement.value(), requirement.count(), requirement.order()) : null);
|
||||
}
|
||||
|
||||
public MethodCall(final Method method,
|
||||
final CallRequirement requirement) {
|
||||
this.method = method;
|
||||
this.requirement = requirement;
|
||||
currentCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
} else if (other instanceof MethodCall) {
|
||||
return methodsEqual(method, ((MethodCall) other).method);
|
||||
} else if (other instanceof Method) {
|
||||
return methodsEqual(method, (Method) other);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* package */ int getOrder() {
|
||||
if (requirement == null || currentCount == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
final int[] order = requirement.order;
|
||||
if (order == null || order.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
return order[Math.min(currentCount - 1, order.length - 1)];
|
||||
}
|
||||
|
||||
/* package */ int getCount() {
|
||||
return (requirement == null) ? 0 :
|
||||
!requirement.allowed ? -1 : requirement.count;
|
||||
}
|
||||
|
||||
/* package */ void incrementCounter() {
|
||||
currentCount++;
|
||||
}
|
||||
|
||||
/* package */ boolean allowCalls() {
|
||||
return getCount() >= 0;
|
||||
}
|
||||
|
||||
/* package */ boolean allowUnlimitedCalls() {
|
||||
return getCount() == 0;
|
||||
}
|
||||
|
||||
/* package */ boolean allowMoreCalls() {
|
||||
final int count = getCount();
|
||||
return count == 0 || count > currentCount;
|
||||
}
|
||||
|
||||
/* package */ void assertAllowMoreCalls() {
|
||||
final int count = getCount();
|
||||
assertTrue(method.getName() + " should not be called", allowCalls());
|
||||
assertTrue(method.getName() + " should be limited to " + count +
|
||||
" call" + (count > 1 ? "s" : ""), allowMoreCalls());
|
||||
}
|
||||
|
||||
/* package */ void assertOrder(final int order) {
|
||||
final int newOrder = getOrder();
|
||||
if (newOrder != 0) {
|
||||
assertTrue(method.getName() + " order number " + newOrder +
|
||||
" does not match expected order number " + order,
|
||||
newOrder >= order);
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ void assertMatchesCount() {
|
||||
if (requirement == null) {
|
||||
return;
|
||||
}
|
||||
final int count = getCount();
|
||||
if (count < 0) {
|
||||
assertEquals(method.getName() + " should not be called",
|
||||
0, currentCount);
|
||||
} else if (count == 0) {
|
||||
assertThat(method.getName() + " should be called", currentCount,
|
||||
is(greaterThan(0)));
|
||||
} else {
|
||||
assertEquals(method.getName() + " should be called " + count + " time" +
|
||||
(count == 1 ? "" : "s"), count, currentCount);
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ CallInfo getInfo() {
|
||||
return new CallInfo(currentCount, getOrder());
|
||||
}
|
||||
|
||||
// Similar to Method.equals, but treat the same method from an interface and an
|
||||
// overriding class as the same (e.g. CharSequence.length == String.length).
|
||||
private static boolean methodsEqual(final @NonNull Method m1, final @NonNull Method m2) {
|
||||
return (m1.getDeclaringClass().isAssignableFrom(m2.getDeclaringClass()) ||
|
||||
m2.getDeclaringClass().isAssignableFrom(m1.getDeclaringClass())) &&
|
||||
m1.getName().equals(m2.getName()) &&
|
||||
m1.getReturnType().equals(m2.getReturnType()) &&
|
||||
Arrays.equals(m1.getParameterTypes(), m2.getParameterTypes());
|
||||
}
|
||||
}
|
||||
|
||||
protected static class CallRecord {
|
||||
public final Method method;
|
||||
public final MethodCall methodCall;
|
||||
public final Object[] args;
|
||||
|
||||
public CallRecord(final Method method, final Object[] args) {
|
||||
this.method = method;
|
||||
this.methodCall = new MethodCall(method);
|
||||
this.args = args;
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ static AssertCalled getAssertCalled(final Method method, final Object callback) {
|
||||
final AssertCalled annotation = method.getAnnotation(AssertCalled.class);
|
||||
if (annotation != null) {
|
||||
return annotation;
|
||||
}
|
||||
|
||||
// Some Kotlin lambdas have an invoke method that carries the annotation,
|
||||
// instead of the interface method carrying the annotation.
|
||||
try {
|
||||
return callback.getClass().getDeclaredMethod(
|
||||
"invoke", method.getParameterTypes()).getAnnotation(AssertCalled.class);
|
||||
} catch (final NoSuchMethodException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addCallbackClasses(final List<Class<?>> list, final Class<?> ifce) {
|
||||
if (!Callbacks.class.equals(ifce.getDeclaringClass())) {
|
||||
list.add(ifce);
|
||||
return;
|
||||
}
|
||||
final Class<?>[] superIfces = ifce.getInterfaces();
|
||||
for (final Class<?> superIfce : superIfces) {
|
||||
addCallbackClasses(list, superIfce);
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<?>[] getCallbackClasses() {
|
||||
final Class<?>[] ifces = Callbacks.class.getDeclaredClasses();
|
||||
final List<Class<?>> list = new ArrayList<>(ifces.length);
|
||||
|
||||
for (final Class<?> ifce : ifces) {
|
||||
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());
|
||||
|
||||
protected final Instrumentation mInstrumentation =
|
||||
InstrumentationRegistry.getInstrumentation();
|
||||
protected final GeckoSessionSettings mDefaultSettings;
|
||||
|
||||
protected GeckoSession mSession;
|
||||
protected Object mCallbackProxy;
|
||||
protected List<CallRecord> mCallRecords;
|
||||
protected int mLastWaitStart;
|
||||
protected int mLastWaitEnd;
|
||||
protected MethodCall mCurrentMethodCall;
|
||||
protected long mTimeoutMillis = DEFAULT_TIMEOUT_MILLIS;
|
||||
|
||||
public GeckoSessionTestRule() {
|
||||
mDefaultSettings = new GeckoSessionSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session set up for the current test.
|
||||
*
|
||||
* @return GeckoSession object.
|
||||
*/
|
||||
public @NonNull GeckoSession getSession() {
|
||||
return mSession;
|
||||
}
|
||||
|
||||
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)
|
||||
throws NoSuchMethodException {
|
||||
return GeckoSession.class.getMethod("get" + cls.getSimpleName());
|
||||
}
|
||||
|
||||
protected void applyAnnotations(final Collection<Annotation> annotations,
|
||||
final GeckoSessionSettings settings) {
|
||||
for (final Annotation annotation : annotations) {
|
||||
if (TimeoutMillis.class.equals(annotation.annotationType())) {
|
||||
mTimeoutMillis = Math.max(((TimeoutMillis) annotation).value(), 100);
|
||||
} else if (Setting.List.class.equals(annotation.annotationType())) {
|
||||
for (final Setting setting : ((Setting.List) annotation).value()) {
|
||||
setting.key().set(settings, setting.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void prepareSession(final Description description) throws Throwable {
|
||||
final GeckoSessionSettings settings = new GeckoSessionSettings(mDefaultSettings);
|
||||
|
||||
applyAnnotations(Arrays.asList(description.getTestClass().getAnnotations()), settings);
|
||||
applyAnnotations(description.getAnnotations(), settings);
|
||||
|
||||
final List<CallRecord> records = new ArrayList<>();
|
||||
mCallRecords = records;
|
||||
mLastWaitStart = 0;
|
||||
mLastWaitEnd = 0;
|
||||
|
||||
final InvocationHandler recorder = new InvocationHandler() {
|
||||
@Override
|
||||
public Object invoke(final Object proxy, final Method method,
|
||||
final Object[] args) {
|
||||
assertEquals("Callbacks must be on UI thread",
|
||||
Looper.getMainLooper(), Looper.myLooper());
|
||||
|
||||
records.add(new CallRecord(method, args));
|
||||
|
||||
try {
|
||||
return method.invoke(Callbacks.Default.INSTANCE, args);
|
||||
} catch (final IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
final Class<?>[] classes = CALLBACK_CLASSES.toArray(new Class<?>[CALLBACK_CLASSES.size()]);
|
||||
mCallbackProxy = Proxy.newProxyInstance(GeckoSession.class.getClassLoader(),
|
||||
classes, recorder);
|
||||
|
||||
mSession = new GeckoSession(settings);
|
||||
|
||||
for (final Class<?> cls : CALLBACK_CLASSES) {
|
||||
if (cls != null) {
|
||||
getCallbackSetter(cls).invoke(mSession, mCallbackProxy);
|
||||
}
|
||||
}
|
||||
|
||||
mSession.openWindow(mInstrumentation.getTargetContext());
|
||||
|
||||
if (settings.getBoolean(GeckoSessionSettings.USE_MULTIPROCESS)) {
|
||||
// Under e10s, we receive an initial about:blank load; don't expose that to the test.
|
||||
waitForPageStop();
|
||||
}
|
||||
}
|
||||
|
||||
protected void cleanupSession() throws Throwable {
|
||||
if (mSession.isOpen()) {
|
||||
mSession.closeWindow();
|
||||
}
|
||||
mSession = null;
|
||||
mCallbackProxy = null;
|
||||
mCallRecords = null;
|
||||
mLastWaitStart = 0;
|
||||
mLastWaitEnd = 0;
|
||||
mTimeoutMillis = DEFAULT_TIMEOUT_MILLIS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement apply(final Statement base, final Description description) {
|
||||
return super.apply(new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
try {
|
||||
prepareSession(description);
|
||||
base.evaluate();
|
||||
} finally {
|
||||
cleanupSession();
|
||||
}
|
||||
}
|
||||
}, description);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldRunOnUiThread(final Description description) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop the current thread until the message queue is idle. If loop is already idle and
|
||||
* timeout is not specified, return immediately. If loop is already idle and timeout is
|
||||
* specified, wait for a message to arrive first; an exception is thrown if timeout
|
||||
* expires during the wait.
|
||||
*
|
||||
* @param timeout Wait timeout in milliseconds or 0 to not wait.
|
||||
*/
|
||||
protected static void loopUntilIdle(final long timeout) {
|
||||
// Adapted from GeckoThread.pumpMessageLoop.
|
||||
final Looper looper = Looper.myLooper();
|
||||
assertNotNull("Looper must exist", looper);
|
||||
|
||||
final MessageQueue queue = looper.getQueue();
|
||||
final Handler handler = new Handler(looper);
|
||||
final MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
|
||||
@Override
|
||||
public boolean queueIdle() {
|
||||
final Message msg = Message.obtain(handler);
|
||||
msg.obj = handler;
|
||||
handler.sendMessageAtFrontOfQueue(msg);
|
||||
return false; // Remove this idle handler.
|
||||
}
|
||||
};
|
||||
final Runnable timeoutRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
fail("Timed out after " + timeout + "ms");
|
||||
}
|
||||
};
|
||||
|
||||
if (timeout > 0) {
|
||||
handler.postDelayed(timeoutRunnable, timeout);
|
||||
} else {
|
||||
queue.addIdleHandler(idleHandler);
|
||||
}
|
||||
|
||||
final Method getNextMessage;
|
||||
try {
|
||||
getNextMessage = queue.getClass().getDeclaredMethod("next");
|
||||
} catch (final NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
getNextMessage.setAccessible(true);
|
||||
|
||||
while (true) {
|
||||
final Message msg;
|
||||
try {
|
||||
msg = (Message) getNextMessage.invoke(queue);
|
||||
} catch (final IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
|
||||
}
|
||||
if (msg.getTarget() == handler && msg.obj == handler) {
|
||||
// Our idle signal.
|
||||
break;
|
||||
} else if (msg.getTarget() == null) {
|
||||
looper.quit();
|
||||
break;
|
||||
}
|
||||
msg.getTarget().dispatchMessage(msg);
|
||||
|
||||
if (timeout > 0) {
|
||||
handler.removeCallbacks(timeoutRunnable);
|
||||
queue.addIdleHandler(idleHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public void waitForPageStop() {
|
||||
waitForPageStops(/* count */ 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.ProgressListener.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,
|
||||
new CallRequirement(/* allowed */ true, count, null)));
|
||||
|
||||
waitUntilCalled(GeckoSession.ProgressListener.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.
|
||||
*
|
||||
* @param callback Target callback interface; must be an interface under GeckoSession.
|
||||
* @param methods List of methods to wait on; use empty or null or wait on any method.
|
||||
*/
|
||||
public void waitUntilCalled(final @NonNull Class<?> callback,
|
||||
final @Nullable String... methods) {
|
||||
assertTrue("Class should be a GeckoSession interface",
|
||||
CALLBACK_CLASSES.contains(callback));
|
||||
|
||||
final int length = (methods != null) ? methods.length : 0;
|
||||
final Pattern[] patterns = new Pattern[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
patterns[i] = Pattern.compile(methods[i]);
|
||||
}
|
||||
|
||||
final List<MethodCall> waitMethods = new ArrayList<>();
|
||||
|
||||
for (final Method method : callback.getDeclaredMethods()) {
|
||||
for (final Pattern pattern : patterns) {
|
||||
if (pattern.matcher(method.getName()).matches()) {
|
||||
waitMethods.add(new MethodCall(method));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waitUntilCalled(callback, waitMethods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until the specified methods have been called on the specified object,
|
||||
* as specified by any {@link AssertCalled @AssertCalled} annotations. If no
|
||||
* {@link AssertCalled @AssertCalled} annotations are found, wait until any method
|
||||
* has been called. Only methods belonging to a GeckoSession callback are supported.
|
||||
*
|
||||
* @param callback Target callback object; must implement an interface under GeckoSession.
|
||||
*/
|
||||
public void waitUntilCalled(final @NonNull Object callback) {
|
||||
if (callback instanceof Class<?>) {
|
||||
waitUntilCalled((Class<?>) callback, (String[]) null);
|
||||
return;
|
||||
}
|
||||
|
||||
final List<MethodCall> methodCalls = new ArrayList<>();
|
||||
for (final Class<?> ifce : CALLBACK_CLASSES) {
|
||||
if (!ifce.isInstance(callback)) {
|
||||
continue;
|
||||
}
|
||||
for (final Method method : ifce.getMethods()) {
|
||||
final Method callbackMethod;
|
||||
try {
|
||||
callbackMethod = callback.getClass().getMethod(method.getName(),
|
||||
method.getParameterTypes());
|
||||
} catch (final NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
final AssertCalled ac = getAssertCalled(callbackMethod, callback);
|
||||
if (ac != null && ac.value()) {
|
||||
methodCalls.add(new MethodCall(callbackMethod, ac));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waitUntilCalled(callback.getClass(), methodCalls);
|
||||
forCallbacksDuringWait(callback);
|
||||
}
|
||||
|
||||
protected void waitUntilCalled(final @NonNull Class<?> listener,
|
||||
final @NonNull List<MethodCall> methodCalls) {
|
||||
// Make sure all handlers are set though #delegateUntilTestEnd or #delegateDuringNextWait,
|
||||
// instead of through GeckoSession directly, so that we can still record calls even with
|
||||
// custom handlers set.
|
||||
for (final Class<?> ifce : CALLBACK_CLASSES) {
|
||||
try {
|
||||
assertSame("Callbacks should be set through" +
|
||||
" GeckoSessionTestRule delegate methods",
|
||||
mCallbackProxy, getCallbackGetter(ifce).invoke(mSession));
|
||||
} catch (final NoSuchMethodException | IllegalAccessException |
|
||||
InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
|
||||
}
|
||||
}
|
||||
|
||||
boolean calledAny = false;
|
||||
int index = mLastWaitStart = mLastWaitEnd;
|
||||
|
||||
while (!calledAny || !methodCalls.isEmpty()) {
|
||||
while (index >= mCallRecords.size()) {
|
||||
loopUntilIdle(mTimeoutMillis);
|
||||
}
|
||||
|
||||
final MethodCall recorded = mCallRecords.get(index).methodCall;
|
||||
calledAny |= recorded.method.getDeclaringClass().isAssignableFrom(listener);
|
||||
index++;
|
||||
|
||||
final int i = methodCalls.indexOf(recorded);
|
||||
if (i < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final MethodCall methodCall = methodCalls.get(i);
|
||||
methodCall.incrementCounter();
|
||||
if (methodCall.allowUnlimitedCalls() || !methodCall.allowMoreCalls()) {
|
||||
methodCalls.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
mLastWaitEnd = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playback callbacks that were made during the previous wait. For any methods
|
||||
* annotated with {@link AssertCalled @AssertCalled}, assert that the callbacks
|
||||
* satisfy the specified requirements. If no {@link AssertCalled @AssertCalled}
|
||||
* annotations are found, assert any method has been called. Only methods belonging
|
||||
* to a GeckoSession callback are supported.
|
||||
*
|
||||
* @param callback Target callback object; must implement one or more interfaces
|
||||
* under GeckoSession.
|
||||
*/
|
||||
public void forCallbacksDuringWait(final @NonNull Object callback) {
|
||||
final Method[] declaredMethods = callback.getClass().getDeclaredMethods();
|
||||
final List<MethodCall> methodCalls = new ArrayList<>(declaredMethods.length);
|
||||
for (final Class<?> ifce : CALLBACK_CLASSES) {
|
||||
if (!ifce.isInstance(callback)) {
|
||||
continue;
|
||||
}
|
||||
for (final Method method : ifce.getMethods()) {
|
||||
final Method callbackMethod;
|
||||
try {
|
||||
callbackMethod = callback.getClass().getMethod(method.getName(),
|
||||
method.getParameterTypes());
|
||||
} catch (final NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
methodCalls.add(new MethodCall(
|
||||
callbackMethod, getAssertCalled(callbackMethod, callback)));
|
||||
}
|
||||
}
|
||||
|
||||
int order = 0;
|
||||
boolean calledAny = false;
|
||||
|
||||
for (int index = mLastWaitStart; index < mLastWaitEnd; index++) {
|
||||
final CallRecord record = mCallRecords.get(index);
|
||||
if (!record.method.getDeclaringClass().isInstance(callback)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final int i = methodCalls.indexOf(record.methodCall);
|
||||
assertTrue(record.method.getName() + " should be found", i >= 0);
|
||||
|
||||
final MethodCall methodCall = methodCalls.get(i);
|
||||
methodCall.assertAllowMoreCalls();
|
||||
methodCall.incrementCounter();
|
||||
methodCall.assertOrder(order);
|
||||
order = Math.max(methodCall.getOrder(), order);
|
||||
|
||||
try {
|
||||
mCurrentMethodCall = methodCall;
|
||||
record.method.invoke(callback, record.args);
|
||||
} catch (final IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
|
||||
} finally {
|
||||
mCurrentMethodCall = null;
|
||||
}
|
||||
calledAny = true;
|
||||
}
|
||||
|
||||
for (final MethodCall methodCall : methodCalls) {
|
||||
methodCall.assertMatchesCount();
|
||||
if (methodCall.requirement != null) {
|
||||
calledAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue("Should have called one of " +
|
||||
Arrays.toString(callback.getClass().getInterfaces()), calledAny);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the current call. Only valid during a {@link #forCallbacksDuringWait}
|
||||
* callback.
|
||||
*
|
||||
* @return Call information
|
||||
*/
|
||||
public @NonNull CallInfo getCurrentCall() {
|
||||
assertNotNull("Should be in a method call", mCurrentMethodCall);
|
||||
return mCurrentMethodCall.getInfo();
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.geckoview.test.util
|
||||
|
||||
import org.mozilla.gecko.GeckoSession
|
||||
import org.mozilla.gecko.util.GeckoBundle
|
||||
|
||||
class Callbacks private constructor() {
|
||||
object Default : All {
|
||||
}
|
||||
|
||||
interface All : ContentListener, NavigationListener, PermissionDelegate, ProgressListener,
|
||||
PromptDelegate, ScrollListener, TrackingProtectionDelegate {
|
||||
}
|
||||
|
||||
interface ContentListener : GeckoSession.ContentListener {
|
||||
override fun onTitleChange(session: GeckoSession, title: String) {
|
||||
}
|
||||
|
||||
override fun onFocusRequest(session: GeckoSession) {
|
||||
}
|
||||
|
||||
override fun onCloseRequest(session: GeckoSession) {
|
||||
}
|
||||
|
||||
override fun onFullScreen(session: GeckoSession, fullScreen: Boolean) {
|
||||
}
|
||||
|
||||
override fun onContextMenu(session: GeckoSession, screenX: Int, screenY: Int, uri: String, elementSrc: String) {
|
||||
}
|
||||
}
|
||||
|
||||
interface NavigationListener : GeckoSession.NavigationListener {
|
||||
override fun onLocationChange(session: GeckoSession, url: String) {
|
||||
}
|
||||
|
||||
override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
|
||||
}
|
||||
|
||||
override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
|
||||
}
|
||||
|
||||
override fun onLoadUri(session: GeckoSession, uri: String, where: GeckoSession.NavigationListener.TargetWindow): Boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
override fun onNewSession(session: GeckoSession, uri: String, response: GeckoSession.Response<GeckoSession>) {
|
||||
response.respond(null)
|
||||
}
|
||||
}
|
||||
|
||||
interface PermissionDelegate : GeckoSession.PermissionDelegate {
|
||||
override fun requestAndroidPermissions(session: GeckoSession, permissions: Array<out String>, callback: GeckoSession.PermissionDelegate.Callback) {
|
||||
callback.reject()
|
||||
}
|
||||
|
||||
override fun requestContentPermission(session: GeckoSession, uri: String, type: String, access: String, callback: GeckoSession.PermissionDelegate.Callback) {
|
||||
callback.reject()
|
||||
}
|
||||
|
||||
override fun requestMediaPermission(session: GeckoSession, uri: String, video: Array<out GeckoSession.PermissionDelegate.MediaSource>, audio: Array<out GeckoSession.PermissionDelegate.MediaSource>, callback: GeckoSession.PermissionDelegate.MediaCallback) {
|
||||
callback.reject()
|
||||
}
|
||||
}
|
||||
|
||||
interface ProgressListener : GeckoSession.ProgressListener {
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
}
|
||||
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
}
|
||||
|
||||
override fun onSecurityChange(session: GeckoSession, securityInfo: GeckoSession.ProgressListener.SecurityInformation) {
|
||||
}
|
||||
}
|
||||
|
||||
interface PromptDelegate : GeckoSession.PromptDelegate {
|
||||
override fun alert(session: GeckoSession, title: String, msg: String, callback: GeckoSession.PromptDelegate.AlertCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForButton(session: GeckoSession, title: String, msg: String, btnMsg: Array<out String>, callback: GeckoSession.PromptDelegate.ButtonCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForText(session: GeckoSession, title: String, msg: String, value: String, callback: GeckoSession.PromptDelegate.TextCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForAuth(session: GeckoSession, title: String, msg: String, options: GeckoSession.PromptDelegate.AuthenticationOptions, callback: GeckoSession.PromptDelegate.AuthCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForChoice(session: GeckoSession, title: String, msg: String, type: Int, choices: Array<out GeckoSession.PromptDelegate.Choice>, callback: GeckoSession.PromptDelegate.ChoiceCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForColor(session: GeckoSession, title: String, value: String, callback: GeckoSession.PromptDelegate.TextCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForDateTime(session: GeckoSession, title: String, type: Int, value: String, min: String, max: String, callback: GeckoSession.PromptDelegate.TextCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForFile(session: GeckoSession, title: String, type: Int, mimeTypes: Array<out String>, callback: GeckoSession.PromptDelegate.FileCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
interface ScrollListener : GeckoSession.ScrollListener {
|
||||
override fun onScrollChanged(session: GeckoSession, scrollX: Int, scrollY: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
interface TrackingProtectionDelegate : GeckoSession.TrackingProtectionDelegate {
|
||||
override fun onTrackerBlocked(session: GeckoSession, uri: String, categories: Int) {
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user