Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit b5d4503

Browse files
committed
Open source Espresso files
1 parent 010368d commit b5d4503

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+4709
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* Copyright (C) 2019 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package androidx.test.espresso.flutter;
17+
18+
import static androidx.test.espresso.Espresso.onView;
19+
import static androidx.test.espresso.flutter.common.Constants.DEFAULT_INTERACTION_TIMEOUT;
20+
import static androidx.test.espresso.flutter.matcher.FlutterMatchers.isFlutterView;
21+
import static com.google.common.base.Preconditions.checkNotNull;
22+
import static org.hamcrest.Matchers.any;
23+
24+
import android.util.Log;
25+
import android.view.View;
26+
import androidx.test.espresso.UiController;
27+
import androidx.test.espresso.ViewAction;
28+
import androidx.test.espresso.flutter.action.FlutterViewAction;
29+
import androidx.test.espresso.flutter.action.WidgetInfoFetcher;
30+
import androidx.test.espresso.flutter.api.FlutterAction;
31+
import androidx.test.espresso.flutter.api.WidgetAction;
32+
import androidx.test.espresso.flutter.api.WidgetAssertion;
33+
import androidx.test.espresso.flutter.api.WidgetMatcher;
34+
import androidx.test.espresso.flutter.assertion.FlutterViewAssertion;
35+
import androidx.test.espresso.flutter.common.Duration;
36+
import androidx.test.espresso.flutter.exception.NoMatchingWidgetException;
37+
import androidx.test.espresso.flutter.internal.idgenerator.IdGenerator;
38+
import androidx.test.espresso.flutter.internal.idgenerator.IdGenerators;
39+
import androidx.test.espresso.flutter.model.WidgetInfo;
40+
import java.util.concurrent.ExecutionException;
41+
import java.util.concurrent.ExecutorService;
42+
import java.util.concurrent.Executors;
43+
import java.util.concurrent.TimeUnit;
44+
import java.util.concurrent.TimeoutException;
45+
import javax.annotation.Nonnull;
46+
import okhttp3.OkHttpClient;
47+
import org.hamcrest.Matcher;
48+
49+
/** Entry point to the Espresso testing APIs on Flutter. */
50+
public final class EspressoFlutter {
51+
52+
private static final String TAG = EspressoFlutter.class.getSimpleName();
53+
54+
private static final OkHttpClient okHttpClient;
55+
private static final IdGenerator<Integer> idGenerator;
56+
private static final ExecutorService taskExecutor;
57+
58+
static {
59+
okHttpClient = new OkHttpClient();
60+
idGenerator = IdGenerators.newIntegerIdGenerator();
61+
taskExecutor = Executors.newCachedThreadPool();
62+
}
63+
64+
/**
65+
* Creates a {@link WidgetInteraction} for the Flutter widget matched by the given {@code
66+
* widgetMatcher}, which is an entry point to perform actions or asserts.
67+
*
68+
* @param widgetMatcher the matcher used to uniquely match a Flutter widget on the screen.
69+
*/
70+
public static WidgetInteraction onFlutterWidget(@Nonnull WidgetMatcher widgetMatcher) {
71+
return new WidgetInteraction(isFlutterView(), widgetMatcher);
72+
}
73+
74+
/**
75+
* Provides fluent testing APIs for test authors to perform actions or asserts on Flutter widgets,
76+
* similar to {@code ViewInteraction} and {@code WebInteraction}.
77+
*/
78+
public static final class WidgetInteraction {
79+
80+
/**
81+
* Adds a little delay to the interaction timeout so that we make sure not to time out before
82+
* the action or assert does.
83+
*/
84+
private static final Duration INTERACTION_TIMEOUT_DELAY = new Duration(1, TimeUnit.SECONDS);
85+
86+
private final Matcher<View> flutterViewMatcher;
87+
private final WidgetMatcher widgetMatcher;
88+
private final Duration timeout;
89+
90+
private WidgetInteraction(Matcher<View> flutterViewMatcher, WidgetMatcher widgetMatcher) {
91+
this(
92+
flutterViewMatcher,
93+
widgetMatcher,
94+
DEFAULT_INTERACTION_TIMEOUT.plus(INTERACTION_TIMEOUT_DELAY));
95+
}
96+
97+
private WidgetInteraction(
98+
Matcher<View> flutterViewMatcher, WidgetMatcher widgetMatcher, Duration timeout) {
99+
this.flutterViewMatcher = checkNotNull(flutterViewMatcher);
100+
this.widgetMatcher = checkNotNull(widgetMatcher);
101+
this.timeout = checkNotNull(timeout);
102+
}
103+
104+
/**
105+
* Executes the given action(s) with synchronization guarantees: Espresso ensures Flutter's in
106+
* an idle state before interacting with the Flutter UI.
107+
*
108+
* <p>If more than one action is provided, actions are executed in the order provided.
109+
*
110+
* @param widgetActions one or more actions that shall be performed. Cannot be {@code null}.
111+
* @return this interaction for further perform/verification calls.
112+
*/
113+
public WidgetInteraction perform(@Nonnull final WidgetAction... widgetActions) {
114+
checkNotNull(widgetActions);
115+
for (WidgetAction widgetAction : widgetActions) {
116+
// If any error occurred, an unchecked exception will be thrown that stops execution of
117+
// following actions.
118+
performInternal(widgetAction);
119+
}
120+
return this;
121+
}
122+
123+
/**
124+
* Evaluates the given widget assertion.
125+
*
126+
* @param assertion a widget assertion that shall be made on the matched Flutter widget. Cannot
127+
* be {@code null}.
128+
*/
129+
public WidgetInteraction check(@Nonnull WidgetAssertion assertion) {
130+
checkNotNull(
131+
assertion,
132+
"Assertion cannot be null. You must specify an assertion on the matched Flutter widget.");
133+
WidgetInfo widgetInfo = performInternal(new WidgetInfoFetcher());
134+
if (widgetInfo == null) {
135+
Log.w(TAG, String.format("Widget info that matches %s is null.", widgetMatcher));
136+
throw new NoMatchingWidgetException(
137+
String.format("Widget info that matches %s is null.", widgetMatcher));
138+
}
139+
FlutterViewAssertion flutterViewAssertion = new FlutterViewAssertion(assertion, widgetInfo);
140+
onView(flutterViewMatcher).check(flutterViewAssertion);
141+
return this;
142+
}
143+
144+
private <T> T performInternal(FlutterAction<T> flutterAction) {
145+
checkNotNull(
146+
flutterAction,
147+
"The action cannot be null. You must specify an action to perform on the matched"
148+
+ " Flutter widget.");
149+
FlutterViewAction<T> flutterViewAction =
150+
new FlutterViewAction(
151+
widgetMatcher, flutterAction, okHttpClient, idGenerator, taskExecutor);
152+
onView(flutterViewMatcher).perform(flutterViewAction);
153+
T result;
154+
try {
155+
if (timeout != null && timeout.getQuantity() > 0) {
156+
result = flutterViewAction.waitUntilCompleted(timeout.getQuantity(), timeout.getUnit());
157+
} else {
158+
result = flutterViewAction.waitUntilCompleted();
159+
}
160+
return result;
161+
} catch (ExecutionException e) {
162+
propagateException(e.getCause());
163+
} catch (InterruptedException | TimeoutException | RuntimeException e) {
164+
propagateException(e);
165+
}
166+
return null;
167+
}
168+
169+
/**
170+
* Propagates exception through #onView so that it get a chance to be handled by the registered
171+
* {@code FailureHandler}.
172+
*/
173+
private void propagateException(Throwable t) {
174+
onView(flutterViewMatcher).perform(new ExceptionPropagator(t));
175+
}
176+
177+
/**
178+
* An exception wrapper that propagates an exception through {@code #onView}, so that it can be
179+
* handled by the registered {@code FailureHandler} for the underlying {@code ViewInteraction}.
180+
*/
181+
static class ExceptionPropagator implements ViewAction {
182+
private final RuntimeException exception;
183+
184+
public ExceptionPropagator(RuntimeException exception) {
185+
this.exception = checkNotNull(exception);
186+
}
187+
188+
public ExceptionPropagator(Throwable t) {
189+
this(new RuntimeException(t));
190+
}
191+
192+
@Override
193+
public String getDescription() {
194+
return "Propagate: " + exception;
195+
}
196+
197+
@Override
198+
public void perform(UiController uiController, View view) {
199+
throw exception;
200+
}
201+
202+
@SuppressWarnings("unchecked")
203+
@Override
204+
public Matcher<View> getConstraints() {
205+
return any(View.class);
206+
}
207+
}
208+
}
209+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright (C) 2019 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package androidx.test.espresso.flutter.action;
17+
18+
import static com.google.common.base.Preconditions.checkNotNull;
19+
import static com.google.common.base.Preconditions.checkState;
20+
21+
import android.os.Looper;
22+
import androidx.test.espresso.IdlingRegistry;
23+
import androidx.test.espresso.IdlingResource;
24+
import androidx.test.espresso.UiController;
25+
import java.util.concurrent.Callable;
26+
import java.util.concurrent.ExecutionException;
27+
import java.util.concurrent.ExecutorService;
28+
import java.util.concurrent.Future;
29+
import java.util.concurrent.FutureTask;
30+
31+
/** Utils for the Flutter actions. */
32+
final class ActionUtil {
33+
34+
/**
35+
* Loops the main thread until the given future task has been done. Users could use this method to
36+
* "synchronize" between the main thread and {@code Future} instances running on its own thread
37+
* (e.g. methods of the {@code FlutterTestingProtocol}), without blocking the main thread.
38+
*
39+
* <p>Usage:
40+
*
41+
* <pre>{@code
42+
* Future<T> fooFuture = flutterTestingProtocol.callFoo();
43+
* T fooResult = loopUntilCompletion("fooTask", androidUiController, fooFuture, executor);
44+
* // Then consumes the fooResult on main thread.
45+
* }</pre>
46+
*
47+
* @param taskName the name that shall be used when registering the task as an {@link
48+
* IdlingResource}. Espresso ignores {@link IdlingResource} with the same name, so always uses
49+
* a unique name if you don't want Espresso to ignore your task.
50+
* @param androidUiController the controller to use to interact with the Android UI.
51+
* @param futureTask the future task that main thread should wait for a completion signal.
52+
* @param executor the executor to use for running async tasks within the method.
53+
* @param <T> the return value type.
54+
* @return the result of the future task.
55+
* @throws ExecutionException if any error occurs during executing the future task.
56+
* @throws InterruptedException when any internal thread is interrupted.
57+
*/
58+
public static <T> T loopUntilCompletion(
59+
String taskName,
60+
UiController androidUiController,
61+
Future<T> futureTask,
62+
ExecutorService executor)
63+
throws ExecutionException, InterruptedException {
64+
65+
checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!");
66+
67+
FutureIdlingResource<T> idlingResourceFuture = new FutureIdlingResource<>(taskName, futureTask);
68+
IdlingRegistry.getInstance().register(idlingResourceFuture);
69+
try {
70+
// It's fine to ignore this {@code Future} handler, since {@code idlingResourceFuture} should
71+
// give us the result/error any way.
72+
@SuppressWarnings("unused")
73+
Future<?> possiblyIgnoredError = executor.submit(idlingResourceFuture);
74+
androidUiController.loopMainThreadUntilIdle();
75+
checkState(idlingResourceFuture.isDone(), "Future task signaled - but it wasn't done.");
76+
return idlingResourceFuture.get();
77+
} finally {
78+
IdlingRegistry.getInstance().unregister(idlingResourceFuture);
79+
}
80+
}
81+
82+
/**
83+
* An {@code IdlingResource} implementation that takes in a {@code Future}, and sends the idle
84+
* signal to the main thread when the given {@code Future} is done.
85+
*
86+
* @param <T> the return value type of this {@code FutureTask}.
87+
*/
88+
private static class FutureIdlingResource<T> extends FutureTask<T> implements IdlingResource {
89+
90+
private final String taskName;
91+
// Written from main thread, read from any thread.
92+
private volatile ResourceCallback resourceCallback;
93+
94+
public FutureIdlingResource(String taskName, final Future<T> future) {
95+
super(
96+
new Callable<T>() {
97+
@Override
98+
public T call() throws Exception {
99+
return future.get();
100+
}
101+
});
102+
this.taskName = checkNotNull(taskName);
103+
}
104+
105+
@Override
106+
public String getName() {
107+
return taskName;
108+
}
109+
110+
@Override
111+
public void done() {
112+
resourceCallback.onTransitionToIdle();
113+
}
114+
115+
@Override
116+
public boolean isIdleNow() {
117+
return isDone();
118+
}
119+
120+
@Override
121+
public void registerIdleTransitionCallback(ResourceCallback callback) {
122+
this.resourceCallback = callback;
123+
}
124+
}
125+
}

0 commit comments

Comments
 (0)