8

I'm working with some simple tests with Espresso. One of them is about click on view and check if a Dialog is showed.

My problem is that sometimes works, and sometimes not. It only work always if I put a sleep before check the dialog. Any solution without ussing sleep?

Here is my code (so simple):

onView(withId(R.id.forgot_password)).perform(click());
// only works if use Thread.sleep(ms) here
onView(withText(R.string.reset_password)).check(matches(isDisplayed()));

Edit:

I'm showing dialog with static helpers, but the simplification is this. And I'm not performing any background task in the middle.

final TextInputDialog textInputDialog = new

TextInputDialog.Builder(context)
                .setTitle(titleId)
                .setInputType(inputType)
                .setHint(hintId)
                .setPreFilledText(preFilledText)
                .setNegativeButton(R.string.cancel, null)
                .setPositiveButton(positiveButtonId, onTextSubmittedListener)
                .create();
textInputDialog.show(textInputDialog);

Thank you!

adalpari
  • 3,052
  • 20
  • 39
  • I don't see why sleep is not a good solution. You want to wait for a set period of time, and that exactly what sleep does. – MoGa Mar 13 '17 at 15:00
  • 1
    Does that click event immediately show dialog, or it performs some background job and then shows dialog? – azizbekian Mar 13 '17 at 15:31
  • 1
    thread.sleep is not a good solution in android espresso tests. and normally it should not be necessary, espresso waits until the mainthread is finished - so it should wait for the dialog. can you share how you display the dialog? – stamanuel Mar 13 '17 at 15:44
  • I've updated my answer. I just check some variables and editTexts and show the dialog. – adalpari Mar 13 '17 at 16:12
  • 1
    the code you provided looks fine, so the error has to be somewhere else. maybe in where and how you exactly call the dialog to show. also what is the error message if the test fails? and how often does it fail/succeed? – stamanuel Mar 14 '17 at 14:52

3 Answers3

6

In some cases disable animations is not possible like:

  • when running tests on cloud devices. Firebase testlab doesn't allow changing the device configuration
  • when you are waiting for some background thread to update the user interface, like the an http response.

In those cases the simplest is to wait for an element to show. Here is how:

Simple helper:

class WaifForUIUpdate {

public static void waifForWithId(@IdRes int stringId) {
    
    ViewInteraction element;
    do {
        waitFor(500);

        //simple example using withText Matcher.
        element = onView(withText(stringId));

    } while (!MatcherExtension.exists(element));

}

static void waitFor(int ms) {
    final CountDownLatch signal = new CountDownLatch(1);

    try {
        signal.await(ms, TimeUnit.MILLISECONDS);
    } catch (InterruptedException e) {
        Assert.fail(e.getMessage());
    }
}
}

Matcher from twisterrob

public class MatcherExtension {
 @CheckResult
    public static boolean exists(ViewInteraction interaction) {
        try {
            interaction.perform(new ViewAction() {
                @Override
                public Matcher<View> getConstraints() {
                    return any(View.class);
                }

                @Override
                public String getDescription() {
                    return "check for existence";
                }

                @Override
                public void perform(UiController uiController, View view) {
                    // no op, if this is run, then the execution will continue after .perform(...)
                }
            });
            return true;
        } catch (AmbiguousViewMatcherException ex) {
            // if there's any interaction later with the same matcher, that'll fail anyway
            return true; // we found more than one
        } catch (NoMatchingViewException ex) {
            return false;
        } catch (NoMatchingRootException ex) {
            // optional depending on what you think "exists" means
            return false;
        }
    }

}

Usage:

WaifForUIUpdate.waifForWithId(R.string.some_string);
//now do your validations
Rafael Ruiz Muñoz
  • 5,333
  • 6
  • 46
  • 92
MiguelSlv
  • 14,067
  • 15
  • 102
  • 169
  • This is working in the case where element exist, how do i break statement , after trying for 50 seconds .. In case where element does not exist ..it does not exit from do while loop .. can you help me with the above question – zzz Oct 18 '18 at 10:45
  • I added a second answer. This one is easy to change into what ever you need. – MiguelSlv Oct 18 '18 at 21:47
4

Finally it seems like the problem was animations. For make Espresso works properly is needed to disable animations in developer options menu.

Disable animations

In this case, the problem was solved. But there are other cases in which the problem could be a background task, like the comments to my question suggest. So I recommend to have a look at IdlingResource https://medium.com/azimolabs/wait-for-it-idlingresource-and-conditionwatcher-602055f32356#.pw55uipfj or this Espresso: Thread.sleep( );

Community
  • 1
  • 1
adalpari
  • 3,052
  • 20
  • 39
0

I ended up using another approach different from my first answer that works good too but it is more easy to adapt to anything, by using a CountdownLatch. Here is the code:

public static boolean viewExists(final Matcher<View> viewMatcher, final long millis) throws InterruptedException {
    final Boolean[] found = new Boolean[1];

    final CountDownLatch latch = new CountDownLatch(1);
    ViewAction action = new ViewAction() {
        @Override
        public Matcher<View> getConstraints() {
            return isRoot();
        }

        @Override
        public String getDescription() {
            return "wait for a specific view with id <" + viewMatcher.toString() + "> during " + millis + " millis.";
        }

        @Override
        public void perform(final UiController uiController, final View view) {
            uiController.loopMainThreadUntilIdle();
            final long startTime = System.currentTimeMillis();
            final long endTime = startTime + millis;


            do {
                for (View child : TreeIterables.breadthFirstViewTraversal(view)) {

                    if (viewMatcher.matches(child)) {
                        Log.d(TAG, "perform: found match");
                        found[0] = true;
                        latch.countDown();
                        return;
                    }
                }

                uiController.loopMainThreadForAtLeast(50);
            }
            while (System.currentTimeMillis() < endTime);

            found[0] = false;
            latch.countDown();
        }
    };
    onView(isRoot()).perform(action);

    latch.await();
    return found[0];
}

Google approach is to use Idling resource classes, but requires to insert test code into production apk, or use flavors and Dependency Injection pattern to avoid it.

MiguelSlv
  • 14,067
  • 15
  • 102
  • 169