WebViewAssertions.java

/*
 * 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.assertion;

import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static androidx.test.espresso.web.model.Atoms.script;
import static androidx.test.espresso.web.model.Atoms.transform;
import static com.google.common.base.Preconditions.checkNotNull;

import android.support.annotation.VisibleForTesting;
import android.webkit.WebView;
import androidx.test.espresso.remote.annotation.RemoteMsgConstructor;
import androidx.test.espresso.remote.annotation.RemoteMsgField;
import androidx.test.espresso.web.model.Atom;
import androidx.test.espresso.web.model.Evaluation;
import androidx.test.espresso.web.model.TransformingAtom;
import java.io.IOException;
import java.io.StringWriter;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.hamcrest.Matcher;
import org.hamcrest.StringDescription;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/** A collection of {@link WebAssertion}s that assert on {@link WebView}s. */
public final class WebViewAssertions {

  private static final ResultDescriber<Object> TO_STRING_DESCRIBER = new ToStringResultDescriber();

  private WebViewAssertions() {}

  /**
   * A WebAssertion which asserts that the given Atom's result is accepted by the provided matcher.
   *
   * @param atom an atom to evaluate on the webview
   * @param resultMatcher a matcher to apply to the result of the atom.
   * @param resultDescriber a describer that converts the result to a string.
   */
  public static <E> WebAssertion<E> webMatches(
      Atom<E> atom,
      final Matcher<E> resultMatcher,
      final ResultDescriber<? super E> resultDescriber) {
    checkNotNull(resultMatcher);
    checkNotNull(resultDescriber);
    checkNotNull(atom);
    return new ResultCheckingWebAssertion<>(atom, resultMatcher, resultDescriber);
  }

  /**
   * A WebAssertion which asserts that the given Atom's result is accepted by the provided matcher.
   *
   * @param atom an atom to evaluate on the webview
   * @param resultMatcher a matcher to apply to the result of the atom.
   */
  public static <E> WebAssertion<E> webMatches(Atom<E> atom, final Matcher<E> resultMatcher) {
    return webMatches(atom, resultMatcher, TO_STRING_DESCRIBER);
  }

  /** A WebAssertion which asserts that the document is matched by the provided matcher. */
  public static WebAssertion<Document> webContent(final Matcher<Document> domMatcher) {
    checkNotNull(domMatcher);
    return webMatches(
        transform(
            script("function getHtml() {return document.documentElement.outerHTML;}"),
            new DocumentParserAtom()),
        domMatcher,
        new WebContentResultDescriber());
  }

  /**
   * Converts a result to a String.
   *
   * @param <E> The type of the result.
   */
  public interface ResultDescriber<E> {
    public String apply(E input);
  }

  @VisibleForTesting
  static final class ResultCheckingWebAssertion<E> extends WebAssertion<E> {
    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 0)
    private final Atom<E> atom;

    @RemoteMsgField(order = 1)
    private final Matcher<E> resultMatcher;

    @RemoteMsgField(order = 2)
    private final ResultDescriber<? super E> resultDescriber;

    @RemoteMsgConstructor
    ResultCheckingWebAssertion(
        Atom<E> atom, Matcher<E> resultMatcher, ResultDescriber<? super E> resultDescriber) {
      super(atom);
      this.atom = atom;
      this.resultMatcher = resultMatcher;
      this.resultDescriber = resultDescriber;
    }

    @Override
    protected void checkResult(WebView view, E result) {
      StringDescription description = new StringDescription();
      description.appendText("'");
      resultMatcher.describeTo(description);
      description.appendText("' doesn't match: ");
      description.appendText(null == result ? "null" : resultDescriber.apply(result));
      assertThat(description.toString(), result, resultMatcher);
    }
  }

  @VisibleForTesting
  static final class ToStringResultDescriber implements ResultDescriber<Object> {

    @RemoteMsgConstructor
    public ToStringResultDescriber() {}

    @Override
    public String apply(Object input) {
      return input.toString();
    }
  }

  @VisibleForTesting
  static final class WebContentResultDescriber implements ResultDescriber<Document> {
    @RemoteMsgConstructor
    public WebContentResultDescriber() {}

    @Override
    public String apply(Document document) {
      try {
        DOMSource docSource = new DOMSource(document);
        Transformer tf = TransformerFactory.newInstance().newTransformer();
        StringWriter writer = new StringWriter();
        StreamResult streamer = new StreamResult(writer);
        tf.transform(docSource, streamer);
        return writer.toString();
      } catch (TransformerException e) {
        return "Could not transform!!!" + e;
      }
    }
  }

  @VisibleForTesting
  static final class DocumentParserAtom
      implements TransformingAtom.Transformer<Evaluation, Document> {
    @RemoteMsgConstructor
    public DocumentParserAtom() {}

    @Override
    public Document apply(Evaluation eval) {
      if (eval.getValue() instanceof String) {
        try {
          return TagSoupDocumentParser.newInstance().parse((String) eval.getValue());
        } catch (SAXException se) {
          throw new RuntimeException("Parse failed: " + eval.getValue(), se);
        } catch (IOException ioe) {
          throw new RuntimeException("Parse failed: " + eval.getValue(), ioe);
        }
      }
      throw new RuntimeException("Value should have been a string: " + eval);
    }
  }
}