wangyao5 / android-test-kit

Automatically exported from code.google.com/p/android-test-kit
0 stars 0 forks source link

enhancement waitForView and onView waiting for idle #50

Closed GoogleCodeExporter closed 9 years ago

GoogleCodeExporter commented 9 years ago
When pressing a button that drastically changes the view, e.g. taking widgets 
down or adding new widgets (e.g. dialogues) if onView is called immediately 
after the action perform(click()) then the onView will fail since the new 
widget is not yet visible.

I have found two helper methods solve my problems, and I think something like 
them should be built into the framework.

onView should wait until the UI thread is idle.  I have seen that perform does 
that, but have not found a location where onView does that.

Below is my workaround that I place before onView.

    public static void settleUiThread(Activity activity)
            throws InterruptedException {
        final Semaphore uiThreadSettled = new Semaphore(0);
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                uiThreadSettled.release();
            }
        });
        boolean acquired = uiThreadSettled.tryAcquire(1, TimeUnit.MINUTES);
        if (!acquired) {
            throw new Error("ui thread settling semaphore not acquired.");
        }
    }

The second workaround is to give the code a chance to bring up the dialogs.

    public static void waitForView(int viewId) {
        long startTime = (new Date()).getTime();
        long endTime = startTime + 15000;
        do {
            try {
                onView(withId(viewId)).check(matches(isDisplayed()));
                return;
            } catch (Throwable ex) {
                Log.d(TAG, "trying again waiting for view "
                        + viewId + " " + ex);
                Thread.yield();
            }
        } while (((new Date()).getTime()) < endTime);
        onView(withId(viewId)).check(matches(isDisplayed()));
    }

I have only seen this problem consistently when running on very fast emulators. 
 Real hardware seems to slow things down enough that the it happens only 
occasionally; enough so that I have added the above calls to my test cases to 
make the reliable.

Original issue reported on code.google.com by marcp...@gmail.com on 20 Jan 2014 at 8:36

GoogleCodeExporter commented 9 years ago
"if onView is called immediately after the action perform(click()) then the 
onView will fail since the new widget is not yet visible. "

I believe I've seen similar issues. I have a simple app that have view A and if 
you swipe to the right you'll get to view B. On view B there's an extra button 
on the action bar which is not presented on view A. If I run my espresso test 
and swipe to view B then immediately uses onView() to try to match the button, 
it'll fail. UI dump from the log would reveal that the button is not in the 
view yet. 

I didn't implement as complicated workaround as OP did. A simple 
Thread.Sleep(1000) before the onView() call made the test pass. 

I don't think onData is applicable here since there is no AdapterView. Neither 
is registerIdelresouce as there's no background thread. 

Original comment by lyfort...@gmail.com on 24 Jan 2014 at 1:06

GoogleCodeExporter commented 9 years ago
[deleted comment]
GoogleCodeExporter commented 9 years ago
I found a little better way.

Pattern of use:

  // wait during 15 seconds for a view
  onView(isRoot()).perform(waitId(R.id.wizardNext, Sampling.SECONDS_15));

Required Action:

/** Perform action of waiting for a specific view id. */
public static ViewAction waitId(final int viewId, final long millis) {
    return new ViewAction() {
        @Override
        public Matcher<View> getConstraints() {
            return isRoot();
        }

        @Override
        public String getDescription() {
            return "wait for a specific view with id <" + viewId + "> 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;
            final Matcher<View> viewMatcher = withId(viewId);

            do {
                for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
                    // found view with required ID
                    if (viewMatcher.matches(child)) {
                        return;
                    }
                }

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

            // timeout happens
            throw new PerformException.Builder()
                    .withActionDescription(this.getDescription())
                    .withViewDescription(HumanReadables.describe(view))
                    .withCause(new TimeoutException())
                    .build();
        }
    };
}

Original comment by kucheren...@gmail.com on 21 Mar 2014 at 4:38

GoogleCodeExporter commented 9 years ago
Thanks for this. I've improved it by taking any view matcher

public static ViewAction waitToFind(final Matcher<View> viewMatcher, final int 
millis) {
        return new ViewAction() {
            @Override
            public Matcher<View> getConstraints() {
                return isRoot();
            }

            @Override
            public String getDescription() {
                return "wait for a specific view with matcher <" + viewMatcher + "> 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)) {
                        // found view with required ID
                        if (viewMatcher.matches(child)) {
                            return;
                        }
                    }

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

                // timeout happens
                throw new PerformException.Builder()
                        .withActionDescription(this.getDescription())
                        .withViewDescription(HumanReadables.describe(view))
                        .withCause(new TimeoutException())
                        .build();
            }
        };
    }

Though I'm sure this could be implemented in an IdlingResource and then 
register it with Espresso

Original comment by zboa...@gmail.com on 14 Apr 2014 at 4:00

GoogleCodeExporter commented 9 years ago
I tried the solution above. But sometimes, I get the below error:
java.lang.RuntimeException: Action will not be performed because the target 
view does not match one or more of the following constraints:
at least 90 percent of the view's area is displayed to the user.

I tried adding constraint like this:
if (viewMatcher.matches(child)&& viewMatcher.matches(isCompletelyDisplayed())

But this timed out. Clearly, suggesting that the function matches with the view 
even if it is partially displayed.

Any suggestions for that.

Original comment by dikshago...@gmail.com on 5 May 2014 at 1:19

GoogleCodeExporter commented 9 years ago
The requested waitForView API breaks the Espresso API paradigm (i.e. everything 
should be synchronized. The test author should not have to make a decision on 
how long to wait for a view). For components that are not synchronized by 
default, Espresso provides the IdlingResource interface. Please use it. It will 
make your tests much cleaner and more dependable.

Some of you may also be hitting 
https://code.google.com/p/android-test-kit/issues/detail?id=55. We will look 
into improving this in the coming release.

Original comment by vale...@google.com on 7 May 2014 at 6:33