RepeatActionUntilViewState.java

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.test.espresso.action;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import android.view.View;
import android.widget.ViewFlipper;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.util.HumanReadables;
import org.hamcrest.Matcher;
import org.hamcrest.StringDescription;

/**
 * Enables performing a given action on a view until it reaches desired state matched by given View
 * matcher. This action is useful in the scenarios where performing the action repeatedly on a view
 * changes its state at runtime. For example, if there is a {@link ViewFlipper} on which user can
 * swipe through the views and it automatically flips between each child at a regular interval, it
 * is not always certain which of the child is displayed at a given point of time. In this case, in
 * order to perform click on child no. 4 (assuming child no. 4 contains a text "Child 4"), repeat
 * action can be used as follows:
 *
 * <p>
 *
 * <blockquote>
 *
 * <pre>{@code
 * int maxAttempts=10;
 * onView(withId(R.id.my_pager))
 *            .perform(repeatedlyUntil(swipeUp(), hasDescendant(withText("Child 4")), maxAttempts),
 *            click());
 * }</pre>
 *
 * </blockquote>
 */
public final class RepeatActionUntilViewState implements ViewAction {

  private final ViewAction mAction;
  private final Matcher<View> mDesiredStateMatcher;
  private final int mMaxAttempts;

  protected RepeatActionUntilViewState(
      ViewAction action, Matcher<View> desiredStateMatcher, int maxAttempts) {
    checkNotNull(action);
    checkNotNull(desiredStateMatcher);
    checkState(maxAttempts > 1, "maxAttempts should be greater than 1");
    this.mAction = action;
    this.mDesiredStateMatcher = desiredStateMatcher;
    this.mMaxAttempts = maxAttempts;
  }

  @Override
  public Matcher<View> getConstraints() {
    return mAction.getConstraints();
  }

  @Override
  public String getDescription() {
    StringDescription stringDescription = new StringDescription();
    mDesiredStateMatcher.describeTo(stringDescription);
    return String.format("%s until: %s", mAction.getDescription(), stringDescription);
  }

  @Override
  public void perform(UiController uiController, View view) {
    int noOfAttempts = 1;
    for (; !mDesiredStateMatcher.matches(view) && noOfAttempts <= mMaxAttempts; noOfAttempts++) {
      mAction.perform(uiController, view);
      uiController.loopMainThreadUntilIdle();
    }
    if (noOfAttempts > mMaxAttempts) {
      throw new PerformException.Builder()
          .withActionDescription(this.getDescription())
          .withViewDescription(HumanReadables.describe(view))
          .withCause(
              new RuntimeException(
                  String.format("Failed to achieve view state after %d attempts", mMaxAttempts)))
          .build();
    }
  }
}