--- 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
@@ -1113,17 +1113,17 @@ class GeckoSessionTestRuleTest : BaseSes
val newSession = sessionRule.createOpenSession()
newSession.loadTestPath(HELLO_HTML_PATH)
newSession.waitForPageStop()
assertThat("Callback count should be correct", counter, equalTo(1))
}
@Test fun delegateDuringNextWait_hasPrecedenceWithSpecificSession() {
- var newSession = sessionRule.createOpenSession()
+ val newSession = sessionRule.createOpenSession()
var counter = 0
newSession.delegateDuringNextWait(object : Callbacks.ProgressDelegate {
@AssertCalled(count = 1)
override fun onPageStop(session: GeckoSession, success: Boolean) {
counter++
}
})
@@ -1138,17 +1138,17 @@ class GeckoSessionTestRuleTest : BaseSes
newSession.loadTestPath(HELLO_HTML_PATH)
sessionRule.session.loadTestPath(HELLO_HTML_PATH)
sessionRule.waitForPageStops(2)
assertThat("Callback count should be correct", counter, equalTo(1))
}
@Test fun delegateDuringNextWait_specificSessionOverridesAll() {
- var newSession = sessionRule.createOpenSession()
+ val newSession = sessionRule.createOpenSession()
var counter = 0
newSession.delegateDuringNextWait(object : Callbacks.ProgressDelegate {
@AssertCalled(count = 1)
override fun onPageStop(session: GeckoSession, success: Boolean) {
counter++
}
})
@@ -1539,9 +1539,96 @@ class GeckoSessionTestRuleTest : BaseSes
}
@WithDevToolsAPI
@Test fun forceGarbageCollection() {
sessionRule.forceGarbageCollection()
sessionRule.session.reload()
sessionRule.session.waitForPageStop()
}
+
+ private interface TestDelegate {
+ fun onDelegate(foo: String, bar: String): Int
+ }
+
+ @Test fun addExternalDelegateUntilTestEnd() {
+ lateinit var delegate: TestDelegate
+
+ sessionRule.addExternalDelegateUntilTestEnd(
+ TestDelegate::class, { newDelegate -> delegate = newDelegate }, { },
+ object : TestDelegate {
+ @AssertCalled(count = 1)
+ override fun onDelegate(foo: String, bar: String): Int {
+ assertThat("First argument should be correct", foo, equalTo("foo"))
+ assertThat("Second argument should be correct", bar, equalTo("bar"))
+ return 42
+ }
+ })
+
+ assertThat("Delegate should be registered", delegate, notNullValue())
+ assertThat("Delegate return value should be correct",
+ delegate.onDelegate("foo", "bar"), equalTo(42))
+ sessionRule.performTestEndCheck()
+ }
+
+ @Test(expected = AssertionError::class)
+ fun addExternalDelegateUntilTestEnd_throwOnNotCalled() {
+ sessionRule.addExternalDelegateUntilTestEnd(TestDelegate::class, { }, { },
+ object : TestDelegate {
+ @AssertCalled(count = 1)
+ override fun onDelegate(foo: String, bar: String): Int {
+ return 42
+ }
+ })
+ sessionRule.performTestEndCheck()
+ }
+
+ @Test fun addExternalDelegateDuringNextWait() {
+ var delegate: Runnable? = null
+
+ sessionRule.addExternalDelegateDuringNextWait(Runnable::class,
+ { newDelegate -> delegate = newDelegate },
+ { delegate = null }, Runnable { })
+
+ assertThat("Delegate should be registered", delegate, notNullValue())
+ delegate?.run()
+
+ mainSession.reload()
+ mainSession.waitForPageStop()
+ mainSession.forCallbacksDuringWait(Runnable @AssertCalled(count = 1) {})
+
+ assertThat("Delegate should be unregistered after wait", delegate, nullValue())
+ }
+
+ @Test fun addExternalDelegateDuringNextWait_hasPrecedence() {
+ var delegate: TestDelegate? = null
+ val register = { newDelegate: TestDelegate -> delegate = newDelegate }
+ val unregister = { _: TestDelegate -> delegate = null }
+
+ sessionRule.addExternalDelegateDuringNextWait(TestDelegate::class, register, unregister,
+ object : TestDelegate {
+ @AssertCalled(count = 1)
+ override fun onDelegate(foo: String, bar: String): Int {
+ return 24
+ }
+ })
+
+ sessionRule.addExternalDelegateUntilTestEnd(TestDelegate::class, register, unregister,
+ object : TestDelegate {
+ @AssertCalled(count = 1)
+ override fun onDelegate(foo: String, bar: String): Int {
+ return 42
+ }
+ })
+
+ assertThat("Wait delegate should be registered", delegate, notNullValue())
+ assertThat("Wait delegate return value should be correct",
+ delegate?.onDelegate("", ""), equalTo(24))
+
+ mainSession.reload()
+ mainSession.waitForPageStop()
+
+ assertThat("Test delegate should still be registered", delegate, notNullValue())
+ assertThat("Test delegate return value should be correct",
+ delegate?.onDelegate("", ""), equalTo(42))
+ sessionRule.performTestEndCheck()
+ }
}
--- 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
@@ -302,16 +302,23 @@ public class GeckoSessionTestRule extend
/**
* @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;
}
+ /**
+ * Interface that represents a function that registers or unregisters a delegate.
+ */
+ public interface DelegateRegistrar<T> {
+ void invoke(T delegate) throws Throwable;
+ }
+
public static class TimeoutException extends RuntimeException {
public TimeoutException(final String detailMessage) {
super(detailMessage);
}
}
public static class RejectedPromiseException extends RuntimeException {
private final Object mReason;
@@ -555,53 +562,139 @@ public class GeckoSessionTestRule extend
return BuildConfig.DEBUG_BUILD;
}
public String getCPUArch() {
return BuildConfig.ANDROID_CPU_ARCH;
}
}
+ protected final class ExternalDelegate<T> {
+ public final Class<T> delegate;
+ private final DelegateRegistrar<T> mRegister;
+ private final DelegateRegistrar<T> mUnregister;
+ private final T mProxy;
+ private boolean mRegistered;
+
+ public ExternalDelegate(final Class<T> delegate, final T impl,
+ final DelegateRegistrar<T> register,
+ final DelegateRegistrar<T> unregister) {
+ this.delegate = delegate;
+ mRegister = register;
+ mUnregister = unregister;
+
+ @SuppressWarnings("unchecked")
+ final T delegateProxy = (T) Proxy.newProxyInstance(
+ getClass().getClassLoader(), impl.getClass().getInterfaces(),
+ Proxy.getInvocationHandler(mCallbackProxy));
+ mProxy = delegateProxy;
+ }
+
+ @Override
+ public int hashCode() {
+ return delegate.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof ExternalDelegate<?> &&
+ delegate.equals(((ExternalDelegate<?>) obj).delegate);
+ }
+
+ public void register() {
+ try {
+ if (!mRegistered) {
+ mRegister.invoke(mProxy);
+ mRegistered = true;
+ }
+ } catch (final Throwable e) {
+ throw unwrapRuntimeException(e);
+ }
+ }
+
+ public void unregister() {
+ try {
+ if (mRegistered) {
+ mUnregister.invoke(mProxy);
+ mRegistered = false;
+ }
+ } catch (final Throwable e) {
+ throw unwrapRuntimeException(e);
+ }
+ }
+ }
+
protected class CallbackDelegates {
private final Map<Pair<GeckoSession, Method>, MethodCall> mDelegates = new HashMap<>();
+ private final List<ExternalDelegate<?>> mExternalDelegates = new ArrayList<>();
private int mOrder;
private String mOldPrefs;
public void delegate(final @Nullable GeckoSession session,
final @NonNull Object callback) {
- for (final Class<?> ifce : CALLBACK_CLASSES) {
+ for (final Class<?> ifce : DEFAULT_DELEGATES) {
if (!ifce.isInstance(callback)) {
continue;
}
assertThat("Cannot delegate null-delegate callbacks",
ifce, not(isIn(mNullDelegates)));
+ addDelegatesForInterface(session, callback, ifce);
+ }
+ }
- 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 Pair<GeckoSession, Method> pair = new Pair<>(session, method);
- final MethodCall call = new MethodCall(
- session, callbackMethod,
- getAssertCalled(callbackMethod, callback), callback);
- // It's unclear if we should assert the call count if we replace an existing
- // delegate half way through. Until that is resolved, forbid replacing an
- // existing delegate during a test. If you are thinking about changing this
- // behavior, first see if #delegateDuringNextWait fits your needs.
- assertThat("Cannot replace an existing delegate",
- mDelegates, not(hasKey(pair)));
- mDelegates.put(pair, call);
+ private void addDelegatesForInterface(@Nullable final GeckoSession session,
+ @NonNull final Object callback,
+ @NonNull final Class<?> ifce) {
+ 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 Pair<GeckoSession, Method> pair = new Pair<>(session, method);
+ final MethodCall call = new MethodCall(
+ session, callbackMethod,
+ getAssertCalled(callbackMethod, callback), callback);
+ // It's unclear if we should assert the call count if we replace an existing
+ // delegate half way through. Until that is resolved, forbid replacing an
+ // existing delegate during a test. If you are thinking about changing this
+ // behavior, first see if #delegateDuringNextWait fits your needs.
+ assertThat("Cannot replace an existing delegate",
+ mDelegates, not(hasKey(pair)));
+ mDelegates.put(pair, call);
}
}
+ public <T> ExternalDelegate<T> addExternalDelegate(
+ @NonNull final Class<T> delegate,
+ @NonNull final DelegateRegistrar<T> register,
+ @NonNull final DelegateRegistrar<T> unregister,
+ @NonNull final T impl) {
+ assertThat("Delegate must be an interface",
+ delegate.isInterface(), equalTo(true));
+
+ // Delegate each interface to the real thing, then register the delegate using our
+ // proxy. That way all calls to the delegate are recorded just like our internal
+ // delegates.
+ addDelegatesForInterface(/* session */ null, impl, delegate);
+
+ final ExternalDelegate<T> externalDelegate =
+ new ExternalDelegate<>(delegate, impl, register, unregister);
+ mExternalDelegates.add(externalDelegate);
+ mAllDelegates.add(delegate);
+ return externalDelegate;
+ }
+
+ @NonNull
+ public List<ExternalDelegate<?>> getExternalDelegates() {
+ return mExternalDelegates;
+ }
+
/** Generate a JS function to set new prefs and return a set of saved prefs. */
public void setPrefs(final @NonNull Map<String, ?> prefs) {
final String existingPrefs;
if (mOldPrefs == null) {
existingPrefs = "{}";
} else {
existingPrefs = String.format("JSON.parse(%s)", JSONObject.quote(mOldPrefs));
}
@@ -661,16 +754,20 @@ public class GeckoSessionTestRule extend
" prefs.set(name, value);" +
" }" +
" }" +
"})()", JSONObject.quote(mOldPrefs)));
mOldPrefs = null;
}
public void clear() {
+ for (int i = mExternalDelegates.size() - 1; i >= 0; i--) {
+ mExternalDelegates.get(i).unregister();
+ }
+ mExternalDelegates.clear();
mDelegates.clear();
mOrder = 0;
restorePrefs();
}
public void clearAndAssert() {
final Collection<MethodCall> values = mDelegates.values();
@@ -722,29 +819,28 @@ public class GeckoSessionTestRule extend
return;
}
final Class<?>[] superIfces = ifce.getInterfaces();
for (final Class<?> superIfce : superIfces) {
addCallbackClasses(list, superIfce);
}
}
- private static Class<?>[] getCallbackClasses() {
+ private static Set<Class<?>> getDefaultDelegates() {
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()]);
+ return new HashSet<>(list);
}
- private static final List<Class<?>> CALLBACK_CLASSES = Arrays.asList(getCallbackClasses());
+ private static final Set<Class<?>> DEFAULT_DELEGATES = getDefaultDelegates();
private static final class TimeoutRunnable implements Runnable {
private long timeout;
public void set(final long timeout) {
this.timeout = timeout;
cancel();
HANDLER.postDelayed(this, timeout);
@@ -784,16 +880,17 @@ public class GeckoSessionTestRule extend
InstrumentationRegistry.getInstrumentation();
protected final GeckoSessionSettings mDefaultSettings;
protected final Set<GeckoSession> mSubSessions = new HashSet<>();
protected ErrorCollector mErrorCollector;
protected GeckoSession mMainSession;
protected Object mCallbackProxy;
protected Set<Class<?>> mNullDelegates;
+ protected Set<Class<?>> mAllDelegates;
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;
@@ -901,20 +998,39 @@ public class GeckoSessionTestRule extend
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());
}
+ @NonNull
+ private Set<Class<?>> getCurrentDelegates() {
+ final List<ExternalDelegate<?>> waitDelegates = mWaitScopeDelegates.getExternalDelegates();
+ final List<ExternalDelegate<?>> testDelegates = mTestScopeDelegates.getExternalDelegates();
+
+ if (waitDelegates.isEmpty() && testDelegates.isEmpty()) {
+ return DEFAULT_DELEGATES;
+ }
+
+ final Set<Class<?>> set = new HashSet<>(DEFAULT_DELEGATES);
+ for (final ExternalDelegate<?> delegate : waitDelegates) {
+ set.add(delegate.delegate);
+ }
+ for (final ExternalDelegate<?> delegate : testDelegates) {
+ set.add(delegate.delegate);
+ }
+ return set;
+ }
+
private void addNullDelegate(final Class<?> delegate) {
if (!Callbacks.class.equals(delegate.getDeclaringClass())) {
assertThat("Null-delegate must be valid interface class",
- delegate, isIn(CALLBACK_CLASSES));
+ delegate, isIn(DEFAULT_DELEGATES));
mNullDelegates.add(delegate);
return;
}
for (final Class<?> ifce : delegate.getInterfaces()) {
addNullDelegate(ifce);
}
}
@@ -999,36 +1115,54 @@ public class GeckoSessionTestRule extend
final InvocationHandler recorder = new InvocationHandler() {
@Override
public Object invoke(final Object proxy, final Method method,
final Object[] args) {
boolean ignore = false;
MethodCall call = null;
if (Object.class.equals(method.getDeclaringClass())) {
+ switch (method.getName()) {
+ case "equals":
+ return proxy == args[0];
+ case "toString":
+ return "Call Recorder";
+ }
ignore = true;
} else if (mCallRecordHandler != null) {
ignore = mCallRecordHandler.handleCall(method, args);
}
+ final boolean isExternalDelegate =
+ !DEFAULT_DELEGATES.contains(method.getDeclaringClass());
if (!ignore) {
assertThat("Callbacks must be on UI thread",
Looper.myLooper(), equalTo(Looper.getMainLooper()));
- assertThat("Callback first argument must be session object",
- args, arrayWithSize(greaterThan(0)));
- assertThat("Callback first argument must be session object",
- args[0], instanceOf(GeckoSession.class));
- final GeckoSession session = (GeckoSession) args[0];
+ final GeckoSession session;
+ if (isExternalDelegate) {
+ session = null;
+ } else {
+ assertThat("Callback first argument must be session object",
+ args, arrayWithSize(greaterThan(0)));
+ assertThat("Callback first argument must be session object",
+ args[0], instanceOf(GeckoSession.class));
+ session = (GeckoSession) args[0];
+ }
records.add(new CallRecord(session, method, args));
call = waitDelegates.prepareMethodCall(session, method);
if (call == null) {
call = testDelegates.prepareMethodCall(session, method);
}
+
+ if (isExternalDelegate) {
+ assertThat("External delegate should be registered",
+ call, notNullValue());
+ }
}
if (call != null && sOnNewSession.equals(method)) {
// We're delegating an onNewSession call.
// Make sure we wait on the newly opened session, if any.
final GeckoSession oldSession = (GeckoSession) args[0];
@SuppressWarnings("unchecked")
final GeckoResponse<GeckoSession> realResponse =
@@ -1052,19 +1186,21 @@ public class GeckoSessionTestRule extend
} catch (final IllegalAccessException | InvocationTargetException e) {
throw unwrapRuntimeException(e);
} finally {
mCurrentMethodCall = null;
}
}
};
- final Class<?>[] classes = CALLBACK_CLASSES.toArray(new Class<?>[CALLBACK_CLASSES.size()]);
+ final Class<?>[] classes = DEFAULT_DELEGATES.toArray(
+ new Class<?>[DEFAULT_DELEGATES.size()]);
mCallbackProxy = Proxy.newProxyInstance(GeckoSession.class.getClassLoader(),
classes, recorder);
+ mAllDelegates = new HashSet<>(DEFAULT_DELEGATES);
if (sRuntime == null) {
final GeckoRuntimeSettings.Builder runtimeSettingsBuilder =
new GeckoRuntimeSettings.Builder();
runtimeSettingsBuilder.arguments(new String[] { "-purgecaches" })
.extras(InstrumentationRegistry.getArguments())
.nativeCrashReportingEnabled(true)
.javaCrashReportingEnabled(true)
@@ -1105,17 +1241,17 @@ public class GeckoSessionTestRule extend
waitForOpenSession(mMainSession);
}
} else if (!mClosedSession) {
openSession(mMainSession);
}
}
protected void prepareSession(final GeckoSession session) throws Throwable {
- for (final Class<?> cls : CALLBACK_CLASSES) {
+ for (final Class<?> cls : DEFAULT_DELEGATES) {
getCallbackSetter(cls).invoke(
session, mNullDelegates.contains(cls) ? null : mCallbackProxy);
}
}
/**
* 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.
@@ -1157,17 +1293,18 @@ public class GeckoSessionTestRule extend
try {
// We cannot detect initial page load without progress delegate.
assertThat("ProgressDelegate cannot be null-delegate when opening session",
GeckoSession.ProgressDelegate.class, not(isIn(mNullDelegates)));
mCallRecordHandler = new CallRecordHandler() {
@Override
public boolean handleCall(final Method method, final Object[] args) {
- final boolean matching = session.equals(args[0]);
+ final boolean matching = DEFAULT_DELEGATES.contains(
+ method.getDeclaringClass()) && session.equals(args[0]);
if (matching && sOnPageStop.equals(method)) {
mCallRecordHandler = null;
}
return matching;
}
};
do {
@@ -1220,16 +1357,17 @@ public class GeckoSessionTestRule extend
mDisplaySurface.release();
mDisplaySurface = null;
mDisplayTexture.release();
mDisplayTexture = null;
}
mMainSession = null;
mCallbackProxy = null;
+ mAllDelegates = null;
mNullDelegates = null;
mCallRecords = null;
mWaitScopeDelegates = null;
mTestScopeDelegates = null;
mLastWaitStart = 0;
mLastWaitEnd = 0;
mTimeoutMillis = 0;
mRDPTabs = null;
@@ -1408,34 +1546,35 @@ public class GeckoSessionTestRule extend
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<>();
boolean isSessionCallback = false;
- for (final Class<?> ifce : CALLBACK_CLASSES) {
+ for (final Class<?> ifce : getCurrentDelegates()) {
if (!ifce.isAssignableFrom(callback)) {
continue;
}
for (final Method method : ifce.getMethods()) {
for (final Pattern pattern : patterns) {
if (!pattern.matcher(method.getName()).matches()) {
continue;
}
waitMethods.add(new MethodCall(session, method,
/* requirement */ null));
break;
}
}
isSessionCallback = true;
}
- assertThat("Class should be a GeckoSession interface",
+ assertThat("Delegate should be a GeckoSession delegate " +
+ "or registered external delegate",
isSessionCallback, equalTo(true));
waitUntilCalled(session, callback, waitMethods);
}
/**
* Wait until the specified methods have been called on the specified object for any
* session, as specified by any {@link AssertCalled @AssertCalled} annotations. If no
@@ -1460,17 +1599,19 @@ public class GeckoSessionTestRule extend
public void waitUntilCalled(final @Nullable GeckoSession session,
final @NonNull Object callback) {
if (callback instanceof Class<?>) {
waitUntilCalled(session, (Class<?>) callback, (String[]) null);
return;
}
final List<MethodCall> methodCalls = new ArrayList<>();
- for (final Class<?> ifce : CALLBACK_CLASSES) {
+ boolean isSessionCallback = false;
+
+ for (final Class<?> ifce : getCurrentDelegates()) {
if (!ifce.isInstance(callback)) {
continue;
}
for (final Method method : ifce.getMethods()) {
final Method callbackMethod;
try {
callbackMethod = callback.getClass().getMethod(method.getName(),
method.getParameterTypes());
@@ -1478,34 +1619,39 @@ public class GeckoSessionTestRule extend
throw new RuntimeException(e);
}
final AssertCalled ac = getAssertCalled(callbackMethod, callback);
if (ac != null && ac.value()) {
methodCalls.add(new MethodCall(session, method,
ac, /* target */ null));
}
}
+ isSessionCallback = true;
}
+ assertThat("Delegate should implement a GeckoSession delegate " +
+ "or registered external delegate",
+ isSessionCallback, equalTo(true));
+
waitUntilCalled(session, callback.getClass(), methodCalls);
forCallbacksDuringWait(session, callback);
}
protected void waitUntilCalled(final @Nullable GeckoSession session,
final @NonNull Class<?> delegate,
final @NonNull List<MethodCall> methodCalls) {
if (session != null && !session.equals(mMainSession)) {
assertThat("Session should be wrapped through wrapSession",
session, isIn(mSubSessions));
}
// 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) {
+ for (final Class<?> ifce : DEFAULT_DELEGATES) {
final Object callback;
try {
callback = getCallbackGetter(ifce).invoke(session == null ? mMainSession : session);
} catch (final NoSuchMethodException | IllegalAccessException |
InvocationTargetException e) {
throw unwrapRuntimeException(e);
}
if (mNullDelegates.contains(ifce)) {
@@ -1561,16 +1707,22 @@ public class GeckoSessionTestRule extend
protected void beforeWait() {
mLastWaitStart = mLastWaitEnd;
}
protected void afterWait(final int endCallIndex) {
mLastWaitEnd = endCallIndex;
mWaitScopeDelegates.clearAndAssert();
+
+ // Register any test-delegates that were not registered due to wait-delegates
+ // having precedence.
+ for (final ExternalDelegate<?> delegate : mTestScopeDelegates.getExternalDelegates()) {
+ delegate.register();
+ }
}
/**
* Playback callbacks that were made on all sessions 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.
@@ -1595,17 +1747,17 @@ public class GeckoSessionTestRule extend
*/
public void forCallbacksDuringWait(final @Nullable GeckoSession session,
final @NonNull Object callback) {
final Method[] declaredMethods = callback.getClass().getDeclaredMethods();
final List<MethodCall> methodCalls = new ArrayList<>(declaredMethods.length);
boolean assertingAnyCall = true;
Class<?> foundNullDelegate = null;
- for (final Class<?> ifce : CALLBACK_CLASSES) {
+ for (final Class<?> ifce : mAllDelegates) {
if (!ifce.isInstance(callback)) {
continue;
}
if (mNullDelegates.contains(ifce)) {
foundNullDelegate = ifce;
}
for (final Method method : ifce.getMethods()) {
final Method callbackMethod;
@@ -1634,17 +1786,18 @@ public class GeckoSessionTestRule extend
}
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) ||
- (session != null && record.args[0] != session)) {
+ (session != null && DEFAULT_DELEGATES.contains(
+ record.method.getDeclaringClass()) && record.args[0] != session)) {
continue;
}
final int i = methodCalls.indexOf(record.methodCall);
checkThat(record.method.getName() + " should be found",
i, greaterThanOrEqualTo(0));
final MethodCall methodCall = methodCalls.get(i);
@@ -2019,9 +2172,92 @@ public class GeckoSessionTestRule extend
*/
public void forceGarbageCollection() {
assertThat("Must enable RDP using @WithDevToolsAPI",
mWithDevTools, equalTo(true));
ensureChromeProcess();
mRDPChromeProcess.getMemory().forceCycleCollection();
mRDPChromeProcess.getMemory().forceGarbageCollection();
}
+
+ /**
+ * Register an external, non-GeckoSession delegate, and start recording the delegate calls
+ * until the end of the test. The delegate can then be used with methods such as {@link
+ * #waitUntilCalled(Class, String...)} and {@link #forCallbacksDuringWait(Object)}. At the
+ * end of the test, the delegate is automatically unregistered. Delegates added by {@link
+ * #addExternalDelegateDuringNextWait} can temporarily take precedence over delegates added
+ * by {@code delegateUntilTestEnd}.
+ *
+ * @param delegate Delegate instance to register.
+ * @param register DelegateRegistrar instance that represents a function to register the
+ * delegate.
+ * @param unregister DelegateRegistrar instance that represents a function to unregister the
+ * delegate.
+ * @param impl Default delegate implementation. Its methods may be annotated with
+ * {@link AssertCalled} annotations to assert expected behavior.
+ * @see #addExternalDelegateDuringNextWait
+ */
+ public <T> void addExternalDelegateUntilTestEnd(@NonNull final Class<T> delegate,
+ @NonNull final DelegateRegistrar<T> register,
+ @NonNull final DelegateRegistrar<T> unregister,
+ @NonNull final T impl) {
+ final ExternalDelegate<T> externalDelegate =
+ mTestScopeDelegates.addExternalDelegate(delegate, register, unregister, impl);
+
+ // Register if there is not a wait delegate to take precedence over this call.
+ if (!mWaitScopeDelegates.getExternalDelegates().contains(externalDelegate)) {
+ externalDelegate.register();
+ }
+ }
+
+ /** @see #addExternalDelegateUntilTestEnd(Class, DelegateRegistrar,
+ * DelegateRegistrar, Object) */
+ public <T> void addExternalDelegateUntilTestEnd(@NonNull final KClass<T> delegate,
+ @NonNull final DelegateRegistrar<T> register,
+ @NonNull final DelegateRegistrar<T> unregister,
+ @NonNull final T impl) {
+ addExternalDelegateUntilTestEnd(JvmClassMappingKt.getJavaClass(delegate),
+ register, unregister, impl);
+ }
+
+ /**
+ * Register an external, non-GeckoSession delegate, and start recording the delegate calls
+ * during the next wait. The delegate can then be used with methods such as {@link
+ * #waitUntilCalled(Class, String...)} and {@link #forCallbacksDuringWait(Object)}. After the
+ * next wait, the delegate is automatically unregistered. Delegates added by {@code
+ * addExternalDelegateDuringNextWait} can temporarily take precedence over delegates added
+ * by {@link #delegateUntilTestEnd}.
+ *
+ * @param delegate Delegate instance to register.
+ * @param register DelegateRegistrar instance that represents a function to register the
+ * delegate.
+ * @param unregister DelegateRegistrar instance that represents a function to unregister the
+ * delegate.
+ * @param impl Default delegate implementation. Its methods may be annotated with
+ * {@link AssertCalled} annotations to assert expected behavior.
+ * @see #addExternalDelegateDuringNextWait
+ */
+ public <T> void addExternalDelegateDuringNextWait(@NonNull final Class<T> delegate,
+ @NonNull final DelegateRegistrar<T> register,
+ @NonNull final DelegateRegistrar<T> unregister,
+ @NonNull final T impl) {
+ final ExternalDelegate<T> externalDelegate =
+ mWaitScopeDelegates.addExternalDelegate(delegate, register, unregister, impl);
+
+ // Always register because this call always takes precedence, but make sure to unregister
+ // any test-delegates first.
+ final int index = mTestScopeDelegates.getExternalDelegates().indexOf(externalDelegate);
+ if (index >= 0) {
+ mTestScopeDelegates.getExternalDelegates().get(index).unregister();
+ }
+ externalDelegate.register();
+ }
+
+ /** @see #addExternalDelegateDuringNextWait(Class, DelegateRegistrar,
+ * DelegateRegistrar, Object) */
+ public <T> void addExternalDelegateDuringNextWait(@NonNull final KClass<T> delegate,
+ @NonNull final DelegateRegistrar<T> register,
+ @NonNull final DelegateRegistrar<T> unregister,
+ @NonNull final T impl) {
+ addExternalDelegateDuringNextWait(JvmClassMappingKt.getJavaClass(delegate),
+ register, unregister, impl);
+ }
}