blob: 106436f2b9ce3d122cd6d62a148a9e03f1f2e612 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package androidx.test.espresso.flutter;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.flutter.common.Constants.DEFAULT_INTERACTION_TIMEOUT;
import static androidx.test.espresso.flutter.matcher.FlutterMatchers.isFlutterView;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.hamcrest.Matchers.any;
import android.util.Log;
import android.view.View;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.flutter.action.FlutterViewAction;
import androidx.test.espresso.flutter.action.WidgetInfoFetcher;
import androidx.test.espresso.flutter.api.FlutterAction;
import androidx.test.espresso.flutter.api.WidgetAction;
import androidx.test.espresso.flutter.api.WidgetAssertion;
import androidx.test.espresso.flutter.api.WidgetMatcher;
import androidx.test.espresso.flutter.assertion.FlutterViewAssertion;
import androidx.test.espresso.flutter.common.Duration;
import androidx.test.espresso.flutter.exception.NoMatchingWidgetException;
import androidx.test.espresso.flutter.internal.idgenerator.IdGenerator;
import androidx.test.espresso.flutter.internal.idgenerator.IdGenerators;
import androidx.test.espresso.flutter.model.WidgetInfo;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nonnull;
import okhttp3.OkHttpClient;
import org.hamcrest.Matcher;
/** Entry point to the Espresso testing APIs on Flutter. */
public final class EspressoFlutter {
private static final String TAG = EspressoFlutter.class.getSimpleName();
private static final OkHttpClient okHttpClient;
private static final IdGenerator<Integer> idGenerator;
private static final ExecutorService taskExecutor;
static {
okHttpClient = new OkHttpClient();
idGenerator = IdGenerators.newIntegerIdGenerator();
taskExecutor = Executors.newCachedThreadPool();
}
/**
* Creates a {@link WidgetInteraction} for the Flutter widget matched by the given {@code
* widgetMatcher}, which is an entry point to perform actions or asserts.
*
* @param widgetMatcher the matcher used to uniquely match a Flutter widget on the screen.
*/
public static WidgetInteraction onFlutterWidget(@Nonnull WidgetMatcher widgetMatcher) {
return new WidgetInteraction(isFlutterView(), widgetMatcher);
}
/**
* Provides fluent testing APIs for test authors to perform actions or asserts on Flutter widgets,
* similar to {@code ViewInteraction} and {@code WebInteraction}.
*/
public static final class WidgetInteraction {
/**
* Adds a little delay to the interaction timeout so that we make sure not to time out before
* the action or assert does.
*/
private static final Duration INTERACTION_TIMEOUT_DELAY = new Duration(1, TimeUnit.SECONDS);
private final Matcher<View> flutterViewMatcher;
private final WidgetMatcher widgetMatcher;
private final Duration timeout;
private WidgetInteraction(Matcher<View> flutterViewMatcher, WidgetMatcher widgetMatcher) {
this(
flutterViewMatcher,
widgetMatcher,
DEFAULT_INTERACTION_TIMEOUT.plus(INTERACTION_TIMEOUT_DELAY));
}
private WidgetInteraction(
Matcher<View> flutterViewMatcher, WidgetMatcher widgetMatcher, Duration timeout) {
this.flutterViewMatcher = checkNotNull(flutterViewMatcher);
this.widgetMatcher = checkNotNull(widgetMatcher);
this.timeout = checkNotNull(timeout);
}
/**
* Executes the given action(s) with synchronization guarantees: Espresso ensures Flutter's in
* an idle state before interacting with the Flutter UI.
*
* <p>If more than one action is provided, actions are executed in the order provided.
*
* @param widgetActions one or more actions that shall be performed. Cannot be {@code null}.
* @return this interaction for further perform/verification calls.
*/
public WidgetInteraction perform(@Nonnull final WidgetAction... widgetActions) {
checkNotNull(widgetActions);
for (WidgetAction widgetAction : widgetActions) {
// If any error occurred, an unchecked exception will be thrown that stops execution of
// following actions.
performInternal(widgetAction);
}
return this;
}
/**
* Evaluates the given widget assertion.
*
* @param assertion a widget assertion that shall be made on the matched Flutter widget. Cannot
* be {@code null}.
*/
public WidgetInteraction check(@Nonnull WidgetAssertion assertion) {
checkNotNull(
assertion,
"Assertion cannot be null. You must specify an assertion on the matched Flutter widget.");
WidgetInfo widgetInfo = performInternal(new WidgetInfoFetcher());
if (widgetInfo == null) {
Log.w(TAG, String.format("Widget info that matches %s is null.", widgetMatcher));
throw new NoMatchingWidgetException(
String.format("Widget info that matches %s is null.", widgetMatcher));
}
FlutterViewAssertion flutterViewAssertion = new FlutterViewAssertion(assertion, widgetInfo);
onView(flutterViewMatcher).check(flutterViewAssertion);
return this;
}
private <T> T performInternal(FlutterAction<T> flutterAction) {
checkNotNull(
flutterAction,
"The action cannot be null. You must specify an action to perform on the matched"
+ " Flutter widget.");
FlutterViewAction<T> flutterViewAction =
new FlutterViewAction(
widgetMatcher, flutterAction, okHttpClient, idGenerator, taskExecutor);
onView(flutterViewMatcher).perform(flutterViewAction);
T result;
try {
if (timeout != null && timeout.getQuantity() > 0) {
result = flutterViewAction.waitUntilCompleted(timeout.getQuantity(), timeout.getUnit());
} else {
result = flutterViewAction.waitUntilCompleted();
}
return result;
} catch (ExecutionException e) {
propagateException(e.getCause());
} catch (InterruptedException | TimeoutException | RuntimeException e) {
propagateException(e);
}
return null;
}
/**
* Propagates exception through #onView so that it get a chance to be handled by the registered
* {@code FailureHandler}.
*/
private void propagateException(Throwable t) {
onView(flutterViewMatcher).perform(new ExceptionPropagator(t));
}
/**
* An exception wrapper that propagates an exception through {@code #onView}, so that it can be
* handled by the registered {@code FailureHandler} for the underlying {@code ViewInteraction}.
*/
static class ExceptionPropagator implements ViewAction {
private final RuntimeException exception;
public ExceptionPropagator(RuntimeException exception) {
this.exception = checkNotNull(exception);
}
public ExceptionPropagator(Throwable t) {
this(new RuntimeException(t));
}
@Override
public String getDescription() {
return "Propagate: " + exception;
}
@Override
public void perform(UiController uiController, View view) {
throw exception;
}
@SuppressWarnings("unchecked")
@Override
public Matcher<View> getConstraints() {
return any(View.class);
}
}
}
}