InteractionRequest.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.remote;

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

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.view.View;
import androidx.test.espresso.Root;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewAssertion;
import androidx.test.espresso.proto.UiInteraction.InteractionRequestProto;
import androidx.test.espresso.proto.UiInteraction.InteractionRequestProto.ActionOrAssertionCase;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageLite;
import org.hamcrest.Matcher;

/**
 * Encapsulates an {@link InteractionRequestProto} request. Takes care of all the proto packing and
 * unpacking.
 */
public final class InteractionRequest implements EspressoRemoteMessage.To<MessageLite> {

  /**
   * This field is used to create an instance of {@link InteractionRequest} from its unwrapped proto
   * message.
   */
  private static final EspressoRemoteMessage.From<InteractionRequest, InteractionRequestProto>
      FROM =
          new EspressoRemoteMessage.From<InteractionRequest, InteractionRequestProto>() {
            @Override
            public InteractionRequest fromProto(InteractionRequestProto interactionRequestProto) {
              Builder interactionRequestBuilder = new Builder();
              interactionRequestBuilder
                  .setRootMatcher(
                      TypeProtoConverters.<Matcher<Root>>anyToType(
                          interactionRequestProto.getRootMatcher()))
                  .setViewMatcher(
                      TypeProtoConverters.<Matcher<View>>anyToType(
                          interactionRequestProto.getViewMatcher()));

              int actionOrAssertionCaseNumber =
                  interactionRequestProto.getActionOrAssertionCase().getNumber();

              if (ActionOrAssertionCase.VIEW_ACTION.getNumber() == actionOrAssertionCaseNumber) {
                interactionRequestBuilder.setViewAction(
                    TypeProtoConverters.<ViewAction>anyToType(
                        interactionRequestProto.getViewAction()));
              }

              if (ActionOrAssertionCase.VIEW_ASSERTION.getNumber() == actionOrAssertionCaseNumber) {
                interactionRequestBuilder.setViewAssertion(
                    TypeProtoConverters.<ViewAssertion>anyToType(
                        interactionRequestProto.getViewAssertion()));
              }
              return interactionRequestBuilder.build();
            }
          };

  @Nullable private final Matcher<Root> rootMatcher;
  @Nullable private final Matcher<View> viewMatcher;
  @Nullable private final ViewAction viewAction;
  @Nullable private final ViewAssertion viewAssertion;

  @VisibleForTesting
  InteractionRequest(
      @Nullable Matcher<Root> rootMatcher,
      @Nullable Matcher<View> viewMatcher,
      @Nullable ViewAction viewAction,
      @Nullable ViewAssertion viewAssertion) {
    this.rootMatcher = rootMatcher;
    this.viewMatcher = viewMatcher;
    this.viewAction = viewAction;
    this.viewAssertion = viewAssertion;
  }

  private InteractionRequest(Builder builder) {
    this(builder.rootMatcher, builder.viewMatcher, builder.viewAction, builder.viewAssertion);
  }

  /** {@inheritDoc} */
  @Override
  @SuppressWarnings("unchecked") // safe covariant cast
  public MessageLite toProto() {
    try {
      InteractionRequestProto.Builder interactionRequestBuilder =
          InteractionRequestProto.newBuilder();
      interactionRequestBuilder.setRootMatcher(TypeProtoConverters.typeToAny(rootMatcher));
      if (viewMatcher != null) {
        interactionRequestBuilder.setViewMatcher(TypeProtoConverters.typeToAny(viewMatcher));
      }

      if (viewAction != null) {
        interactionRequestBuilder.setViewAction(TypeProtoConverters.typeToAny(viewAction));
      }

      if (viewAssertion != null) {
        interactionRequestBuilder.setViewAssertion(TypeProtoConverters.typeToAny(viewAssertion));
      }
      return interactionRequestBuilder.build();
    } catch (ClassCastException cce) {
      throw new RemoteProtocolException(
          "Type does not implement the EspressoRemoteMessage.TO interface", cce);
    }
  }

  /**
   * Returns the {@link Matcher<Root>} associated with this {@link InteractionRequest} or {@code
   * null} if no {@link Matcher<Root>} was set.
   */
  public Matcher<Root> getRootMatcher() {
    return rootMatcher;
  }

  /**
   * Returns the {@link Matcher<View>} associated with this {@link InteractionRequest} or {@code
   * null} if no view matcher was set.
   */
  public Matcher<View> getViewMatcher() {
    return viewMatcher;
  }

  /**
   * Returns the {@link ViewAction} associated with this {@link InteractionRequest} or {@code null}
   * if no {@link ViewAction} was set.
   */
  public ViewAction getViewAction() {
    return viewAction;
  }

  /**
   * Returns the {@link ViewAssertion} associated with this {@link InteractionRequest} or {@code
   * null} if no {@link ViewAssertion} was set.
   */
  public ViewAssertion getViewAssertion() {
    return viewAssertion;
  }

  /**
   * Creates an instance of {@link InteractionRequest} from a View matcher and action.
   *
   * @return remote request object
   */
  public static class Builder {
    private final RemoteDescriptorRegistry remoteDescriptorRegistry;
    private Matcher<Root> rootMatcher;
    private Matcher<View> viewMatcher;
    private ViewAction viewAction;
    private ViewAssertion viewAssertion;
    private byte[] interactionRequestProtoByteArray;

