public final class

Web

extends java.lang.Object

 java.lang.Object

↳androidx.test.espresso.web.sugar.Web

Gradle dependencies

compile group: 'androidx.test.espresso', name: 'espresso-web', version: '3.6.1'

  • groupId: androidx.test.espresso
  • artifactId: espresso-web
  • version: 3.6.1

Artifact androidx.test.espresso:espresso-web:3.6.1 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.test.espresso:espresso-web com.android.support.test.espresso:espresso-web

Androidx class mapping:

androidx.test.espresso.web.sugar.Web android.support.test.espresso.web.sugar.Web

Overview

An Entry Point to work with WebViews on Android.

Similar to onData, WebView interactions are actually composed of several ViewActions. However they need to be properly orchestrated and are quite verbose. Web and WebInteraction wrap this boilerplate and give an Espresso like feel to interacting with WebViews.

WebView interactions constantly cross the Java/Javascript boundary to do their work, since there is no chance of introducing race conditions by exposing data from the Javascript environment (everything we see on the Java side is an isolated copy), returning data from WebInteractions is fully supported.

Summary

Constructors
publicWeb()

Methods
public static Web.WebInteraction<java.lang.Void>onWebView()

public static Web.WebInteraction<java.lang.Void>onWebView(<any> viewMatcher)

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public Web()

Methods

public static Web.WebInteraction<java.lang.Void> onWebView()

public static Web.WebInteraction<java.lang.Void> onWebView(<any> viewMatcher)

Source

/*
 * Copyright (C) 2015 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.web.sugar;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.matcher.ViewMatchers.isJavascriptEnabled;
import static androidx.test.internal.util.Checks.checkArgument;
import static androidx.test.internal.util.Checks.checkNotNull;
import static androidx.test.internal.util.Checks.checkState;
import static org.hamcrest.Matchers.any;

import android.view.View;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.remote.annotation.RemoteMsgConstructor;
import androidx.test.espresso.remote.annotation.RemoteMsgField;
import androidx.test.espresso.web.action.AtomAction;
import androidx.test.espresso.web.action.EnableJavascriptAction;
import androidx.test.espresso.web.assertion.WebAssertion;
import androidx.test.espresso.web.model.Atom;
import androidx.test.espresso.web.model.ElementReference;
import androidx.test.espresso.web.model.WindowReference;
import androidx.test.internal.platform.util.TestOutputEmitter;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.CheckReturnValue;
import org.hamcrest.Matcher;

/**
 * An Entry Point to work with WebViews on Android.
 *
 * <p>Similar to onData, WebView interactions are actually composed of several ViewActions. However
 * they need to be properly orchestrated and are quite verbose. Web and WebInteraction wrap this
 * boilerplate and give an Espresso like feel to interacting with WebViews.
 *
 * <p>WebView interactions constantly cross the Java/Javascript boundary to do their work, since
 * there is no chance of introducing race conditions by exposing data from the Javascript
 * environment (everything we see on the Java side is an isolated copy), returning data from
 * WebInteractions is fully supported.
 */
public final class Web {
  static {
    // Also adds the usage data as test output properties. By default it's no-op.
    Map<String, Serializable> usageProperties = new HashMap<>();
    usageProperties.put("Espresso-Web", "1");
    TestOutputEmitter.addOutputProperties(usageProperties);
  }

  public static WebInteraction<Void> onWebView() {
    return onWebView(isJavascriptEnabled());
  }

  public static WebInteraction<Void> onWebView(Matcher<View> viewMatcher) {
    return new WebInteraction<Void>(viewMatcher);
  }

  private static class Timeout {
    private final long timeout;
    private final TimeUnit unit;
    static final Timeout NONE = new Timeout(-1, TimeUnit.MILLISECONDS, false);

    private Timeout(long timeout, TimeUnit unit, boolean check) {
      this.timeout = timeout;
      this.unit = unit;
      if (check) {
        checkArgument(timeout > 0);
        checkNotNull(unit);
      }
    }
  }

  /**
   * Analogous to a ViewInteraction or a DataInteraction, a WebInteraction exposes a fluent API to
   * the underlying WebView.
   */
  public static class WebInteraction<R> {
    private final Matcher<View> viewMatcher;
    private final boolean brandNew;

