AtomAction.java
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.test.espresso.web.action;
import static androidx.test.espresso.matcher.ViewMatchers.isJavascriptEnabled;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.util.concurrent.Futures.transform;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.webkit.WebView;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.remote.Bindable;
import androidx.test.espresso.web.model.Atom;
import androidx.test.espresso.web.model.ElementReference;
import androidx.test.espresso.web.model.Evaluation;
import androidx.test.espresso.web.model.WindowReference;
import com.google.common.base.Function;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
import org.hamcrest.Matcher;
/**
* A ViewAction which causes the provided Atom to be evaluated within a webview.
*
* <p>It is not recommended to use AtomAction directly.
*
* <p>Instead {@see androidx.test.espresso.web.sugar.Web} for examples of how to interact
* with a WebView's content through Atoms.
*
* <p>If you must use AtomAction directly, take care to remember that they are Stateful (unlike most
* ViewActions) and the caller must call {@link #get()} to ensure that the action has completed.
*
* @param <E> The type the specific Atom returns.
*/
public final class AtomAction<E> implements ViewAction, Bindable {
private static final String TAG = "AtomAction";
private static final String ID = TAG;
private static final String EVALUATION_ERROR_KEY = "evaluation_error_key";
private final SettableFuture<Evaluation> futureEval = SettableFuture.create();
final Atom<E> atom;
@Nullable final WindowReference window;
@Nullable final ElementReference element;
private IAtomActionResultPropagator atomActionResultPropagator =
new IAtomActionResultPropagator.Stub() {
@Override
public void setResult(Evaluation evaluation) throws RemoteException {
futureEval.set(evaluation);
}
@Override
public void setError(Bundle bundle) throws RemoteException {
Throwable evalError = (Throwable) bundle.getSerializable(EVALUATION_ERROR_KEY);
futureEval.setException(evalError);
}
};
/**
* Creates an AtomAction.
*
* @param atom the atom to execute
* @param window (optional/nullable) the window context to execute on.
* @param element (optional/nullable) the element to execute on.
*/
public AtomAction(
Atom<E> atom, @Nullable WindowReference window, @Nullable ElementReference element) {
this.atom = checkNotNull(atom);
this.window = window;
this.element = element;
}
@Override
public Matcher<View> getConstraints() {
return isJavascriptEnabled();
}
@Override
public String getDescription() {
return String.format("Evaluate Atom: %s in window: %s with element: %s", atom, window, element);
}
@Override
public void perform(UiController controller, View view) {
WebView webView = (WebView) view;
if (Build.VERSION.SDK_INT >= 23 && !webView.isHardwareAccelerated()) {
throw new PerformException.Builder()
.withViewDescription(webView.toString())
.withCause(
new RuntimeException("Hardware acceleration is not supported on current device"))
.build();
}
List<Object> arguments = checkNotNull(atom.getArguments(element));
String script = checkNotNull(atom.getScript());
final ListenableFuture<Evaluation> localEval =
JavascriptEvaluation.evaluate(webView, script, arguments, window);
if (null != window && Build.VERSION.SDK_INT == 19) {
Log.w(
TAG,
"WARNING: KitKat does not report when an iframe is loading new content. "
+ "If you are interacting with content within an iframe and that content is changing "
+ "(eg: you have just pressed a submit button). Espresso will not be able to block "
+ "you until the new content has loaded (which it can do on all other API levels). "
+ "You will need to have some custom polling / synchronization with the iframe in "
+ "that case.");
}
localEval.addListener(
new Runnable() {
@Override
public void run() {
try {
atomActionResultPropagator.setResult(localEval.get());
} catch (ExecutionException ee) {
reportException(ee.getCause());
} catch (InterruptedException ie) {
reportException(ie);
} catch (RemoteException re) {
reportException(re);
}
}
},
MoreExecutors.directExecutor());
}
private void reportException(Throwable throwable) {
Bundle errorBundle = new Bundle();
errorBundle.putSerializable(EVALUATION_ERROR_KEY, throwable);
try {
atomActionResultPropagator.setError(errorBundle);
} catch (RemoteException re) {
Log.e(TAG, "Cannot report error to result propagator", re);
}
}
/**
* Return a Future, which will be set and transformed from futureEval. Espresso's public API
* cannot have guava types in its method signatures, so return Future instead of ListenableFuture
* or SettableFuture.
*/
public Future<E> getFuture() {
return transform(
futureEval,
new Function<Evaluation, E>() {
@Override
public E apply(Evaluation e) {
return atom.transform(e);
}
},
directExecutor());
}
/** Blocks until the atom has completed execution. */
public E get() throws ExecutionException, InterruptedException {
checkState(Looper.myLooper() != Looper.getMainLooper(), "On main thread!");
return getFuture().get();
}
/** Blocks until the atom has completed execution with a configurable timeout. */
public E get(long val, TimeUnit unit)
throws ExecutionException, InterruptedException, TimeoutException {
checkState(Looper.myLooper() != Looper.getMainLooper(), "On main thread!");
return getFuture().get(val, unit);
}
@Override
public String getId() {
return ID;
}
@Override
public IBinder getIBinder() {
return atomActionResultPropagator.asBinder();
}
@Override
public void setIBinder(IBinder binder) {
atomActionResultPropagator = IAtomActionResultPropagator.Stub.asInterface(binder);
}
}