    /** Creates a new {@link Builder} instance. */
    public Builder() {
      remoteDescriptorRegistry = RemoteDescriptorRegistry.getInstance();
    }

    /**
     * Sets the root matcher for this {@link InteractionRequest}
     *
     * @see Root
     * @param rootMatcher the root matcher to set
     * @return fluent interface
     */
    public Builder setRootMatcher(@NonNull Matcher<Root> rootMatcher) {
      this.rootMatcher = checkNotNull(rootMatcher);
      checkArgument(
          remoteDescriptorRegistry.hasArgForInstanceType(rootMatcher.getClass()),
          "No RemoteDescriptor registered for ViewMatcher: %s",
          rootMatcher);
      return this;
    }

    /**
     * Sets the view matcher for this {@link InteractionRequest}
     *
     * @param viewMatcher the view matcher to set
     * @return fluent interface
     */
    public Builder setViewMatcher(@NonNull Matcher<View> viewMatcher) {
      this.viewMatcher = checkNotNull(viewMatcher);
      checkArgument(
          remoteDescriptorRegistry.hasArgForInstanceType(viewMatcher.getClass()),
          "No RemoteDescriptor registered for ViewMatcher: %s",
          viewMatcher);
      return this;
    }

    /**
     * Sets the {@link ViewAction} for this {@link InteractionRequest}
     *
     * @param viewAction the view action to set
     * @return fluent interface
     * @throws IllegalStateException if a {@link ViewAssertion} was already set through {@link
     *     InteractionRequest.Builder#setViewAssertion(ViewAssertion)} before. {@link
     *     InteractionRequest} supports only one of {@link ViewAction} or {@link ViewAssertion}, but
     *     not both.
     */
    public Builder setViewAction(@NonNull ViewAction viewAction) {
      this.viewAction = checkNotNull(viewAction);
      checkArgument(
          remoteDescriptorRegistry.hasArgForInstanceType(viewAction.getClass()),
          "No RemoteDescriptor registered for ViewAction: %s",
          viewAction);
      return this;
    }

    /**
     * Sets the {@link ViewAssertion} for this {@link InteractionRequest}
     *
     * @param viewAssertion the view action to set
     * @return fluent interface
     * @throws IllegalStateException if a {@link ViewAction} was already set through {@link
     *     InteractionRequest.Builder#setViewAction(ViewAction)} before. {@link InteractionRequest}
     *     supports only one of {@link ViewAction} or {@link ViewAssertion}, but not both.
     */
    public Builder setViewAssertion(@NonNull ViewAssertion viewAssertion) {
      this.viewAssertion = checkNotNull(viewAssertion);
      checkArgument(
          remoteDescriptorRegistry.hasArgForInstanceType(viewAssertion.getClass()),
          "No RemoteDescriptor registered for ViewAssertion: %s",
          viewAssertion);
      return this;
    }

    /**
     * Set the result proto as a byte array. This byte array will be parsed into an {@link
     * InteractionRequestProto}. Providing an invalid byte array will result in a {@link
     * RemoteProtocolException} when the {@link #build()} method is called!
     *
     * @param protoByteArray the proto byte array to set
     * @return fluent interface {@link Builder}
     */
    public Builder setRequestProto(@NonNull byte[] protoByteArray) {
      this.interactionRequestProtoByteArray =
          checkNotNull(protoByteArray, "protoByteArray cannot be null!");
      return this;
    }

    /**
     * Builds an {@link InteractionRequest} object.
     *
     * @return an {@link InteractionRequest} object.
     * @throws IllegalStateException when conflicting properties are set. You can either set a
     *     {@link Matcher<View>} and a {@link ViewAction} or set the proto byte array but not both.
     *     Setting all values would result in an override, therefore setting both properties will
     *     result in an exception.
     * @throws RemoteProtocolException when the provided proto byte array cannot be parsed into a
     *     protocol buffer of type {@link InteractionRequestProto}
     */
    public InteractionRequest build() {
      if (viewAction != null && viewAssertion != null) {
        throw new IllegalStateException(
            "View Action and Assertion set. Either set a View Action "
                + "or a View Assertion but not both at the same time!");
      }

      if (viewMatcher != null || viewAction != null || viewAssertion != null) {
        if (interactionRequestProtoByteArray != null) {
          throw new IllegalStateException(
              "Instances can either be create from an view matcher. "
                  + "view action and assertion or an interaction request proto byte array but not "
                  + "both!");
        }
      }

      if (interactionRequestProtoByteArray != null) {
        InteractionRequestProto interactionRequestProto;
        try {
          interactionRequestProto =
              InteractionRequestProto.parseFrom(interactionRequestProtoByteArray);
        } catch (InvalidProtocolBufferException ipbe) {
          throw new RemoteProtocolException("Cannot parse interactionResultProto", ipbe);
        }
        return InteractionRequest.FROM.fromProto(interactionRequestProto);
      }

      checkState(
          rootMatcher != null,
          "root matcher is mandatory and needs to be set using:"
              + "Builder.setRootMatcher(Matcher)");

      return new InteractionRequest(this);
    }
  }
}