blob: bb62250eefcb7134ea7dd01b4d9896d657fb5a94 [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.action;
import static androidx.test.espresso.flutter.action.ActionUtil.loopUntilCompletion;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.util.concurrent.Futures.allAsList;
import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import android.graphics.Rect;
import android.util.Log;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.action.GeneralClickAction;
import androidx.test.espresso.action.Press;
import androidx.test.espresso.action.Tap;
import androidx.test.espresso.action.TypeTextAction;
import androidx.test.espresso.flutter.api.FlutterTestingProtocol;
import androidx.test.espresso.flutter.api.SyntheticAction;
import androidx.test.espresso.flutter.api.WidgetAction;
import androidx.test.espresso.flutter.api.WidgetMatcher;
import com.google.common.util.concurrent.JdkFutureAdapters;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.gson.annotations.Expose;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/** An action that types text on a Flutter widget. */
public final class FlutterTypeTextAction implements WidgetAction {
private static final String TAG = FlutterTypeTextAction.class.getSimpleName();
private static final String GET_LOCAL_RECT_TASK_NAME = "FlutterTypeTextAction#getLocalRect";
private static final String FLUTTER_IDLE_TASK_NAME = "FlutterTypeTextAction#flutterIsIdle";
private final String stringToBeTyped;
private final boolean tapToFocus;
private final ExecutorService executor;
/**
* Constructs with the given input string. If the string is empty it results in no-op (nothing is
* typed). By default this action sends a tap event to the center of the widget to attain focus
* before typing.
*
* @param stringToBeTyped String To be typed in.
*/
FlutterTypeTextAction(@Nonnull String stringToBeTyped, @Nonnull ExecutorService executor) {
this(stringToBeTyped, executor, true);
}
/**
* Constructs with the given input string. If the string is empty it results in no-op (nothing is
* typed). By default this action sends a tap event to the center of the widget to attain focus
* before typing.
*
* @param stringToBeTyped String To be typed in.
* @param tapToFocus indicates whether a tap should be sent to the underlying widget before
* typing.
*/
FlutterTypeTextAction(
@Nonnull String stringToBeTyped, @Nonnull ExecutorService executor, boolean tapToFocus) {
this.stringToBeTyped = checkNotNull(stringToBeTyped, "The text to type in cannot be null.");
this.executor = checkNotNull(executor);
this.tapToFocus = tapToFocus;
}
@Override
public ListenableFuture<Void> perform(
@Nullable WidgetMatcher targetWidget,
@Nonnull View flutterView,
@Nonnull FlutterTestingProtocol flutterTestingProtocol,
@Nonnull UiController androidUiController) {
// No-op if string is empty.
if (stringToBeTyped.length() == 0) {
Log.w(TAG, "Text string is empty resulting in no-op (nothing is typed).");
return immediateFuture(null);
}
try {
ListenableFuture<Void> setTextEntryEmulationFuture =
JdkFutureAdapters.listenInPoolThread(
flutterTestingProtocol.perform(null, new SetTextEntryEmulationAction(false)));
ListenableFuture<Rect> widgetRectFuture =
JdkFutureAdapters.listenInPoolThread(flutterTestingProtocol.getLocalRect(targetWidget));
// Waits until both Futures return and then proceeds.
Rect widgetRectInDp =
(Rect)
loopUntilCompletion(
GET_LOCAL_RECT_TASK_NAME,
androidUiController,
allAsList(widgetRectFuture, setTextEntryEmulationFuture),
executor)
.get(0);
// Clicks at the center of the Flutter widget (with no visibility check).
//
// Calls the click action separately so we get a chance to ensure Flutter is idle before
// typing text.
WidgetCoordinatesCalculator coordinatesCalculator =
new WidgetCoordinatesCalculator(widgetRectInDp);
if (tapToFocus) {
GeneralClickAction clickAction =
new GeneralClickAction(
Tap.SINGLE,
coordinatesCalculator,
Press.FINGER,
InputDevice.SOURCE_UNKNOWN,
MotionEvent.BUTTON_PRIMARY);
clickAction.perform(androidUiController, flutterView);
loopUntilCompletion(
FLUTTER_IDLE_TASK_NAME,
androidUiController,
flutterTestingProtocol.waitUntilIdle(),
executor);
}
// Then types in text.
ViewAction typeTextAction = new TypeTextAction(stringToBeTyped, false);
typeTextAction.perform(androidUiController, flutterView);
// Espresso will wait for the main thread to finish, so nothing else to wait for in the
// testing thread.
return immediateFuture(null);
} catch (InterruptedException ie) {
return immediateFailedFuture(ie);
} catch (ExecutionException ee) {
return immediateFailedFuture(ee.getCause());
} finally {
androidUiController.loopMainThreadUntilIdle();
}
}
@Override
public String toString() {
return String.format(Locale.ROOT, "type text(%s)", stringToBeTyped);
}
/**
* The {@link SyntheticAction} that configures text entry emulation.
*
* <p>If the text entry emulation is enabled, the operating system's configured keyboard will not
* be invoked when the widget is focused. Explicitly disables the text entry emulation when text
* input is supposed to be sent using the system's keyboard.
*
* <p>By default, the text entry emulation is enabled in the Flutter testing protocol.
*/
private static final class SetTextEntryEmulationAction extends SyntheticAction {
@Expose private final boolean enabled;
/**
* Constructs with the given text entry emulation setting.
*
* @param enabled whether the text entry emulation is enabled. When {@code enabled} is {@code
* true}, the system's configured keyboard will not be invoked when the widget is focused.
*/
public SetTextEntryEmulationAction(boolean enabled) {
super("set_text_entry_emulation");
this.enabled = enabled;
}
/**
* Constructs with the given text entry emulation setting and also a timeout setting for this
* action.
*
* @param enabled whether the text entry emulation is enabled. When {@code enabled} is {@code
* true}, the system's configured keyboard will not be invoked when the widget is focused.
* @param timeOutInMillis the timeout setting of this action.
*/
public SetTextEntryEmulationAction(boolean enabled, long timeOutInMillis) {
super("set_text_entry_emulation", timeOutInMillis);
this.enabled = enabled;
}
}
}