From 41506ff71226238cf26973981df0f778bc084346 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Mon, 8 Jul 2024 17:12:52 -0700 Subject: [PATCH] WIP eventto PiperOrigin-RevId: 650421011 --- .../java/androidx/test/espresso/base/BUILD | 1 + .../test/espresso/base/ViewFinderImpl.java | 2 +- eventto/BUILD | 6 + eventto/java/androidx/test/eventto/BUILD | 27 +++ .../java/androidx/test/eventto/Eventto.java | 21 +++ .../test/eventto/EventtoViewAction.java | 32 ++++ .../test/eventto/EventtoViewInteraction.java | 164 ++++++++++++++++++ eventto/javatests/androidx/test/eventto/BUILD | 35 ++++ .../androidx/test/eventto/EventtoTest.java | 49 ++++++ .../test/eventto/fixtures/AndroidManifest.xml | 26 +++ .../androidx/test/eventto/fixtures/BUILD | 14 ++ .../eventto/fixtures/DelayedActivity.java | 12 ++ .../test/eventto/fixtures/SimpleActivity.java | 36 ++++ .../fixtures/res/layout/delayed_activity.xml | 18 ++ .../fixtures/res/layout/simple_activity.xml | 61 +++++++ .../test/eventto/robolectric.properties | 2 + .../test/ext/truth/view/ViewSubject.java | 45 +++++ 17 files changed, 550 insertions(+), 1 deletion(-) create mode 100644 eventto/BUILD create mode 100644 eventto/java/androidx/test/eventto/BUILD create mode 100644 eventto/java/androidx/test/eventto/Eventto.java create mode 100644 eventto/java/androidx/test/eventto/EventtoViewAction.java create mode 100644 eventto/java/androidx/test/eventto/EventtoViewInteraction.java create mode 100644 eventto/javatests/androidx/test/eventto/BUILD create mode 100644 eventto/javatests/androidx/test/eventto/EventtoTest.java create mode 100644 eventto/javatests/androidx/test/eventto/fixtures/AndroidManifest.xml create mode 100644 eventto/javatests/androidx/test/eventto/fixtures/BUILD create mode 100644 eventto/javatests/androidx/test/eventto/fixtures/DelayedActivity.java create mode 100644 eventto/javatests/androidx/test/eventto/fixtures/SimpleActivity.java create mode 100644 eventto/javatests/androidx/test/eventto/fixtures/res/layout/delayed_activity.xml create mode 100644 eventto/javatests/androidx/test/eventto/fixtures/res/layout/simple_activity.xml create mode 100644 eventto/javatests/androidx/test/eventto/robolectric.properties create mode 100644 ext/truth/java/androidx/test/ext/truth/view/ViewSubject.java diff --git a/espresso/core/java/androidx/test/espresso/base/BUILD b/espresso/core/java/androidx/test/espresso/base/BUILD index 63c32f7cc..61e747a42 100644 --- a/espresso/core/java/androidx/test/espresso/base/BUILD +++ b/espresso/core/java/androidx/test/espresso/base/BUILD @@ -28,6 +28,7 @@ android_library( "ViewHierarchyExceptionHandler.java", ], ), + visibility = ["//visibility:public"], deps = [ ":default_failure_handler", ":idling_resource_registry", diff --git a/espresso/core/java/androidx/test/espresso/base/ViewFinderImpl.java b/espresso/core/java/androidx/test/espresso/base/ViewFinderImpl.java index 96eace034..70fdc3324 100644 --- a/espresso/core/java/androidx/test/espresso/base/ViewFinderImpl.java +++ b/espresso/core/java/androidx/test/espresso/base/ViewFinderImpl.java @@ -45,7 +45,7 @@ public final class ViewFinderImpl implements ViewFinder { private final Provider rootViewProvider; @Inject - ViewFinderImpl(Matcher viewMatcher, Provider rootViewProvider) { + public ViewFinderImpl(Matcher viewMatcher, Provider rootViewProvider) { this.viewMatcher = viewMatcher; this.rootViewProvider = rootViewProvider; } diff --git a/eventto/BUILD b/eventto/BUILD new file mode 100644 index 000000000..5ccaff62b --- /dev/null +++ b/eventto/BUILD @@ -0,0 +1,6 @@ +alias( + name = "eventto", + testonly = 1, + actual = "//eventto/java/androidx/test/eventto", + visibility = ["//visibility:public"], +) diff --git a/eventto/java/androidx/test/eventto/BUILD b/eventto/java/androidx/test/eventto/BUILD new file mode 100644 index 000000000..b48f019e9 --- /dev/null +++ b/eventto/java/androidx/test/eventto/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_android//android:rules.bzl", "android_library") + +android_library( + name = "eventto", + testonly = 1, + srcs = glob(["*.java"]), + visibility = [ + "//eventto:__pkg__", + ], + deps = [ + "//core", + "//espresso/core/java/androidx/test/espresso:espresso_compiletime", + "//espresso/core/java/androidx/test/espresso:framework", + "//espresso/core/java/androidx/test/espresso:interface", + "//espresso/core/java/androidx/test/espresso/base", + "//espresso/core/java/androidx/test/espresso/matcher", + "//espresso/core/java/androidx/test/espresso/util", + "//opensource/androidx:annotation", + "//runner/monitor", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_guava_guava", + "@maven//:javax_inject_javax_inject", + "@maven//:junit_junit", + "@maven//:org_hamcrest_hamcrest_core", + "@maven_listenablefuture//:com_google_guava_listenablefuture", + ], +) diff --git a/eventto/java/androidx/test/eventto/Eventto.java b/eventto/java/androidx/test/eventto/Eventto.java new file mode 100644 index 000000000..15a864fc7 --- /dev/null +++ b/eventto/java/androidx/test/eventto/Eventto.java @@ -0,0 +1,21 @@ +package androidx.test.eventto; + +import android.view.View; +import androidx.annotation.CheckResult; +import java.time.Duration; +import javax.annotation.CheckReturnValue; +import org.hamcrest.Matcher; + +public class Eventto { + private Eventto() {} + + @CheckReturnValue + @CheckResult + public static EventtoViewInteraction onView(Matcher matcher) { + return new EventtoViewInteraction(matcher); + } + + public static void setDefaultTimeout(Duration duration) { + EventtoViewInteraction.setDefaultTimeout(duration); + } +} diff --git a/eventto/java/androidx/test/eventto/EventtoViewAction.java b/eventto/java/androidx/test/eventto/EventtoViewAction.java new file mode 100644 index 000000000..453fb8176 --- /dev/null +++ b/eventto/java/androidx/test/eventto/EventtoViewAction.java @@ -0,0 +1,32 @@ +package androidx.test.eventto; + +import android.view.View; +import org.hamcrest.Matcher; + +public interface EventtoViewAction { + + /** + * A mechanism for ViewActions to specify what type of views they can operate on. + * + *

A ViewAction can demand that the view passed to perform meets certain constraints. For + * example it may want to ensure the view is already in the viewable physical screen of the device + * or is of a certain type. + * + * @return a + * Matcher that will be tested prior to calling perform. + */ + public Matcher getConstraints(); + + /** + * Returns a description of the view action. The description should not be overly long and should + * fit nicely in a sentence like: "performing %description% action on view with id ..." + */ + public String getDescription(); + + /** + * Performs this action on the given view. + * + * @param view the view to act upon. never null. + */ + public void perform(View view); +} diff --git a/eventto/java/androidx/test/eventto/EventtoViewInteraction.java b/eventto/java/androidx/test/eventto/EventtoViewInteraction.java new file mode 100644 index 000000000..eb468978c --- /dev/null +++ b/eventto/java/androidx/test/eventto/EventtoViewInteraction.java @@ -0,0 +1,164 @@ +package androidx.test.eventto; + + +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; +import android.view.View; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.espresso.UiController; +import androidx.test.espresso.ViewAction; +import androidx.test.espresso.ViewAssertion; +import androidx.test.espresso.ViewFinder; +import androidx.test.espresso.base.ViewFinderImpl; +import androidx.test.internal.util.Checks; +import androidx.test.platform.view.inspector.WindowInspectorCompat; +import androidx.test.platform.view.inspector.WindowInspectorCompat.ViewRetrievalException; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.inject.Provider; +import org.hamcrest.Matcher; + +public class EventtoViewInteraction { + // private final Matcher viewMatcher; + private final ViewFinder viewFinder; + private long timeoutMillis; + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + + private static long defaultTimeoutMillis = Duration.ofSeconds(5).toMillis(); + + EventtoViewInteraction(Matcher viewMatcher) { + // this.viewMatcher = viewMatcher; + this.viewFinder = new ViewFinderImpl(viewMatcher, new SimpleRootViewPicker()); + this.timeoutMillis = defaultTimeoutMillis; + } + + static void setDefaultTimeout(Duration duration) { + defaultTimeoutMillis = duration.toMillis(); + } + + public EventtoViewInteraction withTimeout(Duration timeout) { + timeoutMillis = timeout.toMillis(); + return this; + } + + public void check(ViewAssertion assertion) { + ViewAssertionCallable viewAssertionCallable = new ViewAssertionCallable(viewFinder, assertion); + performRetryingTaskOnUiThread(viewAssertionCallable); + } + + private void performRetryingTaskOnUiThread(Callable viewInteractionCallable) { + long startTime = SystemClock.uptimeMillis(); + long remainingTimeout = timeoutMillis; + long pollDelay = 0; + while (remainingTimeout > 0) { + + FutureTask uiTask = new FutureTask<>(viewInteractionCallable); + mainHandler.postDelayed(uiTask, pollDelay); + try { + if (uiTask.get(remainingTimeout, TimeUnit.MILLISECONDS) == ExecutionStatus.SUCCESS) { + return; + } + remainingTimeout = SystemClock.uptimeMillis() - startTime - timeoutMillis; + pollDelay = 100; + } catch (InterruptedException ie) { + throw new RuntimeException("Interrupted running UI task", ie); + } catch (ExecutionException ee) { + Throwable cause = ee.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } + throw new RuntimeException(cause); + } catch (TimeoutException e) { + throw new RuntimeException(e); + } + } + } + + public void perform(ViewAction action) { + ViewActionCallable callable = + new ViewActionCallable(viewFinder, action, new EventtoUiController()); + performRetryingTaskOnUiThread(callable); + } + + private static class SimpleRootViewPicker implements Provider { + + @Override + public View get() { + + try { + List views = WindowInspectorCompat.getGlobalWindowViews(); + // TODO: handle more than 1 view + Checks.checkArgument(views.size() <= 1); + if (views.size() == 1) { + return views.get(0); + } + } catch (ViewRetrievalException e) { + // + } + return null; + } + } + + private enum ExecutionStatus { + SUCCESS, + RESCHEDULE + } + + private static class ViewAssertionCallable implements Callable { + private final ViewFinder viewFinder; + private final ViewAssertion viewAssertion; + + ViewAssertionCallable(ViewFinder viewFinder, ViewAssertion viewAssertion) { + this.viewFinder = viewFinder; + this.viewAssertion = viewAssertion; + } + + @Override + public ExecutionStatus call() throws Exception { + Log.i("Eventto", "Running ViewAssertionCallable"); + try { + View matchedView = viewFinder.getView(); + viewAssertion.check(matchedView, null); + return ExecutionStatus.SUCCESS; + } catch (NoMatchingViewException e) { + Log.i("Eventto", "Could not find view, retrying", e); + return ExecutionStatus.RESCHEDULE; + } + } + } + + private static class ViewActionCallable implements Callable { + private final ViewFinder viewFinder; + private final ViewAction viewAction; + private final UiController uiController; + + ViewActionCallable( + ViewFinder viewFinder, EventoViewAction viewAction, UiController uiController) { + this.viewFinder = viewFinder; + this.viewAction = viewAction; + this.uiController = uiController; + } + + @Override + public ExecutionStatus call() throws Exception { + Log.i("Eventto", "Running ViewAssertionCallable"); + try { + View matchedView = viewFinder.getView(); + viewAction.perform(uiController, matchedView); + return ExecutionStatus.SUCCESS; + } catch (NoMatchingViewException e) { + Log.i("Eventto", "Could not find view, retrying", e); + return ExecutionStatus.RESCHEDULE; + } + } + } +} diff --git a/eventto/javatests/androidx/test/eventto/BUILD b/eventto/javatests/androidx/test/eventto/BUILD new file mode 100644 index 000000000..bc3d4701a --- /dev/null +++ b/eventto/javatests/androidx/test/eventto/BUILD @@ -0,0 +1,35 @@ +load("@build_bazel_rules_android//android:rules.bzl", "android_local_test") +load("//build_extensions:android_library_test.bzl", "axt_android_library_test") +load("//build_extensions:phone_devices.bzl", "devices") + +java_library( + name = "robolectric_config", + resources = ["robolectric.properties"], +) + +android_local_test( + name = "EventtoTest", + srcs = ["EventtoTest.java"], + jvm_flags = ["-Drobolectric.looperMode=INSTRUMENTATION_TEST"], + deps = [ + ":robolectric_config", + "//espresso/core/java/androidx/test/espresso", + "//eventto", + "//eventto/javatests/androidx/test/eventto/fixtures", + "//ext/junit", + "@maven//:junit_junit", + ], +) + +axt_android_library_test( + name = "EventtoTest_android", + srcs = ["EventtoTest.java"], + device_list = devices([34]), + deps = [ + "//espresso/core/java/androidx/test/espresso", + "//eventto", + "//eventto/javatests/androidx/test/eventto/fixtures", + "//ext/junit", + "@maven//:junit_junit", + ], +) diff --git a/eventto/javatests/androidx/test/eventto/EventtoTest.java b/eventto/javatests/androidx/test/eventto/EventtoTest.java new file mode 100644 index 000000000..db1e0b40d --- /dev/null +++ b/eventto/javatests/androidx/test/eventto/EventtoTest.java @@ -0,0 +1,49 @@ +package androidx.test.eventto; + +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +import androidx.test.eventto.fixtures.SimpleActivity; +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class EventtoTest { + + @Rule + public ActivityScenarioRule activityScenarioRule = + new ActivityScenarioRule<>(SimpleActivity.class); + + // @Test + // public void eventto_find() throws Throwable { + // Eventto.onView(withText("Text View")).check(matches(isDisplayed())); + // } + + @Test + public void eventto_click() throws Throwable { + Eventto.onView(withText("Launch delayed")).perform(click()); + } + + // + // @Test + // public void espresso_click() throws Throwable { + // onView(withText("Launch delayed")).perform(click()); + // + // // expect another activity launch + // onView(withText("Delayed Activity")).check(matches(isDisplayed())); + // } + + // @Test + // public void eventto_findFails() throws Throwable { + // onViewWithText("Text View not there").assertThat(isDisplayed()); + // } + + // @Test + // public void espressoFind() { + // onView(withText("Text View")).check(matches(isDisplayed())); + // } + +} diff --git a/eventto/javatests/androidx/test/eventto/fixtures/AndroidManifest.xml b/eventto/javatests/androidx/test/eventto/fixtures/AndroidManifest.xml new file mode 100644 index 000000000..681340bb7 --- /dev/null +++ b/eventto/javatests/androidx/test/eventto/fixtures/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/eventto/javatests/androidx/test/eventto/fixtures/BUILD b/eventto/javatests/androidx/test/eventto/fixtures/BUILD new file mode 100644 index 000000000..767e7d4cb --- /dev/null +++ b/eventto/javatests/androidx/test/eventto/fixtures/BUILD @@ -0,0 +1,14 @@ +load("@build_bazel_rules_android//android:rules.bzl", "android_library") + +android_library( + name = "fixtures", + srcs = glob(["*.java"]), + exports_manifest = 1, + manifest = "AndroidManifest.xml", + resource_files = glob(["res/**"]), + visibility = [ + "//eventto/javatests/androidx/test/eventto:__subpackages__", + ], + deps = [ + ], +) diff --git a/eventto/javatests/androidx/test/eventto/fixtures/DelayedActivity.java b/eventto/javatests/androidx/test/eventto/fixtures/DelayedActivity.java new file mode 100644 index 000000000..48d3c78e9 --- /dev/null +++ b/eventto/javatests/androidx/test/eventto/fixtures/DelayedActivity.java @@ -0,0 +1,12 @@ +package androidx.test.eventto.fixtures; + +import android.app.Activity; +import android.os.Bundle; + +public class DelayedActivity extends Activity { + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.delayed_activity); + } +} diff --git a/eventto/javatests/androidx/test/eventto/fixtures/SimpleActivity.java b/eventto/javatests/androidx/test/eventto/fixtures/SimpleActivity.java new file mode 100644 index 000000000..863f9a8e7 --- /dev/null +++ b/eventto/javatests/androidx/test/eventto/fixtures/SimpleActivity.java @@ -0,0 +1,36 @@ +package androidx.test.eventto.fixtures; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.text.InputType; +import android.widget.Button; +import android.widget.EditText; + +/** Fixture activity for {@link EventtoTest} */ +public class SimpleActivity extends Activity { + + EditText editText; + Button button; + boolean buttonClicked; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.simple_activity); + + editText = findViewById(R.id.edit_text); + // Disable auto-correct for EditText to avoid typed text is changed + // by these features when running tests. + editText.setInputType(editText.getInputType() & (~InputType.TYPE_TEXT_FLAG_AUTO_CORRECT)); + + button = findViewById(R.id.button); + button.setOnClickListener( + view -> { + buttonClicked = true; + Intent delayedActivityIntent = new Intent(this, DelayedActivity.class); + this.startActivity(delayedActivityIntent); + }); + } +} diff --git a/eventto/javatests/androidx/test/eventto/fixtures/res/layout/delayed_activity.xml b/eventto/javatests/androidx/test/eventto/fixtures/res/layout/delayed_activity.xml new file mode 100644 index 000000000..f5e9fd6e6 --- /dev/null +++ b/eventto/javatests/androidx/test/eventto/fixtures/res/layout/delayed_activity.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/eventto/javatests/androidx/test/eventto/fixtures/res/layout/simple_activity.xml b/eventto/javatests/androidx/test/eventto/fixtures/res/layout/simple_activity.xml new file mode 100644 index 000000000..535988263 --- /dev/null +++ b/eventto/javatests/androidx/test/eventto/fixtures/res/layout/simple_activity.xml @@ -0,0 +1,61 @@ + + + + + + +