    @Nullable private final R result;
    @Nullable private final WindowReference window;
    @Nullable private final ElementReference element;
    private final Timeout timeout;

    private WebInteraction(Matcher<View> viewMatcher) {
      this(viewMatcher, null, null, null, true, new Timeout(10, TimeUnit.SECONDS, true));
    }

    private WebInteraction(
        Matcher<View> viewMatcher,
        R result,
        WindowReference window,
        ElementReference element,
        boolean brandNew,
        Timeout timeout) {
      this.viewMatcher = checkNotNull(viewMatcher);
      this.result = result;
      this.window = window;
      this.element = element;
      this.brandNew = brandNew;
      this.timeout = timeout;
    }

    /**
     * Removes the Element and Window references from this interaction.
     *
     * <p>This is usually necessary when a prior action (for example a click) introduces a
     * navigation that invalidates the ElementReference and WindowReference pointers.
     */
    public WebInteraction<R> reset() {
      return new WebInteraction<R>(viewMatcher, result, null, null, brandNew, timeout);
    }

    /**
     * Performs a force enable of Javascript on a WebView.
     *
     * <p>All WebView interactions are done via Javascript - therefore the WebView we are working on
     * must support Javascript evaluation.
     *
     * <p>Enabling Javascript may cause the WebView under test to be reloaded. This is necessary to
     * ensure the test infrastructure javascript bridges are loaded by the WebView.
     */
    public WebInteraction<R> forceJavascriptEnabled() {
      onView(viewMatcher).perform(new EnableJavascriptAction());
      return this;
    }

    /**
     * Disables all Timeouts on this WebInteraction.
     *
     * <p>Javascript evaluation is performed asynchronously on the WebKit/Chromium thread. By
     * default we wait a short while for the result to be delivered back to the test.
     */
    @CheckResult
    @CheckReturnValue
    public WebInteraction<R> withNoTimeout() {
      return new WebInteraction<R>(viewMatcher, result, window, element, brandNew, Timeout.NONE);
    }

    /** Sets a specific timeout for this WebInteraction. */
    @CheckResult
    @CheckReturnValue
    public WebInteraction<R> withTimeout(long amount, TimeUnit unit) {
      return new WebInteraction<R>(
          viewMatcher, result, window, element, brandNew, new Timeout(amount, unit, true));
    }

    /**
     * Causes this WebInteraction to have it's javascript evaluated in a particular DOM window.
     *
     * <p>By default Javascript may be evaluated in the main window. However in an application which
     * uses frames, you may want to evaluate in another frame.
     */
    @CheckResult
    @CheckReturnValue
    public WebInteraction<R> inWindow(WindowReference window) {
      return new WebInteraction<R>(viewMatcher, result, window, element, brandNew, timeout);
    }

    /**
     * Causes this WebInteraction to have it's javascript evaluated in a particular DOM window.
     *
     * <p>This method accepts an Atom which will be evaluated in the main window to choose a
     * particular DOM window for further interactions. This method will block until the the provided
     * Atom returns with a result.
     */
    @CheckResult
    @CheckReturnValue
    public WebInteraction<R> inWindow(Atom<WindowReference> windowPicker) {
      return new WebInteraction<R>(
          viewMatcher, result, doEval(windowPicker, null, null), element, brandNew, timeout);
    }

    /**
     * Causes this WebInteraction to supply the given ElementReference to the Atom prior to
     * evaluation.
     *
     * <p>Calling this method resets any previously selected ElementReference.
     *
     * <p>{@see Atom#getArguments}
     */
    @CheckResult
    @CheckReturnValue
    public WebInteraction<R> withElement(ElementReference element) {
      return new WebInteraction<R>(viewMatcher, result, window, element, brandNew, timeout);
    }

    /**
     * Causes this WebInteraction to supply the given ElementReference to the Atom prior to
     * evaluation.
     *
     * <p>{@see Atom#getArguments}
     *
     * <p>This method accepts an Atom<ElementReference> which it will evaluate on the current
     * context's Window. This method blocks until the evaluation completes.
     *
     * <p>Calling this method resets any previously selected ElementReference.
     *
     * <p>If you want to evaluate the elementPicker in the context of the previously selected
     * ElementReference {@see #withContextualElement}
     */
    @CheckResult
    @CheckReturnValue
    public WebInteraction<R> withElement(Atom<ElementReference> elementPicker) {
      return new WebInteraction<R>(
          viewMatcher, result, window, doEval(elementPicker, window, null), brandNew, timeout);
    }

