From 05b7130e3f9d96e94ace99d5473d0181b431e223 Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Sun, 13 Nov 2022 22:31:10 +0100 Subject: [PATCH] Cleanup --- .../main/java/com/moka/EspressoInternals.java | 166 ---- moka/src/main/java/com/moka/EspressoMoka.java | 875 ------------------ .../java/com/moka/EspressoMokaRunner.java | 316 ------- .../main/java/com/moka/MokaDataInteraction.kt | 77 -- .../src/main/java/com/moka/MokaInteraction.kt | 107 --- .../main/java/com/moka/MokaViewInteraction.kt | 76 -- .../lib/actions/AppBarLayoutCollapseAction.kt | 57 -- .../actions/NestedScrollViewScrollToAction.kt | 88 -- .../java/com/moka/lib/debug/DebugTrace.kt | 21 - .../com/moka/lib/internals/AnimatorsMoka.java | 98 -- .../java/com/moka/lib/internals/DeviceMoka.kt | 24 - .../com/moka/lib/internals/ExceptionMoka.kt | 32 - .../com/moka/lib/internals/Reflection.java | 130 --- .../TestAccessibilityEventListener.java | 71 -- .../com/moka/lib/matchers/ViewMatchers.kt | 38 - .../com/moka/utils/NoAnimationActivityRule.kt | 23 - .../com/moka/waiter/android/BusyWaiter.kt | 279 ------ .../android/BusyWaiterIdlingResource.java | 41 - .../waiter/android/internal/SetMultiMap.java | 200 ---- .../pager/BusyWaiterPageChangeListener.java | 23 - .../android/testsupport/BusyWaiterExecutor.kt | 40 - .../webview/BusyWaiterWebViewClient.java | 169 ---- moka/src/test/java/com/moka/BusyWaiterTest.kt | 123 --- moka/src/test/java/com/moka/MultiMapTest.kt | 246 ----- sample/build.gradle | 3 +- .../java/com/sample/app/RecyclerViewTest.kt | 2 +- .../sample/app/SampleAndroidJUnitRunner.kt | 40 - 27 files changed, 2 insertions(+), 3363 deletions(-) delete mode 100644 moka/src/main/java/com/moka/EspressoInternals.java delete mode 100644 moka/src/main/java/com/moka/EspressoMoka.java delete mode 100644 moka/src/main/java/com/moka/EspressoMokaRunner.java delete mode 100644 moka/src/main/java/com/moka/MokaDataInteraction.kt delete mode 100644 moka/src/main/java/com/moka/MokaInteraction.kt delete mode 100644 moka/src/main/java/com/moka/MokaViewInteraction.kt delete mode 100644 moka/src/main/java/com/moka/lib/actions/AppBarLayoutCollapseAction.kt delete mode 100644 moka/src/main/java/com/moka/lib/actions/NestedScrollViewScrollToAction.kt delete mode 100644 moka/src/main/java/com/moka/lib/debug/DebugTrace.kt delete mode 100644 moka/src/main/java/com/moka/lib/internals/AnimatorsMoka.java delete mode 100644 moka/src/main/java/com/moka/lib/internals/DeviceMoka.kt delete mode 100644 moka/src/main/java/com/moka/lib/internals/ExceptionMoka.kt delete mode 100644 moka/src/main/java/com/moka/lib/internals/Reflection.java delete mode 100644 moka/src/main/java/com/moka/lib/internals/TestAccessibilityEventListener.java delete mode 100644 moka/src/main/java/com/moka/utils/NoAnimationActivityRule.kt delete mode 100644 moka/src/main/java/com/moka/waiter/android/BusyWaiter.kt delete mode 100644 moka/src/main/java/com/moka/waiter/android/BusyWaiterIdlingResource.java delete mode 100644 moka/src/main/java/com/moka/waiter/android/internal/SetMultiMap.java delete mode 100644 moka/src/main/java/com/moka/waiter/android/pager/BusyWaiterPageChangeListener.java delete mode 100644 moka/src/main/java/com/moka/waiter/android/testsupport/BusyWaiterExecutor.kt delete mode 100644 moka/src/main/java/com/moka/waiter/android/webview/BusyWaiterWebViewClient.java delete mode 100644 moka/src/test/java/com/moka/BusyWaiterTest.kt delete mode 100644 moka/src/test/java/com/moka/MultiMapTest.kt delete mode 100644 sample/src/androidTest/java/com/sample/app/SampleAndroidJUnitRunner.kt diff --git a/moka/src/main/java/com/moka/EspressoInternals.java b/moka/src/main/java/com/moka/EspressoInternals.java deleted file mode 100644 index 29c83c1..0000000 --- a/moka/src/main/java/com/moka/EspressoInternals.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.moka; - -import android.app.Activity; -import android.os.Looper; - -import androidx.annotation.UiThread; -import androidx.test.espresso.BaseLayerComponent; -import androidx.test.espresso.GraphHolder; -import androidx.test.espresso.UiController; -import androidx.test.espresso.base.ActiveRootLister; -import androidx.test.runner.lifecycle.ActivityLifecycleMonitor; -import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; -import androidx.test.runner.lifecycle.Stage; - -import java.util.Collection; -import java.util.concurrent.atomic.AtomicBoolean; - -import timber.log.Timber; - -import static com.moka.EspressoMokaRunner.failIfInstrumentationCaughtAnyErrors; -import static com.moka.EspressoMokaRunner.loopMainThreadMillis; -import static com.moka.EspressoMokaRunner.runOnMainSync; -import static com.moka.lib.internals.ExceptionSugar.propagate; -import static com.moka.lib.internals.Reflection.getStaticFieldValue; -import static com.moka.lib.internals.Reflection.invokeStatic; -import static java.lang.System.currentTimeMillis; - -public final class EspressoInternals { - - // These only need to be init'd once. - private static final BaseLayerComponent BASE_LAYER_COMPONENT = espressoBaseLayerComponent(); - - private EspressoInternals() { - } - - @SuppressWarnings("ThrowableNotThrown") - public static void waitForEspressoToIdle() { - failIfInstrumentationCaughtAnyErrors(); - runOnMainSync(new Runnable() { - @Override - public void run() { - Boolean alreadyLooping = alreadyLooping(); - - if (!alreadyLooping) { // avoid recursive looping - try { - uiController().loopMainThreadUntilIdle(); - } catch (RuntimeException e) { - if (e instanceof IllegalStateException - || e.getCause() instanceof IllegalStateException) { - Timber.w(e, "Swallowing an IllegalStateException from recursively looping espresso."); - } else { - Timber.w(e.getCause(), - "Propagating cause %s from loopMainThreadUntilIdle in waitForEspressoToIdle", - e.getMessage()); - propagate(e); - } - } - } - } - }); - } - - @UiThread - public static Boolean alreadyLooping() { - if (Looper.getMainLooper() == Looper.myLooper()) { - // must be on MainThread, so that we get the correct ThreadLocal value - ThreadLocal alreadyLoopingThreadLocal = getStaticFieldValue("androidx.test.espresso.base.Interrogator", - "interrogating"); - return alreadyLoopingThreadLocal.get(); - } else { - throw new IllegalThreadStateException("Should only call alreadyLooping() on the ui thread."); - } - } - - public static boolean waitForAnyOfOurActivitiesToBeInResumedState() { - return waitForAnyOfOurActivitiesToBeInResumedState(10000); - } - - public static boolean waitForAllOfOurActivitiesToBeDestroyed(final int maxWaitTime) { - boolean notIn = false; - for (Stage stage : new Stage[]{ - Stage.PRE_ON_CREATE, - Stage.CREATED, - Stage.STARTED, - Stage.RESUMED, - Stage.RESTARTED, - Stage.PAUSED, - Stage.STOPPED - }) { - if (!waitForAllActivitiesToBe(notIn, stage, maxWaitTime)) { - return false; - } - } - return true; - } - - public static boolean waitForAnyOfOurActivitiesToBeInResumedState(final int maxWaitTime) { - boolean in = true; - return waitForAllActivitiesToBe(in, Stage.RESUMED, maxWaitTime); - } - - public static boolean waitUntilNoneOfOurActivitiesAreResumed() { - boolean notIn = false; - return waitForAllActivitiesToBe(notIn, Stage.RESUMED); - } - - public static boolean waitUntilNoneOfOurActivitiesAreResumedOrPaused() { - boolean notIn = false; - waitForAllActivitiesToBe(notIn, Stage.RESUMED); - return waitForAllActivitiesToBe(notIn, Stage.PAUSED); - } - - private static boolean waitForAllActivitiesToBe(final boolean shouldActivitiesBeResumed, final Stage stage) { - return waitForAllActivitiesToBe(shouldActivitiesBeResumed, stage, 10000); - } - - private static boolean waitForAllActivitiesToBe(final boolean shouldActivitiesBeResumed, - final Stage stage, - final int maxWaitTime) { - long startTime = currentTimeMillis(); - boolean timedOut = currentTimeMillis() - startTime > maxWaitTime; - boolean someActivitiesAreResumed = !noActivitiesAreInStage(stage); - Timber.d("some Activities are in Stage %s? %s", stage, someActivitiesAreResumed); - Timber.d("timedOut? %s", timedOut); - while (someActivitiesAreResumed != shouldActivitiesBeResumed && !timedOut) { - failIfInstrumentationCaughtAnyErrors(); - loopMainThreadMillis(300); - timedOut = currentTimeMillis() - startTime > maxWaitTime; - someActivitiesAreResumed = !noActivitiesAreInStage(stage); - Timber.d("timedOut? %s", timedOut); - } - return someActivitiesAreResumed == shouldActivitiesBeResumed; - } - - public static boolean noActivitiesAreInStage(final Stage stage) { - final AtomicBoolean noActivityWasInStage = new AtomicBoolean(false); - runOnMainSync(new Runnable() { - @Override - public void run() { - final Collection activitiesInStage = activityLifecycleMonitor().getActivitiesInStage(stage); - boolean empty = activitiesInStage.isEmpty(); - noActivityWasInStage.set(empty); - if (!empty) { - Timber.d("Found %s activities: %s", stage, activitiesInStage); - } - } - }); - return noActivityWasInStage.get(); - } - - public static UiController uiController() { - return BASE_LAYER_COMPONENT.uiController(); - } - - public static ActiveRootLister activeRootLister() { - return BASE_LAYER_COMPONENT.activeRootLister(); - } - - public static ActivityLifecycleMonitor activityLifecycleMonitor() { - return ActivityLifecycleMonitorRegistry.getInstance(); - } - - private static BaseLayerComponent espressoBaseLayerComponent() { - return invokeStatic(GraphHolder.class, "baseLayer"); - } -} diff --git a/moka/src/main/java/com/moka/EspressoMoka.java b/moka/src/main/java/com/moka/EspressoMoka.java deleted file mode 100644 index e296a55..0000000 --- a/moka/src/main/java/com/moka/EspressoMoka.java +++ /dev/null @@ -1,875 +0,0 @@ -package com.moka; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.ActivityManager; -import android.app.ActivityManager.RunningTaskInfo; -import android.app.Instrumentation.ActivityResult; -import android.app.UiAutomation; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.widget.TextView; - -import androidx.annotation.DrawableRes; -import androidx.annotation.IdRes; -import androidx.annotation.IntDef; -import androidx.annotation.StringRes; -import androidx.appcompat.widget.Toolbar; -import androidx.test.espresso.Espresso; -import androidx.test.espresso.NoActivityResumedException; -import androidx.test.espresso.NoMatchingViewException; -import androidx.test.espresso.ViewAssertion; -import androidx.test.espresso.core.internal.deps.guava.base.Stopwatch; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.intent.IntentStubber; -import androidx.test.runner.intent.IntentStubberRegistry; - -import org.hamcrest.BaseMatcher; -import org.hamcrest.CoreMatchers; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.StringDescription; -import org.hamcrest.core.AllOf; - -import java.lang.annotation.Retention; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import timber.log.Timber; - -import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_BACK; -import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_HOME; -import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS; -import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS; -import static android.app.Activity.RESULT_OK; -import static androidx.test.InstrumentationRegistry.getTargetContext; -import static androidx.test.espresso.Espresso.onData; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; -import static androidx.test.espresso.matcher.ViewMatchers.hasFocus; -import static androidx.test.espresso.matcher.ViewMatchers.isClickable; -import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast; -import static androidx.test.espresso.matcher.ViewMatchers.isRoot; -import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; -import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static com.moka.EspressoInternals.waitForEspressoToIdle; -import static com.moka.EspressoInternals.waitUntilNoneOfOurActivitiesAreResumedOrPaused; -import static com.moka.EspressoMokaRunner.getAccessibilityEventListener; -import static com.moka.EspressoMokaRunner.getBusyWaiter; -import static com.moka.EspressoMokaRunner.getMainThreadHandler; -import static com.moka.EspressoMokaRunner.loopMainThreadMillis; -import static com.moka.EspressoMokaRunner.runOnMainSync; -import static com.moka.lib.actions.ViewActions.clickSpannableWithText; -import static com.moka.lib.actions.ViewActions.clickWithRetry; -import static com.moka.lib.actions.ViewActions.requestFocus; -import static com.moka.lib.actions.ViewActions.viewActionWithRetry; -import static com.moka.lib.assertions.ViewAssertions.exists; -import static com.moka.lib.assertions.ViewAssertions.hasChildWithSubstring; -import static com.moka.lib.internals.ExceptionSugar.propagate; -import static com.moka.lib.internals.Reflection.getFieldValue; -import static com.moka.lib.matchers.ViewMatchers.canBeScrolledSoIt; -import static com.moka.lib.matchers.ViewMatchers.contains; -import static com.moka.lib.matchers.ViewMatchers.containsText; -import static com.moka.lib.matchers.ViewMatchers.hasData; -import static com.moka.lib.matchers.ViewMatchers.isGone; -import static com.moka.lib.matchers.ViewMatchers.withClickableText; -import static com.moka.lib.matchers.ViewMatchers.withDrawable; -import static java.lang.String.format; -import static java.lang.annotation.RetentionPolicy.SOURCE; -import static java.util.Locale.US; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.core.AllOf.allOf; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -/** - * Entry point to the Espresso Moka. - */ -public final class EspressoMoka { - public static final ActivityResult ACTIVITY_RESULT_OK = new ActivityResult(RESULT_OK, null); - - private static final int MAX_WAIT = 30000; - private static final String ATTEMPTING_TO_RESUME_ACTIVITY = "ATTEMPTING TO RESUME ACTIVITY"; - private static final int AREA_PERCENTAGE = 94; - private static final ScheduledExecutorService ESPRESSO_SUGAR_THREAD = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { - @Override - public Thread newThread(@Nonnull final Runnable r) { - return new Thread(r, "ESPRESSO_SUGAR_THREAD"); - } - }); - private static boolean sOnStartCalled = false; - private static AtomicBoolean sCurrentlyTryingToResume = new AtomicBoolean(false); - private static AtomicReference sStopwatch = new AtomicReference<>(Stopwatch.createUnstarted()); - - private EspressoMoka() { - sStopwatch.get().start(); - } - - static void onStart() { - sOnStartCalled = true; - } - - private static void assertOnStartWasCalled() { - // You have to start onStart() ! otherwise it will fail - assertTrue(sOnStartCalled); - } - - /** - * Causes the Espresso thread to suspend execution. - * Make sure to only use when debugging. - */ - public static void pause() { - try { - Thread.sleep(Integer.MAX_VALUE); - } catch (InterruptedException e) { - //noinspection ThrowableNotThrown - propagate(e); - } - } - - /** - * Performs a click action on the given {@link MokaViewInteraction}. - */ - public static void click(@Nonnull final MokaViewInteraction viewInteraction) { - viewInteraction.click(); - } - - /** - * Checks that a {@link View} exists in the {@link View} hierarchy based on its resource id. - */ - public static void checkThatViewWithIdExists(final @IdRes int viewId) { - loopMainThreadUntilViewExists(withId(viewId)); - } - - /** - * Performs an action that clicks on a {@link View} based on its text value. - */ - public static void clickOnViewWithText(@Nonnull final String string) { - onViewWithText(is(string), canBeScrolledSoIt(isCompletelyDisplayed())).perform(clickWithRetry()); - } - - /** - * Performs an action that clicks on a {@link View} based on its resource id. - */ - public static void clickOnViewWithId(final @IdRes int id) { - onViewWithId(id, canBeScrolledSoIt(isDisplayingAtLeast(AREA_PERCENTAGE))).perform(requestFocus(), clickWithRetry()); - } - - /** - * Performs an action that clicks on a {@link View} based on its resource id and on its text value. - */ - public static void clickOnViewWithIdAndText(final @IdRes int id, final String string) { - onViewWithIdAndText(id, string).perform(clickWithRetry()); - } - - /** - * Performs an action that clicks on a clickable substring inside of a {@link TextView}. - */ - public static void clickOnViewWithClickableText(@Nonnull final String text) { - getSugarViewInteraction(withClickableText(text)).perform(clickSpannableWithText(text)); - } - - /** - * Performs an action that clicks the back button. - */ - public static void pressBack() { - waitForEspressoToIdle(); - onView(isRoot()).perform(viewActionWithRetry(androidx.test.espresso.action.ViewActions.pressBack())); - waitForEspressoToIdle(); - waitForAccessibilityStreamToIdle(); - } - - /** - * Performs an action that clicks the Up button. - */ - public static void clickOnApplicationUpIcon() { - onView(withContentDescription("Navigate up")).perform(clickWithRetry()); - } - - /** - * Checks that a {@link View} is gone based on its resource id. - */ - public static void checkThatViewWithIdIsGone(final @IdRes int viewId) { - loopMainThreadUntilViewExists(allOf(withId(viewId), isGone())); - } - - /** - * Checks that a {@link View} is not displayed based on its resource id. - */ - public static void checkThatViewWithIdIsNotDisplayed(final @IdRes int viewId) { - loopMainThreadUntilViewDoesNotExists(allOf(withId(viewId), isDisplayed())); - } - - /** - * Checks that a {@link View} is not being displayed and is not part of the {@link View} hierarchy. - */ - public static void checkThatViewDoesNotExist(@Nonnull final Matcher viewMatcher) { - loopMainThreadUntilViewDoesNotExists(viewMatcher, MAX_WAIT); - } - - /** - * Checks that a {@link View} with the given resource id is displaying the string. - */ - public static void checkThatViewWithIdHasStringDisplayed(@IdRes int id, final String substring) { - onViewWithIdAndText(id, substring).checkMatches(isDisplayed()); - } - - /** - * Checks that a {@link View} with the given resource id is clickable. - */ - public static void checkThatViewWithIdWithStringIsClickable(@IdRes int id, final String substring) { - onViewWithIdAndText(id, substring).checkMatches(isClickable()); - } - - /** - * Checks that the {@link Toolbar} title is exactly the given text. - */ - public static void checkToolBarTitleIs(@Nonnull final String title) { - onView(allOf(instanceOf(Toolbar.class), hasDescendant(withText(title)))).check(matches(isDisplayed())); - } - - /** - * Checks that the {@link Toolbar} does not have a TextView, meaning a null or empty title was set. - */ - public static void checkToolBarTitleIsEmpty() { - onView(allOf(instanceOf(Toolbar.class), not(hasDescendant(CoreMatchers.instanceOf(TextView.class))))).check(matches(isDisplayed())); - } - - /** - * Checks that the {@link Toolbar} title contains the given text. - */ - public static void checkToolBarContains(@Nonnull final String text) { - onView(allOf(instanceOf(Toolbar.class), hasDescendant(containsText(text)))).check(matches(isDisplayed())); - } - - /** - * Checks that the {@link Toolbar} Up button is not displayed. - */ - public static void checkThatToolbarUpButtonIsNotVisible() { - checkThatViewWithContentDescriptionIsNotDisplayedAndItDoesNotExists("Navigate up"); - } - - public static void checkThatToolbarUpButtonIsVisible() { - onView(withContentDescription("Navigate up")).check(matches(isDisplayed())); - } - - /** - * Waits for the accessibility event stream to become idle. - */ - @SuppressLint("OverlyBroadExceptionCaught") - public static void waitForAccessibilityStreamToIdle() { - final UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); - try { - boolean timedOut = false; - boolean waitedSuccessfully = false; - while (!(timedOut) && !(waitedSuccessfully)) { - try { - uiAutomation.waitForIdle(20, 100); - waitedSuccessfully = true; - } catch (TimeoutException e) { - Timber.w(e, "Timed out while waiting for Accessibility Stream to be idle."); - timedOut = true; - } - } - } catch (Exception e) { - Timber.w(e, "Ignoring Exception while waitForAccessibilityStreamToIdle"); - } - } - - /** - * Performs an action to go to the home screen and then attempts to resume the previous {@link Activity}. - */ - public static void goToHomeScreenThenAttemptToResumeActivity() { - goToHomeScreenThenAttemptToResumeActivity(6000, null); - } - - /** - * Checks that a text is not being displayed and at the same time is not part of the {@link View} hierarchy. - */ - public static void checkThatStringIsNotDisplayedAndItDoesNotExists(@Nonnull final String substring) { - checkThatStringIsNotDisplayedAndItDoesNotExists(substring, MAX_WAIT); - } - - /** - * Checks that a text is not being displayed is not part of the {@link TextView} hierarchy. - * It waits the given amount of time before failing. - * - * @param maxWaitInMilliseconds in milliseconds - */ - public static void checkThatStringIsNotDisplayedAndItDoesNotExists(@Nonnull final String substring, final int maxWaitInMilliseconds) { - loopMainThreadUntilViewDoesNotExists(allOf(withText(contains(substring)), isDisplayed()), maxWaitInMilliseconds); - } - - /** - * Checks that a {@link View} with the given resource id is displaying the drawable resource. - */ - public static void checkThatImageWithIdWithDrawableIdIsDisplayed(@IdRes int id, @DrawableRes int drawableId) { - onViewWithId(id).checkMatches(withDrawable(drawableId)); - } - - /** - * Checks that a {@link View} with the given text is visible (Visibility = VISIBLE), but not necessarily displayed. - */ - public static void checkThatStringIsVisible(@Nonnull final String substring) { - onViewWithText(contains(substring), withEffectiveVisibility(androidx.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE)); - } - - /** - * Checks that a {@link View} containing the substring is displayed. - */ - public static void checkThatStringIsDisplayed(@Nonnull final String substring) { - checkThatStringIsDisplayed(substring, MAX_WAIT); - } - - /** - * Checks that a {@link View} containing the substring is displayed. - * It waits the given amount of time before failing. - */ - public static void checkThatStringIsDisplayed(@Nonnull final String substring, final int maxWaitInMilliseconds) { - onViewWithText(contains(substring), canBeScrolledSoIt(isDisplayed()), maxWaitInMilliseconds).check(exists()); - } - - /** - * Checks that a {@link View} matched on its resource id, contains the a child displaying the given substring. - */ - public static void checkThatViewWithIdHasASubStringWithText(@IdRes final int parentId, @Nonnull final String substring) { - onViewWithId(parentId).check(hasChildWithSubstring(substring)).checkMatches(isDisplayed()); - } - - /** - * Checks that a {@link View} containing the substring is displayed and that is satisfies the secondary matcher. - */ - public static void checkThatStringIsDisplayed(@Nonnull final String substring, @Nonnull final Matcher matcher) { - onViewWithText(contains(substring), allOf(isDisplayed(), matcher)).check(exists()); - } - - /** - * Checks that a {@link View} with the text is not being displayed, but it is part of the {@link View} hierarchy - */ - public static void checkThatStringIsNotDisplayed(@Nonnull final String substring) { - onViewWithText(contains(substring)).checkMatches(not(isDisplayed())); - } - - /** - * Checks that a {@link View} with the given resource id does not exists in the View. - */ - public static void checkThatViewWithIdDoesNotExist(@IdRes final int viewId) { - loopMainThreadUntilViewDoesNotExists(withId(viewId)); - } - - @SuppressWarnings("unchecked") - public static void clickOnAdapterItemAtPosition(@Nonnull final Class type, @IdRes final int adapterViewId, final int position) { - onAdapterItem(type, (Matcher) instanceOf(type)) - .inAdapterView(withId(adapterViewId)) - .atPosition(position) - .perform(clickWithRetry()); - } - - /** - * Creates a {@link MokaViewInteraction} for a {@link View} based on its resource id. - */ - @Nonnull - public static MokaViewInteraction onViewWithId(@IdRes final int id) { - return getSugarViewInteraction(withId(id)); - } - - /** - * Creates a {@link MokaViewInteraction} for a {@link View} based on its resource id and its content description. - */ - @Nonnull - public static MokaViewInteraction onViewWithIdAndContentDescription(@IdRes final int id, @Nonnull final String desc) { - return getSugarViewInteraction(allOf(withId(id), withContentDescription(desc))); - } - - /** - * Creates a {@link MokaViewInteraction} for a {@link View} based on its resource id. - */ - @Nonnull - public static MokaViewInteraction onFocusedViewWithId(@IdRes final int id) { - return getSugarViewInteraction(allOf(withId(id), hasFocus())); - } - - /** - * Creates a {@link MokaViewInteraction} for a {@link View} based on its resource id and a matcher. - */ - @Nonnull - public static MokaViewInteraction onViewWithId(@IdRes final int id, @Nonnull final Matcher viewMatcher) { - return getSugarViewInteraction(allOf(withId(id), viewMatcher)); - } - - /** - * Creates a {@link MokaViewInteraction} for a {@link View} based on its resource id and on its text value. - */ - @Nonnull - public static MokaViewInteraction onViewWithIdAndText(@IdRes final int id, String text) { - return getSugarViewInteraction(allOf(withId(id), withText(text))); - } - - /** - * Creates a {@link MokaViewInteraction} for a {@link View} based on its resource id, its text value and a matcher. - */ - @Nonnull - public static MokaViewInteraction onViewWithText(@Nonnull final Matcher matcher, @Nonnull final Matcher viewMatcher) { - return onViewWithText(matcher, viewMatcher, MAX_WAIT); - } - - /** - * Creates a {@link MokaViewInteraction} for a {@link View} based on its resource id, its text value and a matcher. - * It waits the given amount of time before failing. - * - * @param maxWaitInMilliseconds in milliseconds - */ - @Nonnull - public static MokaViewInteraction onViewWithText(@Nonnull final Matcher matcher, - @Nonnull final Matcher viewMatcher, - final int maxWaitInMilliseconds) { - return getSugarViewInteraction(allOf(withText(matcher), viewMatcher), maxWaitInMilliseconds); - } - - /** - * Creates a {@link MokaViewInteraction} for a {@link View} based on its text value. - */ - @Nonnull - public static MokaViewInteraction onViewWithText(@Nonnull final String text) { - return getSugarViewInteraction(withText(text)); - } - - /** - * Creates a {@link MokaViewInteraction} for a {@link View} based on a String matcher. - */ - @Nonnull - public static MokaViewInteraction onViewWithText(@Nonnull final Matcher matcher) { - return getSugarViewInteraction(withText(matcher)); - } - - /** - * Creates a {@link MokaViewInteraction} for a {@link View} based on it's hint property value. - */ - @Nonnull - public static MokaViewInteraction onViewWithHint(@Nonnull final String hint) { - return getSugarViewInteraction(withHint(is(hint))); - } - - public static Matcher withHint(final Matcher stringMatcher) { - return new BaseMatcher() { - @Override - public void describeTo(Description description) { - } - - @Override - public boolean matches(Object item) { - try { - Method method = item.getClass().getMethod("getHint"); - return stringMatcher.matches(method.invoke(item)); - } catch (NoSuchMethodException e) { - } catch (InvocationTargetException e) { - } catch (IllegalAccessException e) { - } - return false; - } - }; - } - - /** - * Creates a {@link MokaViewInteraction} from a {@link View} matcher. - */ - @Nonnull - public static MokaViewInteraction onView(@Nonnull final Matcher viewMatcher) { - return getSugarViewInteraction(viewMatcher); - } - - /** - * Creates a {@link MokaDataInteraction} that matches by data type and value on an Adapter {@link View} matched by resource id - */ - @Nonnull - public static MokaDataInteraction onDataIn(@IdRes final int list, @Nonnull final Class type, @Nonnull final Matcher valueMatcher) { - onViewWithId(list).loopMainThreadUntil(hasData()); - return checkItemIsPresentIn(list, type, valueMatcher); - } - - /** - * Creates a {@link MokaDataInteraction} that matches by data type and value on an Adapter {@link View} matched by resource - * id and checks that the element is displayed. - */ - @Nonnull - public static MokaDataInteraction checkItemIsPresentIn(@IdRes final int list, - @Nonnull final Class type, - @Nonnull final Matcher valueMatcher) { - onViewWithId(list).loopMainThreadUntil(hasData()); - MokaDataInteraction mokaDataInteraction = onAdapterItem(type, valueMatcher).inAdapterView(withId(list)); - mokaDataInteraction.check(matches(isDisplayed())); - return mokaDataInteraction; - } - - /** - * Creates a {@link MokaDataInteraction} that matches by data type and value on any Adapter View. - */ - @Nonnull - public static MokaDataInteraction onAdapterItem(@Nonnull final Class type, @Nonnull final Matcher valueMatcher) { - return getSugarDataInteraction(AllOf.allOf(is(instanceOf(type)), valueMatcher)); - } - - /** - * Creates a {@link MokaDataInteraction} from a data matcher. - */ - @Nonnull - private static MokaDataInteraction getSugarDataInteraction(@Nonnull final Matcher dataMatcher) { - return new MokaDataInteraction(onData(dataMatcher)); - } - - /** - * Loops on the main thread until the {@link View} we are trying to match exists. - */ - @SuppressWarnings("WeakerAccess") - public static void loopMainThreadUntilViewExists(@Nonnull final Matcher viewMatcher) { - loopMainThreadUntilViewExists(viewMatcher, MAX_WAIT); - } - - /** - * Loops on the main thread until the {@link View} we are trying to match exists. - * It waits the given amount of time before failing. - * - * @param maxWaitInMilliseconds in milliseconds - */ - @SuppressWarnings("WeakerAccess") - public static void loopMainThreadUntilViewExists(@Nonnull final Matcher viewMatcher, final int maxWaitInMilliseconds) { - loopMainThreadUntilView(viewMatcher, true, maxWaitInMilliseconds); - } - - public static boolean intentWillBeStubbedOut(@Nonnull final Intent intent) { - return intentStubber().getActivityResultForIntent(intent) != null; - } - - /** - * Performs a global action to go to the home screen, attempt to resume the previous activity and execute the given task. - * - * @param delayInMilliSeconds amount of time to wait (in milliseconds) before attempting to resume. - * @param afterHomeScreenTask task to execute after resuming the previous activity. - */ - @SuppressWarnings("FutureReturnValueIgnored") - public static void goToHomeScreenThenAttemptToResumeActivity(final int delayInMilliSeconds, @Nullable final Runnable afterHomeScreenTask) { - waitForEspressoToIdle(); - // Finished in TestRunner when some activity get resumed - if (sCurrentlyTryingToResume.compareAndSet(false, true)) { - launchHomeScreen(); - getBusyWaiter().busyWith(ATTEMPTING_TO_RESUME_ACTIVITY); - getMainThreadHandler().postDelayed(new Runnable() { - @Override - public void run() { - if (afterHomeScreenTask != null) { - afterHomeScreenTask.run(); - } - bringOurTaskToFront(); - ESPRESSO_SUGAR_THREAD.schedule(new Runnable() { - @Override - public void run() { - try { - loopMainThreadMillis(delayInMilliSeconds / 12); - final long millisSinceLastResumed = elapsedTimeSinceProcessStartIn(MILLISECONDS); - if (millisSinceLastResumed > (delayInMilliSeconds * 1.2)) { - Timber.w("No recently resumed activity, trying again... last resumed was over %d millis ago", - millisSinceLastResumed); - goToHomeScreenThenAttemptToResumeActivity(); - } else { - Timber.d("Activity resumed successfully after going to the Home screen, nothing further to do."); - getBusyWaiter().completed(ATTEMPTING_TO_RESUME_ACTIVITY); - } - } finally { - sCurrentlyTryingToResume.set(false); - } - } - }, (long) (delayInMilliSeconds * .3f), MILLISECONDS); - } - }, delayInMilliSeconds); - } else { - getBusyWaiter().completed(ATTEMPTING_TO_RESUME_ACTIVITY); - Timber.d("Skipped trying to go home because we were already trying... "); - } - } - - /** - * Performs a global action that attempts to launch the home screen. - */ - @SuppressWarnings("WeakerAccess") - @SuppressLint("OverlyBroadExceptionCaught") - public static void launchHomeScreen() { - tryToGetToHomeScreen(); - try { - for (int i = 0; i < 3; i++) { - // don't want to accidentally send back to our app, so make sure it is not in the foreground first. - if (!waitUntilNoneOfOurActivitiesAreResumedOrPaused()) { - tryToGetToHomeScreen(); - } - } - } catch (Exception e) { - // UiAutomation is flaky and occasionally we can't connect. - // we will just ignore - Timber.i("Couldn't connect to UiAutomation to send back key"); - } - waitForAccessibilityStreamToIdle(); - waitUntilNoneOfOurActivitiesAreResumedOrPaused(); - Timber.d("Finished Launching home screen."); - } - - /** - * Performs a global action that attempts to open the Notifications Drawer. - */ - @SuppressLint("OverlyBroadExceptionCaught") - public static void tryToOpenNotifications() { - getAccessibilityEventListener().waitUntil(not(withPackageName(InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()))); - Timber.i("Trying to open notifications via performGlobalAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS)"); - try { - performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS); - } catch (Exception e) { - // UiAutomation is flaky and occasionally we can't connect. - sendHomeIntent(); - } - waitForEspressoToIdle(); - } - - /** - * Performs a global action. Such an action can be performed at any moment - * regardless of the current application or user location in that application. - * - * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_BACK - * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_HOME - * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_NOTIFICATIONS - * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_RECENTS - */ - @SuppressWarnings("WeakerAccess") - public static void performGlobalAction(@GlobalAction final int globalAction) { - // looked at uiautomator code to figure out how to do this. - final UiAutomation automation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); - waitForAccessibilityStreamToIdle(); - assertNotNull("We can open the notification drawer without an instance of uiAutomation", automation); - assertTrue("Couldn't open the notification drawer.", automation.performGlobalAction(globalAction)); - waitForAccessibilityStreamToIdle(); - } - - /** - * Returns a {@link ComponentName} for an Activity class. - * Useful for Intent matching with {@link androidx.test.espresso.intent.matcher.IntentMatchers#hasComponent(ComponentName)} - */ - @Nonnull - public static ComponentName withClass(@Nonnull final Class activityClass) { - return new ComponentName(getTargetContext(), activityClass); - } - - /** - * Returns a localized string from the application's package's default string table. - */ - @Nonnull - public static String getString(@StringRes int id) { - return getTargetContext().getString(id); - } - - @SuppressWarnings("SameParameterValue") - private static void checkThatViewWithContentDescriptionIsNotDisplayedAndItDoesNotExists(@Nonnull final String contentDescription) { - loopMainThreadUntilViewDoesNotExists(allOf(withContentDescription(contentDescription), isDisplayed())); - } - - @SuppressLint("OverlyBroadExceptionCaught") - private static void tryToGetToHomeScreen() { - //todo - get reference to main package - getAccessibilityEventListener().waitUntil(not(withPackageName(InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()))); - Timber.i("Sending Home button via performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME)"); - try { - performGlobalAction(GLOBAL_ACTION_HOME); - } catch (Exception e) { - // UiAutomation is flaky and occasionally we can't connect. - sendHomeIntent(); - } - waitForEspressoToIdle(); - } - - @SuppressWarnings("deprecation") - // we are only using getRunningTasks for testing purposes, but this may not work on 21+ - private static void bringOurTaskToFront() { - Timber.d("After sitting on the home screen for a while, going to move our task to front."); - final ActivityManager activityManager = (ActivityManager) getTargetContext().getSystemService(Context.ACTIVITY_SERVICE); - final List runningTasks = activityManager.getRunningTasks(10); - RunningTaskInfo ourTask = null; - for (RunningTaskInfo runningTask : runningTasks) { - final String lastActiveTime = Build.VERSION.SDK_INT < 28 ? format(US, "lastActiveTime: %d,", getTaskLastActiveTime(runningTask)) : ""; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - Timber.d("Task: %s, Description: %s, %s Top Activity: %s, BaseActivity: %s", - runningTask, - runningTask.description, - lastActiveTime, - runningTask.topActivity, - runningTask.baseActivity); - } - } - } - - private static Long getTaskLastActiveTime(RunningTaskInfo runningTask) { - return getFieldValue(runningTask, RunningTaskInfo.class, "lastActiveTime"); - } - - private static void sendHomeIntent() { - Timber.i("Sending Home button intent"); - // From: http://stackoverflow.com/questions/4756835/how-to-launch-home-screen-programmatically-in-android - Intent startMain = new Intent(Intent.ACTION_MAIN); - startMain.addCategory(Intent.CATEGORY_HOME); - startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - getTargetContext().startActivity(startMain); - } - - private static IntentStubber intentStubber() { - final AtomicReference intentStubber = new AtomicReference<>(null); - // IntentStubberRegistry.getInstance() must be invoked from the main thread. - runOnMainSync(new Runnable() { - @Override - public void run() { - intentStubber.compareAndSet(null, IntentStubberRegistry.getInstance()); - } - }); - return intentStubber.get(); - } - - @SuppressWarnings("ThrowableNotThrown") - private static void loopMainThreadUntilView(@Nonnull final Matcher viewMatcher, - final boolean shouldExist, - final int maxWaitInMilliseconds) { - final AtomicBoolean viewExists = new AtomicBoolean(!shouldExist); - final AtomicReference exception = new AtomicReference<>(null); - - long startTime = System.currentTimeMillis(); - boolean timedOut = false; - long count = 1; - do { - try { - final long currentTimeMillis = System.currentTimeMillis(); - timedOut = currentTimeMillis - startTime > maxWaitInMilliseconds; - if (timedOut) { - Timber.d("Timing out with start time = %s and currentTimeMillis = %s", startTime, currentTimeMillis); - break; - } - count *= 2; - if (count > 32) { // back off after a few tries - Timber.d("Going to idle this many times before checking again: %d", count); - for (int i = 0; i < count; i++) { - waitForEspressoToIdle(); - } - } - Espresso.onView(viewMatcher).check(new ViewAssertion() { - @Override - public void check(final View view, final NoMatchingViewException noViewFoundException) { - final StringDescription description = new StringDescription(); - viewMatcher.describeTo(description); - Timber.d("Checking for: %s", description.toString()); - if (noViewFoundException != null) { - exception.set(noViewFoundException); - } - if (view != null && !(view.hasWindowFocus())) { - Timber.w("Found view %s matching: %s, but its Window did not have focus.", view, viewMatcher); - view.getRootView().requestFocus(); - } - viewExists.set(view != null && view.hasWindowFocus()); - } - }); - } catch (RuntimeException e) { - if (e instanceof IllegalStateException - || e.getCause() instanceof IllegalStateException) { - Timber.w(e, "Swallowing an IllegalStateException from recursively looping espresso."); - } else if (e instanceof NoActivityResumedException - || e.getMessage().contains("Waited for the root") - || (e.getCause() != null - && e.getCause().getMessage() != null - && e.getCause().getMessage().contains("Waited for the root"))) { - Timber.e(e, "Going to try and resume activity..."); - startTime = System.currentTimeMillis(); - count = 0; - Timber.d("Reset starttime to %s", startTime); - } else { - propagate(e); - } - } - } while (viewExists.get() != shouldExist); - - if (timedOut) { - final StringDescription stringDescription = new StringDescription(); - viewMatcher.describeTo(stringDescription); - final Throwable noViewFoundException = exception.get(); - fail(format(US, "Timed out after waiting %d seconds for view %s that meets this criteria %s\n%s", - maxWaitInMilliseconds / 1000, - shouldExist ? "to exist" : "to NOT exist", - stringDescription.toString(), - noViewFoundException != null ? noViewFoundException.getMessage() : "" - )); - } - } - - @Nonnull - private static Long elapsedTimeSinceProcessStartIn(@Nonnull final TimeUnit timeUnit) { - return sStopwatch.get().elapsed(timeUnit); - } - - private static BaseMatcher withPackageName(@Nonnull final String packageName) { - return new BaseMatcher() { - @Override - public boolean matches(final Object o) { - if (o instanceof AccessibilityEvent) { - final AccessibilityEvent accessibilityEvent = (AccessibilityEvent) o; - final CharSequence charSequence = accessibilityEvent.getPackageName(); - return charSequence.toString().contains(packageName); - } - return false; - } - - @Override - public void describeTo(final Description description) { - description.appendText(" contains packageName = " + packageName); - } - }; - } - - private static void loopMainThreadUntilViewDoesNotExists(@Nonnull final Matcher viewMatcher) { - loopMainThreadUntilViewDoesNotExists(viewMatcher, MAX_WAIT); - } - - private static void loopMainThreadUntilViewDoesNotExists(@Nonnull final Matcher viewMatcher, final int maxWaitInMilliseconds) { - loopMainThreadUntilView(viewMatcher, false, maxWaitInMilliseconds); - } - - @Nonnull - private static MokaViewInteraction getSugarViewInteraction(@Nonnull final Matcher viewMatcher) { - return getSugarViewInteraction(viewMatcher, MAX_WAIT); - } - - @Nonnull - private static MokaViewInteraction getSugarViewInteraction(@Nonnull final Matcher viewMatcher, final int maxWaitInMilliseconds) { - assertOnStartWasCalled(); - loopMainThreadUntilViewExists(viewMatcher, maxWaitInMilliseconds); - return new MokaViewInteraction(Espresso.onView(viewMatcher)); - } - - @SuppressWarnings("WeakerAccess") - @Retention(SOURCE) - @IntDef({GLOBAL_ACTION_BACK, GLOBAL_ACTION_HOME, GLOBAL_ACTION_NOTIFICATIONS, GLOBAL_ACTION_RECENTS}) - public @interface GlobalAction { - } -} diff --git a/moka/src/main/java/com/moka/EspressoMokaRunner.java b/moka/src/main/java/com/moka/EspressoMokaRunner.java deleted file mode 100644 index 5e1c19d..0000000 --- a/moka/src/main/java/com/moka/EspressoMokaRunner.java +++ /dev/null @@ -1,316 +0,0 @@ -package com.moka; - -import android.app.Activity; -import android.app.UiAutomation; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.view.accessibility.AccessibilityEvent; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.intent.IntentCallback; -import androidx.test.runner.intent.IntentMonitorRegistry; -import androidx.test.runner.lifecycle.ActivityLifecycleCallback; -import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; -import androidx.test.runner.lifecycle.Stage; - -import com.moka.lib.internals.ExceptionFromAnotherPlace; -import com.moka.lib.internals.TestAccessibilityEventListener; -import com.moka.lib.mainthread.MainThread; -import com.moka.waiter.android.BusyWaiter; - -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import timber.log.Timber; - -import static android.os.Looper.getMainLooper; -import static com.moka.EspressoInternals.alreadyLooping; -import static com.moka.EspressoInternals.waitForEspressoToIdle; -import static com.moka.EspressoMoka.intentWillBeStubbedOut; -import static com.moka.lib.internals.AnimatorsMoka.disableAnimators; -import static com.moka.lib.internals.DeviceSugar.printAvailableScreenSpace; -import static com.moka.lib.internals.ExceptionSugar.propagate; -import static java.lang.String.format; -import static java.util.Locale.US; - -public final class EspressoMokaRunner { - - private static final Handler MAIN_THREAD_HANDLER = new Handler(getMainLooper()); - private static final Thread MAIN_THREAD = getMainLooper().getThread(); - private static final BusyWaiter BUSY_WAITER = BusyWaiter.Companion.withOperationsCompletedOn(new BusyWaiterExecutionThread()); - private static final AtomicReference LAST_ERROR_OBJECT = new AtomicReference<>(null); - private static final AtomicReference LAST_THROWABLE = new AtomicReference<>(null); - private static final AtomicBoolean WAIT_FOR_ACTIVITIES_TO_RESUME = new AtomicBoolean(true); - private static final TestAccessibilityEventListener ACCESSIBILITY_EVENT_LISTENER = new TestAccessibilityEventListener(BUSY_WAITER); - private static final ActivityLifecycleCallback FINISH_ASYNC_OPERATION_WHEN_ACTIVITY_RESUMES = new ResumedActivityWatcher(); - - private static volatile Thread INSTRUMENTATION_THREAD; - private static boolean IS_STARTED = false; - private static boolean READY_TO_RUN_TESTS = false; - - private EspressoMokaRunner() { - } - - public static void onCreate(final Bundle arguments) { - MainThread.INSTANCE.checkOnMainThread(); - catchExceptionsFromTheApp(); - disableAnimators(); - - // turn off espresso usage tracking. - arguments.putString("disableAnalytics", "true"); - } - - public static void onStart() { - MainThread.INSTANCE.checkNotOnMainThread(); - IS_STARTED = true; - INSTRUMENTATION_THREAD = Thread.currentThread(); - makeEspressoWaitUntilActivitiesAreResumed(); - printAvailableScreenSpace(); - setupAccessibilityEventListener(); - EspressoMoka.onStart(); - } - - public static boolean onException(Object obj, @Nullable Throwable e) { - LAST_ERROR_OBJECT.set(obj); - LAST_THROWABLE.set(e); - - if (INSTRUMENTATION_THREAD == null && MainThread.INSTANCE.onMainThread()) { - // the application had an exception during start-up, so we should just fail - return false; - } - interruptInstrumentationThreadIfWaiting(); - return true; - } - - public static void runOnMainSync(@Nonnull final Runnable runner) { - runOnMainSyncDelayed(runner, 0); - } - - @Nonnull - static Handler getMainThreadHandler() { - return MAIN_THREAD_HANDLER; - } - - @Nonnull - public static BusyWaiter getBusyWaiter() { - return BUSY_WAITER; - } - - @Nonnull - static TestAccessibilityEventListener getAccessibilityEventListener() { - return ACCESSIBILITY_EVENT_LISTENER; - } - - static void failIfInstrumentationCaughtAnyErrors() { - if (!IS_STARTED || !READY_TO_RUN_TESTS) { - // Don't fail until we have at least gone thru the start up process and are ready to run tests - return; - } - final Throwable throwable = LAST_THROWABLE.get(); - final Object lastObject = LAST_ERROR_OBJECT.get(); - LAST_THROWABLE.set(null); - LAST_ERROR_OBJECT.set(null); - if (throwable != null && !(shouldIgnoreError(throwable, lastObject))) { - // TODO modify spoon so I can take a screen shot here. - Timber.e(throwable, "failIfInstrumentationCaughtAnyErrors is going to rethrow"); - throw new ExceptionFromAnotherPlace(throwable, lastObject); - } else if (throwable != null) { - Timber.w(throwable, "Ignoring exception (%s) from: %s", throwable.getMessage(), lastObject); - } - } - - static void loopMainThreadMillis(final int delayMillis) { - // not sure if this check is 100% necessary. - final AtomicBoolean alreadyLooping = new AtomicBoolean(false); - runOnMainSync(new Runnable() { - @Override - public void run() { - alreadyLooping.set(alreadyLooping()); - } - }); - if (alreadyLooping.get()) { - Timber.d("Main thread is already looping, NOT looping for %d millis", delayMillis); - return; - } - - runOnMainSyncDelayed(new Runnable() { - @Override - public void run() { - Timber.d("Main thread has been looping for %d millis", delayMillis); - } - }, - delayMillis); - } - - public static void runOnMainSyncDelayed(@Nonnull final Runnable runner, final int delayMillis) { - // the built in "runOnMainSync" is not very flexible ( it is not easily interrupted ) - // if we want the tests to continue after app crash we need this to be interruptable. - failIfInstrumentationCaughtAnyErrors(); - if (MainThread.INSTANCE.onMainThread()) { - if (delayMillis > 0) { - throw new IllegalStateException(format(US, "Can't runOnMainSyncDelayed with delay(%d) > 0 if you are already on the main thread", - delayMillis)); - } - runner.run(); - } else { - final FutureTask booleanFutureTask = new FutureTask<>(new Callable() { - @Override - public Throwable call() throws Exception { - try { - runner.run(); - } catch (Throwable e) { - return e; - } - return null; - } - }); - // post something AFTER the runnable that we can drain the queue. - MAIN_THREAD_HANDLER.postDelayed(booleanFutureTask, delayMillis); - try { - final Throwable throwable = booleanFutureTask.get(); - if (IS_STARTED && READY_TO_RUN_TESTS && throwable != null) { - throw new ExceptionFromAnotherPlace(throwable, MAIN_THREAD_HANDLER.getLooper().getThread()); - } else if (throwable != null) { - LAST_THROWABLE.set(throwable); - LAST_ERROR_OBJECT.set(runner); - } - } catch (InterruptedException | ExecutionException e) { - Timber.w(e, "runOnMainSync was Interrupted, it was probably not synchronous."); - } - } - failIfInstrumentationCaughtAnyErrors(); - } - - public static void waitForIdleSync() { - failIfInstrumentationCaughtAnyErrors(); - waitForEspressoToIdle(); - failIfInstrumentationCaughtAnyErrors(); - if (IS_STARTED) { - // WARNING: - // this assumes the GoogleInstrumentation super class calls this as part of super.onStart, - // but BEFORE it starts run tests - READY_TO_RUN_TESTS = true; - } - } - - private static boolean shouldIgnoreError(final Throwable throwable, final Object lastObject) { - // setting up for future if/else - //noinspection RedundantIfStatement - if (throwable instanceof NullPointerException - && lastObject instanceof Thread - && ((Thread) lastObject).getName().contains("qtp")) { - // ignore NPE from WireMock/Jetty on background threads - return true; - } - - return false; - } - - private static void catchExceptionsFromTheApp() { - final Thread.UncaughtExceptionHandler originalUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); - Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { - @Override - public void uncaughtException(@NotNull final Thread thread, @NotNull final Throwable ex) { - if (thread.equals(INSTRUMENTATION_THREAD)) { - propagate(ex); - } else { - Timber.w(ex, "ExecutionThread with name (%s) died with exception", thread.getName()); - onException(thread, ex); - interruptInstrumentationThreadIfWaiting(); - } - } - }); - // we should keep the original on the main thread until later, in case there is a problem during app start up. - MAIN_THREAD.setUncaughtExceptionHandler(originalUncaughtExceptionHandler); - } - - private static void interruptInstrumentationThreadIfWaiting() { - // wake up the test thread as whatever it was waiting for it probably not going to happen. - // it should just fail and move on to the next test. - if (INSTRUMENTATION_THREAD != null) { - Thread.State state = INSTRUMENTATION_THREAD.getState(); - Timber.i("Instrumentation ExecutionThread State: %s", state); - if (state == Thread.State.BLOCKED || state == Thread.State.WAITING) { - Timber.i("About to interrupt Instrumentation ExecutionThread:"); - INSTRUMENTATION_THREAD.interrupt(); - state = INSTRUMENTATION_THREAD.getState(); - Timber.i("Instrumentation ExecutionThread State after interrupt: %s", state); - } - } - } - - private static void setupAccessibilityEventListener() { - // wrap in an anonymous class so we don't get problems on old API levels. - InstrumentationRegistry.getInstrumentation().getUiAutomation().setOnAccessibilityEventListener(new UiAutomation.OnAccessibilityEventListener() { - @Override - public void onAccessibilityEvent(final AccessibilityEvent accessibilityEvent) { - ACCESSIBILITY_EVENT_LISTENER.onAccessibilityEvent(accessibilityEvent); - } - }); - } - - public static void tellEspressoToNotWaitForActivityToResume() { - WAIT_FOR_ACTIVITIES_TO_RESUME.set(false); - } - - public static void tellEspressoToWaitForActivityToResume() { - WAIT_FOR_ACTIVITIES_TO_RESUME.set(true); - } - - private static void makeEspressoWaitUntilActivitiesAreResumed() { - IntentMonitorRegistry.getInstance().addIntentCallback(new IntentCallback() { - @Override - public void onIntentSent(final Intent intent) { - runOnMainSync(new Runnable() { - @Override - public void run() { - if (!intentWillBeStubbedOut(intent) - && WAIT_FOR_ACTIVITIES_TO_RESUME.get() - && isExplicitIntent(intent) - && targetsSomethingInTheAppUnderTest(intent)) { - // finished in the activity lifecycle monitor - BUSY_WAITER.busyWith(intent.getComponent().getClassName()); - } - } - }); - } - }); - - ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(FINISH_ASYNC_OPERATION_WHEN_ACTIVITY_RESUMES); - } - - private static boolean isExplicitIntent(@Nonnull final Intent intent) { - return intent.getComponent() != null; - } - - private static boolean targetsSomethingInTheAppUnderTest(final Intent intent) { - return Objects.equals(intent.getPackage(), InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()); - } - - private static class ResumedActivityWatcher implements ActivityLifecycleCallback { - @Override - public void onActivityLifecycleChanged(final Activity activity, final Stage stage) { - if (stage == Stage.RESUMED) { - // started in UiTest.getActivity() - BUSY_WAITER.completed(activity.getClass().getName()); - } - } - } - - private static class BusyWaiterExecutionThread implements BusyWaiter.ExecutionThread { - @Override - public void execute(@NotNull final Runnable runnable) { - MAIN_THREAD_HANDLER.post(runnable); - } - } -} diff --git a/moka/src/main/java/com/moka/MokaDataInteraction.kt b/moka/src/main/java/com/moka/MokaDataInteraction.kt deleted file mode 100644 index 4bb986f..0000000 --- a/moka/src/main/java/com/moka/MokaDataInteraction.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.moka - -import android.view.View -import androidx.test.espresso.* -import androidx.test.espresso.action.AdapterViewProtocol -import androidx.test.espresso.action.ViewActions.clearText -import androidx.test.espresso.action.ViewActions.typeText -import androidx.test.espresso.assertion.ViewAssertions.matches -import com.moka.lib.actions.clickWithRetry -import org.hamcrest.Matcher - -class MokaDataInteraction internal constructor(private val dataInteraction: DataInteraction) : MokaInteraction() { - - fun click(): MokaDataInteraction { - perform(clickWithRetry()) - return this - } - - fun type(text: String): MokaDataInteraction { - perform(typeText(text)) - return this - } - - fun clearAndType(text: String): MokaDataInteraction { - perform(clearText()) - return type(text) - } - - fun setText(text: String): MokaDataInteraction { - perform(com.moka.lib.actions.setText(text)) - return this - } - - fun onChildView(childMatcher: Matcher): MokaDataInteraction { - runWithRetry { dataInteraction.onChildView(childMatcher) } - return this - } - - fun inRoot(rootMatcher: Matcher): MokaDataInteraction { - runWithRetry { dataInteraction.inRoot(rootMatcher) } - return this - } - - fun inAdapterView(adapterMatcher: Matcher): MokaDataInteraction { - runWithRetry { dataInteraction.inAdapterView(adapterMatcher) } - return this - } - - fun atPosition(atPosition: Int): MokaDataInteraction { - runWithRetry { dataInteraction.atPosition(atPosition) } - return this - } - - fun usingAdapterViewProtocol(adapterViewProtocol: AdapterViewProtocol): MokaDataInteraction { - runWithRetry { dataInteraction.usingAdapterViewProtocol(adapterViewProtocol) } - return this - } - - fun check(assertion: ViewAssertion): MokaDataInteraction { - runWithRetry { dataInteraction.check(assertion) } - return this - } - - fun checkMatches(matcher: Matcher): MokaDataInteraction { - loopMainThreadUntil(matcher) - runWithRetry { dataInteraction.check(matches(matcher)) } - return this - } - - override fun perform(vararg viewActions: ViewAction): MokaViewInteraction { - return runWithRetryAndReturnSugarViewInteraction { dataInteraction.perform(*viewActions) } - } - - private fun runWithRetryAndReturnSugarViewInteraction(action: () -> ViewInteraction): MokaViewInteraction { - return MokaViewInteraction(runWithRetry(action)) - } -} diff --git a/moka/src/main/java/com/moka/MokaInteraction.kt b/moka/src/main/java/com/moka/MokaInteraction.kt deleted file mode 100644 index 57d70f7..0000000 --- a/moka/src/main/java/com/moka/MokaInteraction.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.moka - -import android.view.View -import androidx.test.espresso.* -import androidx.test.espresso.util.HumanReadables.getViewHierarchyErrorMessage -import com.moka.EspressoInternals.waitForEspressoToIdle -import com.moka.EspressoMokaRunner.runOnMainSyncDelayed -import com.moka.lib.internals.TestAccessibilityEventListener.toPrintString -import com.moka.lib.internals.propagate -import org.hamcrest.BaseMatcher -import org.hamcrest.Description -import org.hamcrest.Matcher -import timber.log.Timber -import java.lang.System.currentTimeMillis - -@Suppress("UnnecessaryParentheses") -abstract class MokaInteraction { - fun loopMainThreadUntil(viewMatcher: Matcher) = perform(LoopUntil(viewMatcher)) - - abstract fun perform(vararg viewActions: ViewAction): MokaViewInteraction - - protected fun runWithRetry(action: () -> T): T { - try { - try { - return action() - } catch (e: RuntimeException) { - Timber.w(e, "Exception while performing %s, going to retry", action) - when { - shouldWaitAndTryAgain(e) -> { - // Sometimes we just didn't wait long enough for the UI to update with the new View hierarchy - waitForEspressoToIdle() - runOnMainSyncDelayed({ Timber.w("Stalling because of exception: %s", e.message) }, 20) - waitForEspressoToIdle() - Timber.w(e, "Retrying failed action after a bit of waiting... ") - return action() - } - else -> throw e - } - } - } catch (e: Exception) { - throw propagate(e) - } - } - - private fun shouldWaitAndTryAgain(e: RuntimeException): Boolean { - return hasCause(e, NoMatchingViewException::class.java) || hasCause(e, InjectEventSecurityException::class.java) - } - - private fun hasCause(e: Throwable?, clazz: Class<*>): Boolean { - return e != null && (clazz.isInstance(e) || e.cause != null) && hasCause(e.cause, clazz) - } - - private fun shouldTryToResumeActivity(e: Throwable): Boolean { - val cause = e.cause - return (e is PerformException && cause?.javaClass != RuntimeException::class.java) - || e is NoActivityResumedException - || e.message?.contains("Waited for the root") ?: false - || (cause != null && shouldTryToResumeActivity(cause)) - } - - private class LoopUntil(private val viewMatcher: Matcher) : ViewAction { - - override fun getConstraints(): Matcher { - return object : BaseMatcher() { - override fun matches(o: Any): Boolean = o is View - - override fun describeTo(description: Description) { - description.appendText("Is instance of View") - } - } - } - - override fun getDescription(): String = "looping the main thread until ${toPrintString(viewMatcher)}" - - @Suppress("TooGenericExceptionThrown") - override fun perform(uiController: UiController, view: View) { - val startTime = currentTimeMillis() - var timeSinceStart = currentTimeMillis() - startTime - var timedOut = timeSinceStart > TIME_OUT - var count = 0 - while (!viewMatcher.matches(view) && !timedOut) { - count++ - uiController.loopMainThreadUntilIdle() - if (count < 5) { - for (i in 0 until count) { // linear back off if it doesn't match right away... - uiController.loopMainThreadUntilIdle() - } - } - timeSinceStart = currentTimeMillis() - startTime - timedOut = timeSinceStart > TIME_OUT - } - if (timedOut) { - throw RuntimeException( - getViewHierarchyErrorMessage( - view.rootView, - listOf(view), - "Timed out after ${(TIME_OUT / 1000)} seconds, while looping until $view meets criteria $viewMatcher", "**PROBLEM**" - ) - ) - } - } - - companion object { - private const val TIME_OUT: Long = 30000 - } - } -} diff --git a/moka/src/main/java/com/moka/MokaViewInteraction.kt b/moka/src/main/java/com/moka/MokaViewInteraction.kt deleted file mode 100644 index 414afeb..0000000 --- a/moka/src/main/java/com/moka/MokaViewInteraction.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.moka - -import android.view.View -import androidx.test.espresso.Root -import androidx.test.espresso.ViewAction -import androidx.test.espresso.ViewAssertion -import androidx.test.espresso.ViewInteraction -import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.action.ViewActions.clearText -import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView -import androidx.test.espresso.assertion.ViewAssertions.matches -import com.moka.lib.actions.clickWithRetry -import com.moka.lib.actions.requestFocus -import com.moka.lib.matchers.hasWindowFocus -import org.hamcrest.Matcher - -class MokaViewInteraction internal constructor(private val viewInteraction: ViewInteraction) : MokaInteraction() { - - fun click(): MokaViewInteraction { - perform(requestFocus()) - loopMainThreadUntil(hasWindowFocus()) - perform(clickWithRetry()) - return this - } - - fun type(text: String): MokaViewInteraction { - perform(clickWithRetry()) - perform(typeTextIntoFocusedView(text)) - return this - } - - fun pressImeActionButton(): MokaViewInteraction { - perform(ViewActions.pressImeActionButton()) - return this - } - - fun clearAndType(text: String): MokaViewInteraction { - perform(clearText()) - return type(text) - } - - fun setText(text: String): MokaViewInteraction { - perform(com.moka.lib.actions.setText(text)) - return this - } - - fun scrollTo(): MokaViewInteraction { - perform(androidx.test.espresso.action.ViewActions.scrollTo()) - return this - } - - fun inRoot(rootMatcher: Matcher): MokaViewInteraction { - runWithRetry { viewInteraction.inRoot(rootMatcher) } - return this - } - - fun check(assertion: ViewAssertion): MokaViewInteraction { - runWithRetry { viewInteraction.check(assertion) } - return this - } - - fun checkMatches(matcher: Matcher): MokaViewInteraction { - loopMainThreadUntil(matcher) - runWithRetry { viewInteraction.check(matches(matcher)) } - return this - } - - override fun perform(vararg viewActions: ViewAction): MokaViewInteraction { - return runWithRetryAndReturnSugarViewInteraction { viewInteraction.perform(*viewActions) } - } - - private fun runWithRetryAndReturnSugarViewInteraction(action: () -> Unit): MokaViewInteraction { - runWithRetry(action) - return this - } -} diff --git a/moka/src/main/java/com/moka/lib/actions/AppBarLayoutCollapseAction.kt b/moka/src/main/java/com/moka/lib/actions/AppBarLayoutCollapseAction.kt deleted file mode 100644 index fb934a0..0000000 --- a/moka/src/main/java/com/moka/lib/actions/AppBarLayoutCollapseAction.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.moka.lib.actions - -import android.view.View -import android.view.ViewGroup -import androidx.test.espresso.PerformException -import androidx.test.espresso.UiController -import androidx.test.espresso.ViewAction -import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom -import androidx.test.espresso.util.HumanReadables -import com.google.android.material.appbar.AppBarLayout -import com.moka.lib.internals.Reflection.getFieldValue -import org.hamcrest.Matcher - -fun collapseAppBar() = AppBarLayoutCollapseAction() - -class AppBarLayoutCollapseAction : ViewAction { - override fun getDescription(): String = "Collapse AppBarLayout" - - override fun getConstraints(): Matcher = isAssignableFrom(View::class.java) - - override fun perform(uiController: UiController, view: View) { - val rootView = view.rootView as ViewGroup - val appBarLayout = findAppBarLayoutDescendant(rootView) - ?: throw PerformException.Builder() - .withViewDescription(HumanReadables.getViewHierarchyErrorMessage(rootView, null, "no AppBarLayout found", null)) - .build() - appBarLayout.collapseWithoutAnimation() - - uiController.loopMainThreadUntilIdle() - while (appBarLayout.hasPendingAction()) { - uiController.loopMainThreadForAtLeast(60) - } - } - - private fun findAppBarLayoutDescendant(rootView: ViewGroup): AppBarLayout? { - return (0..rootView.childCount) - .map { rootView.getChildAt(it) } - .mapNotNull { - when (it) { - is AppBarLayout -> it - is ViewGroup -> findAppBarLayoutDescendant(it) - else -> null - } - } - .singleOrNull() - } -} - -private const val PENDING_ACTION_NONE = 0x0 -private fun AppBarLayout.collapseWithoutAnimation() { - this.setExpanded(false, false) -} - -private fun AppBarLayout.hasPendingAction(): Boolean { - val pendingAction: Int = getFieldValue(this, "mPendingAction") - return pendingAction != PENDING_ACTION_NONE -} diff --git a/moka/src/main/java/com/moka/lib/actions/NestedScrollViewScrollToAction.kt b/moka/src/main/java/com/moka/lib/actions/NestedScrollViewScrollToAction.kt deleted file mode 100644 index ec7e69b..0000000 --- a/moka/src/main/java/com/moka/lib/actions/NestedScrollViewScrollToAction.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.moka.lib.actions - -import android.graphics.Rect -import android.view.View -import android.widget.OverScroller -import androidx.core.widget.NestedScrollView -import androidx.test.espresso.PerformException -import androidx.test.espresso.UiController -import androidx.test.espresso.ViewAction -import androidx.test.espresso.matcher.ViewMatchers.* -import androidx.test.espresso.util.HumanReadables.getViewHierarchyErrorMessage -import com.moka.lib.internals.Reflection.getFieldValue -import org.hamcrest.Matcher -import org.hamcrest.Matchers.allOf -import org.hamcrest.Matchers.anyOf -import timber.log.Timber - -/** - * This view action allows scrolling to occur on a NestedScrollView - * - * - * Enables scrolling to the given view. Typically, scrollTo() can be used, but this - * does not work with NestedScrollView. - * - * - * See: https://issuetracker.google.com/issues/37087431 - * - * - * Source: https://gist.github.com/miszmaniac/12f720b7e898ece55d2464fe645e1f36 - */ -class NestedScrollViewScrollToAction : ViewAction { - - override fun getConstraints(): Matcher { - return allOf(withEffectiveVisibility(Visibility.VISIBLE), isDescendantOfA(anyOf( - isAssignableFrom(NestedScrollView::class.java)))) - } - - override fun perform(uiController: UiController, view: View) { - uiController.loopMainThreadUntilIdle() - if (isDisplayingAtLeast(90).matches(view)) { - Timber.i("View is already displayed. Returning.") - return - } - - val parentScrollView = findScrollView(view) - parentScrollView.requestLayout() - - uiController.loopMainThreadUntilIdle() - - val rect = Rect() - view.getDrawingRect(rect) - if (!/* immediate */view.requestRectangleOnScreen(rect, false)) { - Timber.w("Scrolling to view was requested, but none of the parents scrolled.") - } - val mScroller = getFieldValue(parentScrollView, NestedScrollView::class.java, "mScroller") - - // this is slower that I would like... should look for a faster way to scroll - while (!mScroller.isFinished) { - uiController.loopMainThreadUntilIdle() - } - - if (!isDisplayingAtLeast(90).matches(view)) { - throw PerformException.Builder() - .withActionDescription(this.description) - .withViewDescription(getViewHierarchyErrorMessage(view.rootView, - listOf(view), - "Scrolling to view was attempted, but the view is not displayed", - "NOT DISPLAYED 90 percent")) - .build() - } - } - - private fun findScrollView(view: View): View { - val parent = view.parent as View - return parent as? NestedScrollView ?: findScrollView(parent) - } - - override fun getDescription(): String { - return "scroll to" - } - - companion object { - - fun scrollTo(): NestedScrollViewScrollToAction { - return NestedScrollViewScrollToAction() - } - } -} diff --git a/moka/src/main/java/com/moka/lib/debug/DebugTrace.kt b/moka/src/main/java/com/moka/lib/debug/DebugTrace.kt deleted file mode 100644 index dbe0ad2..0000000 --- a/moka/src/main/java/com/moka/lib/debug/DebugTrace.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.moka.lib.debug - -import android.os.Trace -import com.moka.BuildConfig - -object DebugTrace { - - @JvmStatic - fun beginSection(sectionName: String) { - if (BuildConfig.DEBUG) { - Trace.beginSection(sectionName) - } - } - - @JvmStatic - fun endSection() { - if (BuildConfig.DEBUG) { - Trace.endSection() - } - } -} diff --git a/moka/src/main/java/com/moka/lib/internals/AnimatorsMoka.java b/moka/src/main/java/com/moka/lib/internals/AnimatorsMoka.java deleted file mode 100644 index b088c22..0000000 --- a/moka/src/main/java/com/moka/lib/internals/AnimatorsMoka.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.moka.lib.internals; - -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ValueAnimator; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.Collection; - -import javax.annotation.Nonnull; - -import static android.os.Build.VERSION.SDK_INT; -import static com.moka.EspressoMokaRunner.runOnMainSync; -import static com.moka.lib.internals.Reflection.invokeStatic; -import static com.moka.lib.internals.Reflection.setFieldValue; - -public final class AnimatorsMoka { - - private AnimatorsMoka() { - - } - - public static void disableAnimators() { - runOnMainSync(new Runnable() { - @Override - public void run() { - //android disabled access to hidden methods starting API 28 - if (SDK_INT < 28) { - setFieldValue(null, ValueAnimator.class, "sDurationScale", 0.0f); - setFieldValue(getStaticAnimationHandler(), getAnimationsCallBacks(), new ArrayListThatDisablesAddedAnimators()); - } - } - - @NonNull - private String getAnimationsCallBacks() { - return SDK_INT < 24 ? "mAnimations" : "mAnimationCallbacks"; - } - - @Nonnull - private Object getStaticAnimationHandler() { - if (SDK_INT < 24) { - return invokeStatic("android.animation.ValueAnimator", "getOrCreateAnimationHandler"); - } else { - return invokeStatic("android.animation.AnimationHandler", "getInstance"); - } - } - }); - } - - private static class ArrayListThatDisablesAddedAnimators extends ArrayList { - @Override - public boolean add(final T object) { - disableAnimation(object); - return super.add(object); - } - - @Override - public void add(final int index, final T object) { - disableAnimation(object); - super.add(index, object); - } - - @Override - public boolean addAll(final Collection collection) { - for (T t : collection) { - disableAnimation(t); - } - return super.addAll(collection); - } - - @Override - public boolean addAll(final int index, final Collection collection) { - for (T t : collection) { - disableAnimation(t); - } - return super.addAll(index, collection); - } - - @Override - public T set(final int index, final T object) { - disableAnimation(object); - return super.set(index, object); - } - - private void disableAnimation(final Object object) { - if (object instanceof Animator) { - ((Animator) object).setDuration(0); - } - if (object instanceof AnimatorSet) { - disableAnimation(((AnimatorSet) object).getChildAnimations()); - } else if (object instanceof ValueAnimator) { - ((ValueAnimator) object).setRepeatCount(0); - } - } - } -} diff --git a/moka/src/main/java/com/moka/lib/internals/DeviceMoka.kt b/moka/src/main/java/com/moka/lib/internals/DeviceMoka.kt deleted file mode 100644 index a043f20..0000000 --- a/moka/src/main/java/com/moka/lib/internals/DeviceMoka.kt +++ /dev/null @@ -1,24 +0,0 @@ -@file:JvmName("DeviceSugar") - -package com.moka.lib.internals - -import android.content.Context -import android.graphics.Point -import android.util.DisplayMetrics -import android.view.WindowManager -import androidx.test.platform.app.InstrumentationRegistry -import timber.log.Timber - -fun printAvailableScreenSpace() { - val windowManager = InstrumentationRegistry.getInstrumentation().context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - - val displayMetrics = DisplayMetrics().apply { windowManager.defaultDisplay.getMetrics(this) } - - val densityDpi = displayMetrics.densityDpi - val outSmallestSize = Point() - val outLargestSize = Point() - - windowManager.defaultDisplay.getCurrentSizeRange(outSmallestSize, outLargestSize) - - Timber.d("dpi: $densityDpi\nPortrait: ${outLargestSize.y} high by ${outSmallestSize.x} wide") -} diff --git a/moka/src/main/java/com/moka/lib/internals/ExceptionMoka.kt b/moka/src/main/java/com/moka/lib/internals/ExceptionMoka.kt deleted file mode 100644 index d7b2616..0000000 --- a/moka/src/main/java/com/moka/lib/internals/ExceptionMoka.kt +++ /dev/null @@ -1,32 +0,0 @@ -@file:JvmName("ExceptionSugar") - -package com.moka.lib.internals - -import timber.log.Timber -import java.io.PrintStream -import java.io.PrintWriter - -@Suppress("TooGenericExceptionThrown") -fun propagate(e: Throwable): RuntimeException { - Timber.w(e, "Propagating Throwable: ${e.javaClass.simpleName} ${e.message}") - if (e is RuntimeException) { - throw e - } else { - throw RuntimeException(e) - } -} - -class ExceptionFromAnotherPlace(throwable: Throwable, thread: Any) : RuntimeException(message(throwable, thread), throwable) { - - override fun printStackTrace(err: PrintStream) = cause?.printStackTrace(err) - ?: super.printStackTrace(err) - - override fun printStackTrace(err: PrintWriter) = cause?.printStackTrace(err) - ?: super.printStackTrace(err) - - companion object { - private fun message(throwable: Throwable, thread: Any): String { - return "From $thread: ExecutionThread died with an exception ( ${throwable.javaClass} : ${throwable.message} ), attempting to restart." - } - } -} diff --git a/moka/src/main/java/com/moka/lib/internals/Reflection.java b/moka/src/main/java/com/moka/lib/internals/Reflection.java deleted file mode 100644 index 92b92d5..0000000 --- a/moka/src/main/java/com/moka/lib/internals/Reflection.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.moka.lib.internals; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import static com.moka.lib.internals.ExceptionSugar.propagate; - -public final class Reflection { - - private static final Class[] EMPTY_CLASS_ARRAY = new Class[]{}; - - private Reflection() { - } - - @SuppressWarnings({"TypeParameterUnusedInFormals"}) - public static T getStaticFieldValue(String clazz, final String fieldName) { - return getFieldValue(null, clazz(clazz, Thread.currentThread().getContextClassLoader()), fieldName); - } - - @SuppressWarnings({"TypeParameterUnusedInFormals"}) - public static T getFieldValue(Object o, String clazz, final String fieldName) { - return getFieldValue(o, clazz(clazz, o.getClass().getClassLoader()), fieldName); - } - - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - public static T getFieldValue(Object o, Class clazz, final String fieldName) { - try { - Field get = clazz.getDeclaredField(fieldName); - get.setAccessible(true); - return (T) get.get(o); - } catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException e) { - throw propagate(e); - } - } - - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - public static T getFieldValue(Object o, final String fieldName) { - return getFieldValue(o, o.getClass(), fieldName); - } - - @SuppressWarnings("unchecked") - public static void setFieldValue(Object o, final String fieldName, Object value) { - setFieldValue(o, o.getClass(), fieldName, value); - } - - @SuppressWarnings("unchecked") - public static void setFieldValue(Object o, Class clazz, final String fieldName, Object value) { - try { - Field set = clazz.getDeclaredField(fieldName); - set.setAccessible(true); - set.set(o, value); - } catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException e) { - throw propagate(e); - } - } - - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - public static T invokeStatic(final String aClass, final String methodName) { - try { - return invokeStatic(Reflection.class.getClassLoader().loadClass(aClass), methodName); - } catch (ClassNotFoundException | IllegalArgumentException e) { - throw propagate(e); - } - } - - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - public static T invokeStatic(final Class aClass, final String methodName) { - try { - Method get = aClass.getDeclaredMethod(methodName); - get.setAccessible(true); - return (T) get.invoke(null); - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - throw propagate(e); - } - } - - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - public static T invokeStatic(final Class aClass, final String methodName, final Class[] parameterTypes, final Object... args) { - try { - Method get = aClass.getDeclaredMethod(methodName, parameterTypes); - get.setAccessible(true); - return (T) get.invoke(null, args); - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - throw propagate(e); - } - } - - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - public static T invoke(Object instance, final String methodName, final Class[] parameterTypes, final Object... args) { - return invoke(instance, instance.getClass(), methodName, parameterTypes, args); - } - - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - public static T invoke(Object instance, final Class aClass, final String methodName, final Class[] parameterTypes, final Object... args) { - try { - Method get = aClass.getDeclaredMethod(methodName, parameterTypes); - get.setAccessible(true); - return (T) get.invoke(instance, args); - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - throw propagate(e); - } - } - - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - public static T newInstance(String s, final ClassLoader classLoader) { - return newInstance(s, classLoader, EMPTY_CLASS_ARRAY); - } - - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - public static T newInstance(String s, final ClassLoader classLoader, final Class[] parameterTypes, final Object... args) { - try { - Class clazz = clazz(s, classLoader); - Constructor constructor = clazz.getDeclaredConstructor(parameterTypes); - constructor.setAccessible(true); - return (T) constructor.newInstance(args); - } catch (InstantiationException | InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { - throw propagate(e); - } - } - - public static Class clazz(String s, final ClassLoader classLoader) { - try { - return Class.forName(s, false, classLoader); - } catch (ClassNotFoundException e) { - throw propagate(e); - } - } -} diff --git a/moka/src/main/java/com/moka/lib/internals/TestAccessibilityEventListener.java b/moka/src/main/java/com/moka/lib/internals/TestAccessibilityEventListener.java deleted file mode 100644 index 33cdcfb..0000000 --- a/moka/src/main/java/com/moka/lib/internals/TestAccessibilityEventListener.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.moka.lib.internals; - -import android.annotation.SuppressLint; -import android.view.accessibility.AccessibilityEvent; - -import com.moka.waiter.android.BusyWaiter; - -import org.hamcrest.Matcher; -import org.hamcrest.StringDescription; - -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -import timber.log.Timber; - -import static com.moka.EspressoMoka.waitForAccessibilityStreamToIdle; -import static com.moka.lib.internals.ExceptionSugar.propagate; - -public class TestAccessibilityEventListener { - - private final BusyWaiter busyWaiter; - private final Executor accessibilityEventListenerExecutor = Executors.newSingleThreadExecutor(); - private List> matchers = new CopyOnWriteArrayList<>(); - - public TestAccessibilityEventListener(final BusyWaiter busyWaiter) { - this.busyWaiter = busyWaiter; - } - - // It's okay to catch a Throwable here since the goal is to log unexpected errors within an matcher that we do not own. - @SuppressLint("OverlyBroadExceptionCaught") - public void onAccessibilityEvent(final AccessibilityEvent accessibilityEvent) { - for (Iterator> iterator = matchers.iterator(); iterator.hasNext(); ) { - final Matcher matcher = iterator.next(); - final boolean matches; - try { - matches = matcher.matches(accessibilityEvent); - if (matches) { - Timber.d("Found a match for matcher (%s) : %s", toPrintString(matcher), accessibilityEvent.toString()); - accessibilityEventListenerExecutor.execute(new Runnable() { - @Override - public void run() { - waitForAccessibilityStreamToIdle(); - busyWaiter.postCompleted(matcher); - waitForAccessibilityStreamToIdle(); - } - }); - matchers.remove(matcher); - } - } catch (Throwable e) { - Timber.e(e, "Error matching accessibilityEvent"); - propagate(e); - } - } - - Timber.d(accessibilityEvent.toString()); - } - - public void waitUntil(Matcher accessibilityEventMatcher) { - matchers.add(accessibilityEventMatcher); - busyWaiter.busyWith(accessibilityEventMatcher); - } - - public static String toPrintString(final Matcher matcher) { - final StringDescription stringDescription = new StringDescription(); - matcher.describeTo(stringDescription); - return stringDescription.toString(); - } -} diff --git a/moka/src/main/java/com/moka/lib/matchers/ViewMatchers.kt b/moka/src/main/java/com/moka/lib/matchers/ViewMatchers.kt index d3dca00..b50c586 100644 --- a/moka/src/main/java/com/moka/lib/matchers/ViewMatchers.kt +++ b/moka/src/main/java/com/moka/lib/matchers/ViewMatchers.kt @@ -19,9 +19,7 @@ import androidx.test.espresso.matcher.BoundedMatcher import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.viewpager.widget.ViewPager import com.google.android.material.textfield.TextInputLayout -import com.moka.EspressoInternals import com.moka.lib.actions.SpannableUtils.getSpansValues -import com.moka.lib.internals.TestAccessibilityEventListener.toPrintString import org.hamcrest.BaseMatcher import org.hamcrest.CoreMatchers.containsString import org.hamcrest.Description @@ -74,42 +72,6 @@ fun withTextColor(@ColorInt color: Int): Matcher { } } -/** - * Returns a [Matcher] which accepts a view so long as it can be scrolled to satisfy - * the input view matcher. Note that it also actually scrolls the [View]. - */ -fun canBeScrolledSoIt(viewMatcher: Matcher): Matcher { - return object : BaseMatcher() { - override fun matches(o: Any?): Boolean { - if (o != null && o is View) { - val matches = viewMatcher.matches(o) - if (matches) { - return true - } - - var parent: ViewParent? = o.parent - while (parent != null) { - if (parent is ScrollView) { - parent.requestChildFocus(o, o) - EspressoInternals.waitForEspressoToIdle() - return viewMatcher.matches(o) - } else if (parent is NestedScrollView) { - parent.requestChildFocus(o, o) - EspressoInternals.waitForEspressoToIdle() - return viewMatcher.matches(o) - } - parent = parent.parent - } - } - return false - } - - override fun describeTo(description: Description) { - description.appendText(" can be scrolled so it " + toPrintString(viewMatcher)) - } - } -} - /** * Returns a [Matcher] that matches a [View] if it has [android.view.Window] Focus */ diff --git a/moka/src/main/java/com/moka/utils/NoAnimationActivityRule.kt b/moka/src/main/java/com/moka/utils/NoAnimationActivityRule.kt deleted file mode 100644 index 8c5c22f..0000000 --- a/moka/src/main/java/com/moka/utils/NoAnimationActivityRule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.moka.utils - -import android.app.Activity -import android.content.Intent -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.ActivityTestRule -import androidx.test.uiautomator.UiDevice -import timber.log.Timber - -open class NoAnimationActivityRule(activityClass: Class) : ActivityTestRule(activityClass) { - - private val device by lazy { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) } - - override fun launchActivity(startIntent: Intent?): T { - Timber.d(device.executeShellCommand("settings put global window_animation_scale 0")) - Timber.d(device.executeShellCommand("settings put global transition_animation_scale 0")) - Timber.d(device.executeShellCommand("settings put global animator_duration_scale 0")) - Timber.d(device.executeShellCommand("settings put system window_animation_scale 0")) - Timber.d(device.executeShellCommand("settings put system transition_animation_scale 0")) - Timber.d(device.executeShellCommand("settings put system animator_duration_scale 0")) - return super.launchActivity(startIntent) - } -} \ No newline at end of file diff --git a/moka/src/main/java/com/moka/waiter/android/BusyWaiter.kt b/moka/src/main/java/com/moka/waiter/android/BusyWaiter.kt deleted file mode 100644 index 0ae7a60..0000000 --- a/moka/src/main/java/com/moka/waiter/android/BusyWaiter.kt +++ /dev/null @@ -1,279 +0,0 @@ -package com.moka.waiter.android - -import androidx.annotation.GuardedBy -import timber.log.Timber -import java.lang.System.identityHashCode -import java.util.* -import java.util.concurrent.locks.ReentrantLock - -/** - * This allows the app to let Espresso know when it is busy and Espresso should wait. - * It is not really tied to Espresso and could be used in any case where you need to know if the app is "busy". - * - * Call busyWith when you start being "busy" - * Call completed when you are done and start being "idle". - * - * Generally, you should call completed from a finally block. - * - * Espresso will wait for the app to be "idle". - * - * Proper use of the BusyWaiter will avoid having to "wait" or "sleep" in tests. - * Be sure not be "busy" langer than necessary, otherwise it will slow down your tests. - */ -class BusyWaiter private constructor(private val completedOnExecutionThread: ExecutionThread) { - - @GuardedBy("lock") - private val operationsInProgress = com.moka.waiter.android.internal.SetMultiMap() - @GuardedBy("lock") - private val currentlyTrackedCategories = EnumSet.allOf(Category::class.java) - @GuardedBy("lock") - private val noLongerBusyCallbacks = ArrayList(4) - private val lock = ReentrantLock() - private val defaultCategory = Category.GENERAL - - val name: String - get() { - lock.lock() - try { - return String.format(this.javaClass.simpleName + "@%d with operations: %s", identityHashCode(this), operationsInProgress) - } finally { - lock.unlock() - } - } - - val isNotBusy: Boolean - get() { - lock.lock() - try { - for (currentlyTrackedOperation in currentlyTrackedCategories) { - if (isBusyWith(currentlyTrackedOperation)) { - return false - } - } - return true - } finally { - lock.unlock() - } - } - - /** - * Record the start of an async operation. - * - * @param operation An object that identifies the request. Must have a correct equals()/hashCode(). - */ - fun busyWith(operation: Any) { - busyWith(operation, defaultCategory) - } - - /** - * Record the start of an async operation. - * - * @param operation An object that identifies the request. Must have a correct equals()/hashCode(). - */ - fun busyWith(operation: Any, category: Category) { - lock.lock() - val wasAdded: Boolean - try { - wasAdded = operationsInProgress.add(category, operation) - if (wasAdded) { - Timber.i("busyWith -> [$operation] was added to active operations") - } - } finally { - lock.unlock() - } - } - - fun registerNoLongerBusyCallback(noLongerBusyCallback: NoLongerBusyCallback) { - lock.lock() - try { - noLongerBusyCallbacks.add(noLongerBusyCallback) - } finally { - lock.unlock() - } - } - - fun payAttentionToCategory(category: Category) { - lock.lock() - try { - currentlyTrackedCategories.add(category) - } finally { - lock.unlock() - } - } - - fun ignoreCategory(category: Category) { - lock.lock() - try { - currentlyTrackedCategories.remove(category) - } finally { - lock.unlock() - } - } - - fun completedEverythingInCategory(category: Category) { - lock.lock() - try { - val iterator = operationsInProgress.valuesIterator(category) - while (iterator.hasNext()) { - val next = iterator.next() - completed(next, iterator) - } - } finally { - lock.unlock() - } - } - - fun completedEverything() { - lock.lock() - try { - val iterator = operationsInProgress.valuesIterator() - while (iterator.hasNext()) { - val next = iterator.next() - completed(next, iterator) - } - } finally { - lock.unlock() - } - } - - fun completedEverythingMatching(matcher: OperationMatcher) { - lock.lock() - try { - val iterator = operationsInProgress.valuesIterator() - while (iterator.hasNext()) { - val next = iterator.next() - if (matcher.matches(next)) { - completed(next, iterator) - } - } - } finally { - lock.unlock() - } - } - - /** - * Marks an operation as complete, using the "completedOnExecutionThread" for this BusyWaiter instance. - */ - fun postCompleted(operationInProgress: Any) { - completedOnExecutionThread.execute(object : Runnable { - override fun run() { - completed(operationInProgress) - } - - override fun toString(): String { - return "completed($operationInProgress)" - } - }) - } - - /** - * Marks an object as completed ( immediately on the current ExecutionThread ) - * - * @param operationInProgress an object that was passed to busyWith - */ - fun completed(operationInProgress: Any) { - completed(operationInProgress, null) - } - - private fun completed(operation: Any?, iterator: MutableIterator?) { - if (operation == null) { - throw NullPointerException("null can not be `completed` null, operation must be non-null") - } - lock.lock() - val wasRemoved: Boolean - try { - if (iterator != null) { - // if the collection is being iterated, - // then we HAVE to use the iterator for removal to avoid ConcurrentModificationException - iterator.remove() - wasRemoved = true - } else { - wasRemoved = operationsInProgress.removeValue(operation) - } - if (wasRemoved) { - Timber.i("completed -> [${operation}] was removed from active operations") - } - if (wasRemoved && isNotBusy) { - for (noLongerBusyCallback in noLongerBusyCallbacks) { - Timber.i("All operations are now finished, we are now idle") - noLongerBusyCallback.noLongerBusy() - } - } - } finally { - lock.unlock() - } - } - - private fun isBusyWith(currentlyTrackedOperation: Category): Boolean { - lock.lock() - try { - return !operationsInProgress.values(currentlyTrackedOperation).isEmpty() - } finally { - lock.unlock() - } - } - - fun toVerboseString(): String { - try { - lock.lock() - val operations = operationsInProgress - val sb = StringBuilder() - .append("\n=====================**") - .append("\n BusyWaiter Information ") - .append("\n=====================**") - try { - sb.append("\nTotal Operations:") - .append(operations.allValues().size) - .append("\nList of operations in progress:") - .append("\n===========================*") - for (category in operations.allKeys()) { - sb.append("\nCATEGORY: ======= ").append(category.name).append(" =======") - for (operation in operations.values(category)) { - sb.append("\n").append(operation.toString()) - } - } - } catch (e: Exception) { - sb.append(e.message) - sb.append("\n===* FAILED to get list of progress operations ===*") - } - - return sb.append("\n===========================*\n").toString() - } finally { - lock.unlock() - } - } - - enum class Category { - GENERAL, - NETWORK, - DIALOG - } - - interface NoLongerBusyCallback { - fun noLongerBusy() - } - - interface OperationMatcher { - fun matches(o: Any): Boolean - } - - interface ExecutionThread { - - fun execute(runnable: Runnable) - - companion object { - val IMMEDIATE: ExecutionThread = object : ExecutionThread { - override fun execute(runnable: Runnable) { - runnable.run() - } - } - } - } - - companion object { - fun withOperationsCompletedOn(completedOnExecutionThread: ExecutionThread): BusyWaiter { - return BusyWaiter(completedOnExecutionThread) - } - } - -} diff --git a/moka/src/main/java/com/moka/waiter/android/BusyWaiterIdlingResource.java b/moka/src/main/java/com/moka/waiter/android/BusyWaiterIdlingResource.java deleted file mode 100644 index cdc08a2..0000000 --- a/moka/src/main/java/com/moka/waiter/android/BusyWaiterIdlingResource.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.moka.waiter.android; - -import androidx.test.espresso.IdlingResource; - -/** - *

- * This class is a bridge between espresso's IdlingResource and the app, so the app doesn't have to depend on espresso. - *

- * See here: - * https://code.google.com/p/android-test-kit/wiki/EspressoSamples#Using_registerIdlingResource_to_synchronize_with_custom_resource - * and here: - * https://code.google.com/p/android-test-kit/source/browse/testapp_test/src/main/java/com/google/android/apps/common/testing/ui/testapp/AdvancedSynchronizationTest.java - */ -public class BusyWaiterIdlingResource implements IdlingResource { - - private final BusyWaiter busyWaiter; - - public BusyWaiterIdlingResource(final BusyWaiter busyWaiter) { - this.busyWaiter = busyWaiter; - } - - @Override - public String getName() { - return busyWaiter.getName(); - } - - @Override - public boolean isIdleNow() { - return busyWaiter.isNotBusy(); - } - - @Override - public void registerIdleTransitionCallback(final ResourceCallback resourceCallback) { - busyWaiter.registerNoLongerBusyCallback(new BusyWaiter.NoLongerBusyCallback() { - @Override - public void noLongerBusy() { - resourceCallback.onTransitionToIdle(); - } - }); - } -} \ No newline at end of file diff --git a/moka/src/main/java/com/moka/waiter/android/internal/SetMultiMap.java b/moka/src/main/java/com/moka/waiter/android/internal/SetMultiMap.java deleted file mode 100644 index 205063e..0000000 --- a/moka/src/main/java/com/moka/waiter/android/internal/SetMultiMap.java +++ /dev/null @@ -1,200 +0,0 @@ -package com.moka.waiter.android.internal; - -import androidx.annotation.Nullable; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; - -import static java.util.Collections.emptySet; -import static java.util.Collections.unmodifiableSet; - -/** - * Simple SetMultiMap designed for the needs of BusyWaiter. - * I didn't want to have BusyWaiter depend on guava, so guava's Setmultimap wasn't an option. - * Builds on top of HashMap/HashSet's internally. - *

- * This collection maps keys of type K to sets of values of type V. - * Each value is unique across all keys in the collection. - *

- * Not the most efficient implementation, but good enough for the BusyWaiter - *

- * WARNING: this Impl has some weird quirks, so probably not good for general purpose use. - * Biggest quirk is the at each value is unique across all keys. - * if you want to change the key for a value, you must remove it and re-add it. - *

- */ - -public class SetMultiMap { - - private HashMap> map = new HashMap<>(); - private HashMap reverseMap = new HashMap<>(); - - /** - * All keys are unique and all values are unique. - * If the Value already exists under any Key, then it will not be added again. - *

- * WARNING: - * To re-associate with another Key, first `removeValue` then re-add the Value - * Else this method will throw an exception. - * - * @return if a new entry was added. - */ - public boolean add(K key, V value) throws IllegalStateException { - if (reverseMap.get(value) != null && reverseMap.get(value) != key) { - throw new IllegalStateException("You can't insert the same value for 2 different keys.\n" - + "This mapping already exists: \n'" + reverseMap.get(value) + "' =>\n" - + " '" + value + "'\nbut you tried to add this new mapping: \n" - + "'" + key + "' =>\n '" + value + "'\nRemove the old mapping first!"); - } - - if (reverseMap.get(value) == key) { - return false; - } - - Set valueSet = map.get(key); - if (valueSet == null) { - valueSet = new HashSet<>(); - map.put(key, valueSet); - } - valueSet.add(value); - reverseMap.put(value, key); - return true; - } - - /** - * Each value ONLY appears once, so it is associated with at most one key. - * - * @return key for which the value is associated with. - */ - @Nullable - public K keyFor(V value) { - return reverseMap.get(value); - } - - /** - * Removes the value if and only if it exists in the map. - * - * @return true if an entry was removed. - */ - public boolean removeValue(V value) { - if (reverseMap.containsKey(value)) { - final K keyForRemoved = reverseMap.remove(value); - map.get(keyForRemoved).remove(value); - return true; - } - return false; - } - - /** - * @return provides an iterator ( with remove support ) for all the values in the collection. - */ - public Iterator valuesIterator() { - final Iterator> valueIterator = reverseMap.entrySet().iterator(); - return multiMapIteratorFromReverseMapIterator(valueIterator); - } - - private Iterator multiMapIteratorFromReverseMapIterator(final Iterator> valueIterator) { - return new Iterator() { - private Map.Entry mapEntry; - - @Override - public boolean hasNext() { - return valueIterator.hasNext(); - } - - @Override - public V next() { - mapEntry = valueIterator.next(); - return mapEntry.getKey(); - } - - @Override - public void remove() { - valueIterator.remove(); - map.get(mapEntry.getValue()).remove(mapEntry.getKey()); - } - }; - } - - private Iterator multiMapIteratorFromForwardMapIterator(final Iterator valueIterator) { - return new Iterator() { - private V nextValue; - - @Override - public boolean hasNext() { - return valueIterator.hasNext(); - } - - @Override - public V next() { - nextValue = valueIterator.next(); - return nextValue; - } - - @Override - public void remove() { - valueIterator.remove(); - reverseMap.remove(nextValue); - } - }; - } - - /** - * @return All the keys that has ever been used in this map since its creation - */ - public Set allKeys() { - return unmodifiableSet(map.keySet()); - } - - /** - * @return All values across all keys - */ - public Set allValues() { - return unmodifiableSet(reverseMap.keySet()); - } - - /** - * @return Values for the given key - */ - public Set values(K key) { - final Set values = map.get(key); - if (values == null) { - return emptySet(); - } - return unmodifiableSet(values); - } - - /** - * @return True if and only if there are no values ( there may be keys ) - */ - public boolean hasNoValues() { - return reverseMap.isEmpty(); - } - - public Iterator valuesIterator(final K key) { - Set values = map.get(key); - if (values == null) { - values = emptySet(); - } - final Iterator valueIterator = values.iterator(); - return multiMapIteratorFromForwardMapIterator(valueIterator); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("SetMultiMap{\n"); - for (Map.Entry> entry : map.entrySet()) { - sb.append(" '").append(entry.getKey()).append("' =>\n"); - for (V value : entry.getValue()) { - sb.append(" '").append(value).append("'\n"); - } - } - sb.append("}"); - - return sb.toString(); - } -} diff --git a/moka/src/main/java/com/moka/waiter/android/pager/BusyWaiterPageChangeListener.java b/moka/src/main/java/com/moka/waiter/android/pager/BusyWaiterPageChangeListener.java deleted file mode 100644 index 103d677..0000000 --- a/moka/src/main/java/com/moka/waiter/android/pager/BusyWaiterPageChangeListener.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.moka.waiter.android.pager; - -import androidx.viewpager.widget.ViewPager; - -import com.moka.waiter.android.BusyWaiter; - -public class BusyWaiterPageChangeListener extends ViewPager.SimpleOnPageChangeListener { - private BusyWaiter busyWaiter; - - public BusyWaiterPageChangeListener(BusyWaiter busyWaiter) { - this.busyWaiter = busyWaiter; - } - - @Override - public void onPageScrollStateChanged(final int state) { - super.onPageScrollStateChanged(state); - if (state != ViewPager.SCROLL_STATE_IDLE) { - busyWaiter.busyWith(this); - } else { - busyWaiter.completed(this); - } - } -} diff --git a/moka/src/main/java/com/moka/waiter/android/testsupport/BusyWaiterExecutor.kt b/moka/src/main/java/com/moka/waiter/android/testsupport/BusyWaiterExecutor.kt deleted file mode 100644 index 35631b4..0000000 --- a/moka/src/main/java/com/moka/waiter/android/testsupport/BusyWaiterExecutor.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.moka.waiter.testsupport - -import com.moka.waiter.android.BusyWaiter -import com.moka.waiter.android.BusyWaiter.Category.NETWORK -import timber.log.Timber -import java.lang.Thread.currentThread -import java.util.concurrent.Executor -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors.newFixedThreadPool - -/** - * This is an implementation of the Executor interface that will track operations - * using the BusyWaiter. I.e. when this executor is executing something, BusyWaiter - * will appear to reflect the operation in progress. - * - * - * Espresso will wait until there are not active tasks on this executor. - */ -class BusyWaiterExecutor(private val busyWaiter: BusyWaiter, private val mExecutorToNotifyFinishOperationsOn: Executor) : Executor { - - private val delegate: ExecutorService = newFixedThreadPool(4) - - override fun execute(command: Runnable) { - val trackerObj = Any() - Timber.i("Starting $command on thread ${currentThread()}") - busyWaiter.busyWith(trackerObj, NETWORK) - delegate.execute { - try { - command.run() - } finally { - // This allows you to finish on another queue ( e.g. the main looper on android ) - mExecutorToNotifyFinishOperationsOn.execute { - Timber.i("Finishing $command on thread ${currentThread()}") - busyWaiter.completed(trackerObj) - } - } - } - } - -} diff --git a/moka/src/main/java/com/moka/waiter/android/webview/BusyWaiterWebViewClient.java b/moka/src/main/java/com/moka/waiter/android/webview/BusyWaiterWebViewClient.java deleted file mode 100644 index e57a323..0000000 --- a/moka/src/main/java/com/moka/waiter/android/webview/BusyWaiterWebViewClient.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.moka.waiter.android.webview; - -import android.annotation.TargetApi; -import android.graphics.Bitmap; -import android.net.http.SslError; -import android.os.Build; -import android.os.Message; -import android.view.KeyEvent; -import android.webkit.ClientCertRequest; -import android.webkit.HttpAuthHandler; -import android.webkit.SslErrorHandler; -import android.webkit.WebResourceError; -import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; -import android.webkit.WebView; -import android.webkit.WebViewClient; - -import androidx.annotation.RequiresApi; - -import com.moka.waiter.android.BusyWaiter; - -@SuppressWarnings("deprecation") // need to delegate to deprecated methods. -public class BusyWaiterWebViewClient extends WebViewClient { - - private final BusyWaiter busyWaiter; - private final WebViewClient delegate; - - private BusyWaiterWebViewClient(final BusyWaiter busyWaiter, final WebViewClient delegate) { - this.busyWaiter = busyWaiter; - this.delegate = delegate; - } - - public static class Builder { - private BusyWaiter busyWaiter1; - private WebViewClient delegate; - - public Builder(final BusyWaiter busyWaiter) { - this.busyWaiter1 = busyWaiter; - } - - public Builder wrapWebViewClient(final WebViewClient delegate) { - this.delegate = delegate; - return this; - } - - public BusyWaiterWebViewClient build() { - return new BusyWaiterWebViewClient(busyWaiter1, delegate); - } - } - - public static Builder with(BusyWaiter busyWaiter) { - return new Builder(busyWaiter); - } - - @Override - public void onPageStarted(final WebView view, final String url, final Bitmap favicon) { - busyWaiter.busyWith(view, BusyWaiter.Category.NETWORK); - delegate.onPageStarted(view, url, favicon); - } - - @Override - public void onPageFinished(final WebView view, final String url) { - delegate.onPageFinished(view, url); - busyWaiter.completed(view); - } - - @Override - public void onReceivedError(final WebView view, final int errorCode, final String description, final String failingUrl) { - delegate.onReceivedError(view, errorCode, description, failingUrl); - busyWaiter.completed(view); - } - - @Override - public void onReceivedSslError(final WebView view, final SslErrorHandler handler, final SslError error) { - delegate.onReceivedSslError(view, handler, error); - busyWaiter.completed(view); - } - - @Override - public boolean shouldOverrideUrlLoading(final WebView view, final String url) { - return delegate.shouldOverrideUrlLoading(view, url); - } - - @RequiresApi(api = Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(final WebView view, final WebResourceRequest request) { - return delegate.shouldOverrideUrlLoading(view, request); - } - - @Override - public void onLoadResource(final WebView view, final String url) { - delegate.onLoadResource(view, url); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - @Override - public void onPageCommitVisible(final WebView view, final String url) { - delegate.onPageCommitVisible(view, url); - } - - @Override - public WebResourceResponse shouldInterceptRequest(final WebView view, final String url) { - return delegate.shouldInterceptRequest(view, url); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public WebResourceResponse shouldInterceptRequest(final WebView view, final WebResourceRequest request) { - return delegate.shouldInterceptRequest(view, request); - } - - @Override - public void onTooManyRedirects(final WebView view, final Message cancelMsg, final Message continueMsg) { - delegate.onTooManyRedirects(view, cancelMsg, continueMsg); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - @Override - public void onReceivedError(final WebView view, final WebResourceRequest request, final WebResourceError error) { - delegate.onReceivedError(view, request, error); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - @Override - public void onReceivedHttpError(final WebView view, final WebResourceRequest request, final WebResourceResponse errorResponse) { - delegate.onReceivedHttpError(view, request, errorResponse); - } - - @Override - public void onFormResubmission(final WebView view, final Message dontResend, final Message resend) { - delegate.onFormResubmission(view, dontResend, resend); - } - - @Override - public void doUpdateVisitedHistory(final WebView view, final String url, final boolean isReload) { - delegate.doUpdateVisitedHistory(view, url, isReload); - } - - @Override - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public void onReceivedClientCertRequest(final WebView view, final ClientCertRequest request) { - delegate.onReceivedClientCertRequest(view, request); - } - - @Override - public void onReceivedHttpAuthRequest(final WebView view, final HttpAuthHandler handler, final String host, final String realm) { - delegate.onReceivedHttpAuthRequest(view, handler, host, realm); - } - - @Override - public boolean shouldOverrideKeyEvent(final WebView view, final KeyEvent event) { - return delegate.shouldOverrideKeyEvent(view, event); - } - - @Override - public void onUnhandledKeyEvent(final WebView view, final KeyEvent event) { - delegate.onUnhandledKeyEvent(view, event); - } - - @Override - public void onScaleChanged(final WebView view, final float oldScale, final float newScale) { - delegate.onScaleChanged(view, oldScale, newScale); - } - - @Override - public void onReceivedLoginRequest(final WebView view, final String realm, final String account, final String args) { - delegate.onReceivedLoginRequest(view, realm, account, args); - } -} diff --git a/moka/src/test/java/com/moka/BusyWaiterTest.kt b/moka/src/test/java/com/moka/BusyWaiterTest.kt deleted file mode 100644 index 6607b5f..0000000 --- a/moka/src/test/java/com/moka/BusyWaiterTest.kt +++ /dev/null @@ -1,123 +0,0 @@ -package com.moka - -import com.moka.waiter.android.BusyWaiter -import com.moka.waiter.android.BusyWaiter.Category.NETWORK -import com.moka.waiter.android.BusyWaiter.ExecutionThread.Companion.IMMEDIATE -import org.assertj.core.api.Java6Assertions.assertThat -import org.junit.Assert.* -import org.junit.Before -import org.junit.Test - -class BusyWaiterTest { - - private lateinit var busyWaiter: BusyWaiter - - @Before - @Throws(Exception::class) - fun setUp() { - busyWaiter = BusyWaiter.withOperationsCompletedOn(IMMEDIATE) - } - - @Test - @Throws(Exception::class) - fun whenBusy_thenIsNotBusyReturnsFalse() { - busyWaiter.busyWith(this) - assertIsBusy(busyWaiter) - } - - @Test - @Throws(Exception::class) - fun whenNetworkBusyAndNotTrackingNetwork_thenIsNotBusy() { - busyWaiter.busyWith(this, NETWORK) - busyWaiter.ignoreCategory(NETWORK) - - // should not be busy because we aren't tracking network requests - assertNotBusy(busyWaiter) - } - - @Test - @Throws(Exception::class) - fun whenNetworkBusyAndPayAttention_thenIsBusy() { - busyWaiter.busyWith(this, NETWORK) - busyWaiter.ignoreCategory(NETWORK) - busyWaiter.payAttentionToCategory(NETWORK) - - assertIsBusy(busyWaiter) - } - - @Test - @Throws(Exception::class) - fun whenNetworkBusyAndCompletedNetwork_thenIsNotBusy() { - busyWaiter.busyWith(this, NETWORK) - busyWaiter.completedEverythingInCategory(NETWORK) - - assertNotBusy(busyWaiter) - } - - - @Test - @Throws(Exception::class) - fun whenCompletedNetwork_thenIsNotBusy() { - busyWaiter.completedEverythingInCategory(NETWORK) - - assertNotBusy(busyWaiter) - } - - @Test - @Throws(Exception::class) - fun whenNetworkBusy_thenIsNotBusyReturnsFalse() { - busyWaiter.busyWith(this, NETWORK) - - assertIsBusy(busyWaiter) - } - - @Test - @Throws(Exception::class) - fun whenCompleted_thenIsNotBusyReturnsTrue() { - busyWaiter.busyWith(this) - busyWaiter.completed(this) - - assertNotBusy(busyWaiter) - } - - @Test - @Throws(Exception::class) - fun whenBusyWithSomething_thenNameIncludesSomething() { - busyWaiter.busyWith("some thing") - assertThat(busyWaiter.name).contains("some thing") - } - - @Test - @Throws(Exception::class) - fun whenCompletedEverything_thenIsNotBusyReturnsTrue() { - busyWaiter.busyWith(this) - busyWaiter.busyWith(Any()) - busyWaiter.busyWith(Any()) - busyWaiter.completedEverything() - assertNotBusy(busyWaiter) - } - - @Test - @Throws(Exception::class) - fun whenCompletedOnlySome_thenStillBusy() { - val o1 = Any() - val o2 = Any() - assertNotBusy(busyWaiter) - busyWaiter.busyWith(o1) - assertIsBusy(busyWaiter) - busyWaiter.busyWith(o2) - assertIsBusy(busyWaiter) - busyWaiter.completed(o2) - assertIsBusy(busyWaiter) - busyWaiter.completed(o1) - assertNotBusy(busyWaiter) - } - - private fun assertNotBusy(busyWaiter: BusyWaiter) { - assertTrue(busyWaiter.isNotBusy) - } - - private fun assertIsBusy(busyWaiter: BusyWaiter) { - assertFalse(busyWaiter.isNotBusy) - } -} diff --git a/moka/src/test/java/com/moka/MultiMapTest.kt b/moka/src/test/java/com/moka/MultiMapTest.kt deleted file mode 100644 index 32d8a96..0000000 --- a/moka/src/test/java/com/moka/MultiMapTest.kt +++ /dev/null @@ -1,246 +0,0 @@ -package com.moka - -import com.moka.waiter.android.internal.SetMultiMap -import org.assertj.core.api.Java6Assertions.assertThat -import org.assertj.core.api.Java6Assertions.fail -import org.junit.Before -import org.junit.Test - -class MultiMapTest { - - private lateinit var map: SetMultiMap - - @Before - @Throws(Exception::class) - fun setUp() { - map = SetMultiMap() - } - - @Test - @Throws(Exception::class) - fun whenValueAdd_thenCanBeRetrieved() { - val map = this.map - map.add(1, "1") - map.add(1, "2") - map.add(2, "4") - map.add(2, "3") - - assertThat(map.values(1)).containsExactlyInAnyOrder("1", "2") - assertThat(map.values(2)).containsExactlyInAnyOrder("4", "3") - assertThat(map.allValues()).containsExactlyInAnyOrder("1", "2", "3", "4") - } - - @Test - @Throws(Exception::class) - fun whenValueAddedFirstTime_thenReturnTrue() { - val wasAdded = map.add(1, "2") - - assertThat(wasAdded).isTrue() - } - - @Test - @Throws(Exception::class) - fun whenKeyValueAddedSecondTime_thenReturnFalse() { - map.add(2, "2") - val resultForSecondAdd = map.add(2, "2") - - assertThat(resultForSecondAdd).isFalse() - } - - @Test - @Throws(Exception::class) - fun whenNoValues_thenReturnEmptySet() { - assertThat(map.values(1)).isEmpty() - } - - @Test - @Throws(Exception::class) - fun whenKeyFor_thenReturnValue() { - map.add(1, "1") - map.add(2, "3") - map.add(1, "2") - - assertThat(map.keyFor("1")).isEqualTo(1) - } - - - @Test - @Throws(Exception::class) - fun whenAddSameValue_thenThrowException() { - try { - map.add(1, "1") - map.add(2, "1") - fail("Adding the same value twice must throw an exception") - } catch (e: IllegalStateException) { - // expected - } - - } - - @Test - @Throws(Exception::class) - fun whenRemoveValue_thenCanAddBackWithDifferentKey() { - map.add(1, "1") - map.add(1, "3") - map.removeValue("1") - map.add(2, "1") - map.add(1, "2") - - assertThat(map.keyFor("1")).isEqualTo(2) - } - - @Test - @Throws(Exception::class) - fun whenRemoveTwice_thenReturnFalseOnSecondTime() { - map.add(1, "1") - map.add(2, "2") - val resultWhenValuePresent = map.removeValue("1") - val resultAfterValueRemoved = map.removeValue("1") - - assertThat(resultWhenValuePresent).isTrue() - assertThat(resultAfterValueRemoved).isFalse() - } - - @Test - @Throws(Exception::class) - fun whenAllKeys_thenAllKeysArePresent() { - map.add(1, "1") - map.add(2, "2") - map.add(2, "3") - - assertThat(map.allKeys()).containsExactlyInAnyOrder(1, 2) - } - - @Test - @Throws(Exception::class) - fun whenAllValues_thenAllValuesArePresent() { - map.add(1, "1") - map.add(2, "2") - map.add(2, "3") - - assertThat(map.allValues()).containsExactlyInAnyOrder("1", "2", "3") - } - - @Test - @Throws(Exception::class) - fun valuesReturnsOnlyValuesForTheSpecifiedKey() { - map.add(1, "1") - map.add(2, "2") - map.add(2, "3") - - assertThat(map.values(2)).containsExactlyInAnyOrder("2", "3") - } - - @Test - @Throws(Exception::class) - fun whenAllValuesRemoved_ThenIsEmpty() { - map.add(1, "1") - map.removeValue("1") - - assertThat(map.hasNoValues()).isTrue() - } - - @Test - @Throws(Exception::class) - fun whenHasValue_ThenNotIsEmpty() { - map.add(1, "1") - - assertThat(map.hasNoValues()).isFalse() - } - - @Test - @Throws(Exception::class) - fun whenIterateRemove_ThenIsActuallyRemoved() { - map.add(1, "A") - map.add(1, "B") - map.add(2, "C") - map.add(2, "C") - map.add(3, "E") - map.add(3, "D") - - val stringIterator = map.valuesIterator() - while (stringIterator.hasNext()) { - val next = stringIterator.next() - if (next == "C") { - stringIterator.remove() - } - } - - assertThat(map.allValues()).containsExactlyInAnyOrder("A", "B", "D", "E") - assertThat(map.allKeys()).containsExactlyInAnyOrder(1, 2, 3) - assertThat(map.values(2)).isEmpty() - } - - @Test - @Throws(Exception::class) - fun whenIterateValuesForKeyAndRemove_ThenIsActuallyRemoved() { - map.add(1, "A") - map.add(1, "B") - map.add(2, "X") - map.add(2, "Y") - map.add(3, "E") - map.add(3, "C") - map.add(3, "D") - - val stringIterator = map.valuesIterator(3) - while (stringIterator.hasNext()) { - val next = stringIterator.next() - if (next == "C") { - stringIterator.remove() - } - } - - assertThat(map.allValues()).containsExactlyInAnyOrder("A", "B", "X", "E", "Y", "D") - assertThat(map.allKeys()).containsExactlyInAnyOrder(1, 2, 3) - assertThat(map.values(3)).containsExactlyInAnyOrder("D", "E") - } - - @Test - @Throws(Exception::class) - fun whenIterateValuesForKeyWithNoValues_thenNoExceptionThrown() { - map.add(1, "A") - map.add(1, "B") - map.add(2, "X") - map.add(2, "Y") - map.add(3, "E") - map.add(3, "C") - map.add(3, "D") - - val stringIterator = map.valuesIterator(4) - while (stringIterator.hasNext()) { - val next = stringIterator.next() - if (next == "C") { - stringIterator.remove() - } - } - - assertThat(map.allValues()).containsExactlyInAnyOrder("A", "C", "B", "X", "E", "Y", "D") - assertThat(map.allKeys()).containsExactlyInAnyOrder(1, 2, 3) - assertThat(map.values(3)).containsExactlyInAnyOrder("D", "E", "C") - } - - @Test - @Throws(Exception::class) - fun toString_thenPrintSomethingReasonable() { - map.add(1, "A") - map.add(1, "B") - map.add(2, "X") - map.add(2, "Y") - map.add(3, "E") - map.add(3, "C") - map.add(3, "D") - - assertThat(map.toString()).isEqualTo("SetMultiMap{\n" - + " '1' =>\n" - + " 'A'\n" - + " 'B'\n" - + " '2' =>\n" - + " 'X'\n" - + " 'Y'\n" - + " '3' =>\n" - + " 'C'\n" - + " 'D'\n" - + " 'E'\n" - + "}") - } -} diff --git a/sample/build.gradle b/sample/build.gradle index 440d275..aed81bf 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -15,8 +15,7 @@ android { applicationId "com.sample.app" versionCode 1 versionName "1.0" - testInstrumentationRunner "com.sample.app.SampleAndroidJUnitRunner" -// testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments useTestStorageService: 'true' } buildTypes { diff --git a/sample/src/androidTest/java/com/sample/app/RecyclerViewTest.kt b/sample/src/androidTest/java/com/sample/app/RecyclerViewTest.kt index 6f4ccdd..a33444c 100644 --- a/sample/src/androidTest/java/com/sample/app/RecyclerViewTest.kt +++ b/sample/src/androidTest/java/com/sample/app/RecyclerViewTest.kt @@ -1,9 +1,9 @@ package com.sample.app +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.moka.EspressoMoka.onView import com.moka.lib.assertions.MatchOperator import com.moka.lib.assertions.RecyclerViewItemCountAssertion import org.junit.Rule diff --git a/sample/src/androidTest/java/com/sample/app/SampleAndroidJUnitRunner.kt b/sample/src/androidTest/java/com/sample/app/SampleAndroidJUnitRunner.kt deleted file mode 100644 index 847dfdc..0000000 --- a/sample/src/androidTest/java/com/sample/app/SampleAndroidJUnitRunner.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.sample.app - -import android.os.Bundle -import androidx.test.espresso.IdlingPolicies -import androidx.test.runner.AndroidJUnitRunner -import com.moka.EspressoMokaRunner -import com.moka.lib.debug.DebugTrace.beginSection -import com.moka.lib.debug.DebugTrace.endSection -import timber.log.Timber -import java.util.concurrent.TimeUnit - -class SampleAndroidJUnitRunner : AndroidJUnitRunner() { - override fun onCreate(arguments: Bundle) { - EspressoMokaRunner.onCreate(arguments) - super.onCreate(arguments) - IdlingPolicies.setMasterPolicyTimeout(15, TimeUnit.SECONDS) - IdlingPolicies.setIdlingResourceTimeout(15, TimeUnit.SECONDS) - } - - override fun onStart() { - EspressoMokaRunner.onStart() - super.onStart() - } - - override fun waitForIdleSync() { - super.waitForIdleSync() - EspressoMokaRunner.waitForIdleSync() - } - - override fun onException(obj: Any, e: Throwable): Boolean { - beginSection(e.message + " SampleAndroidJUnitRunner.onException") - return try { - super.onException(obj, e) - Timber.e(e, "%s", obj.toString()) - EspressoMokaRunner.onException(obj, e) - } finally { - endSection() - } - } -} \ No newline at end of file