    /**
     * Allows for contextually evaluating this WebInteraction with the selected element.
     *
     * <p>Specifically the elementPicker atom will be evaluated with the CURRENTLY selected element
     * to discover the new element to work against.
     *
     * <p>This allows callers to interact with a document that looks like this:
     *
     * <pre>
     * {@literal
     * <div id="teacher">
     *   <div id="person_name">
     *     <p>Socrates</p>
     *   </div>
     * </div>
     * <div id="student">
     *   <div id="person_name">
     *     <p>Plato</p>
     *   </div>
     * </div>
     * }
     * </pre>
     *
     * With code like this:
     *
     * <pre>{@code
     * onWebView()
     *   .withElement(findElement(Locator.ID, "teacher"))
     *   .withContextualElement(findElement(Locator.ID, "person_name"))
     *   .check(webMatches(getText(), containsString("Socrates")));
     * }</pre>
     */
    @CheckResult
    @CheckReturnValue
    public WebInteraction<R> withContextualElement(Atom<ElementReference> elementPicker) {
      return new WebInteraction<R>(
          viewMatcher, result, window, doEval(elementPicker, window, element), brandNew, timeout);
    }

    /**
     * Executes the provided atom within the current context (the combination of Window and Element
     * References).
     *
     * <p>This method blocks until the Atom returns. The result of the Atom's evaluation is used to
     * create a new instance of WebInteraction which can be used to access the result of the Atom's
     * evaluation.
     */
    public <E> WebInteraction<E> perform(Atom<E> atom) {
      E newResult = doEval(atom, window, element);
      return new WebInteraction<E>(viewMatcher, newResult, window, element, false, timeout);
    }

    /**
     * Evaluates the given WebAssertion.
     *
     * <p>The WebAssertion's atom is evaluated, after it's evaluation completes, the WebAssertion is
     * run on the main thread to perform further checks. The WebAssertion is given the Atom's result
     * and the WebView it had run against.
     *
     * <p>After this method completes, the result of the atom's evaluation is avaliable via get.
     */
    public <E> WebInteraction<E> check(WebAssertion<E> assertion) {
      E newResult = doEval(assertion.getAtom(), window, element);
      onView(viewMatcher).check(assertion.toViewAssertion(newResult));
      return new WebInteraction<E>(viewMatcher, newResult, window, element, false, timeout);
    }

    private <E> E doEval(Atom<E> atom, WindowReference window, ElementReference elem) {
      checkNotNull(atom, "Need an atom!");

      AtomAction<E> atomAction = new AtomAction(atom, window, elem);
      onView(viewMatcher).perform(atomAction);
      try {
        if (timeout == Timeout.NONE) {
          return atomAction.get();
        } else {
          return atomAction.get(timeout.timeout, timeout.unit);
        }
      } catch (ExecutionException ee) {
        onView(viewMatcher).perform(new ExceptionPropagator(ee.getCause()));
        return null; // always throws.
      } catch (InterruptedException ie) {
        onView(viewMatcher).perform(new ExceptionPropagator(ie));
        return null; // always throws.
      } catch (TimeoutException te) {
        onView(viewMatcher).perform(new ExceptionPropagator(te));
        return null; // always throws.
      } catch (RuntimeException re) {
        onView(viewMatcher).perform(new ExceptionPropagator(re));
        return null; // always throws.
      }
    }

    /** Returns the result of a prior call to perform or check. */
    public R get() {
      checkState(!brandNew, "Perform or Check never called on this WebInteraction!");
      return result;
    }

    static class ExceptionPropagator implements ViewAction {
      @RemoteMsgField(order = 0)
      private final RuntimeException error;

      @RemoteMsgConstructor
      public ExceptionPropagator(RuntimeException error) {
        this.error = checkNotNull(error);
      }

      public ExceptionPropagator(Throwable t) {
        this(new RuntimeException(t));
      }

      @Override
      public String getDescription() {
        return "Propagate: " + error;
      }

      @Override
      public void perform(UiController uiController, View view) {
        throw error;
      }

      @SuppressWarnings("unchecked")
      @Override
      public Matcher<View> getConstraints() {
        return any(View.class);
      }
    }
  }
}