DriverAtoms.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.webdriver;

import static androidx.test.espresso.web.model.Atoms.castOrDie;
import static com.google.common.base.Preconditions.checkNotNull;

import android.support.annotation.VisibleForTesting;
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.ElementReference;
import androidx.test.espresso.web.model.Evaluation;
import androidx.test.espresso.web.model.SimpleAtom;
import androidx.test.espresso.web.model.TransformingAtom;
import androidx.test.espresso.web.model.WindowReference;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.List;
import java.util.Map;

/** A collection of Javascript Atoms from the WebDriver project. */
public final class DriverAtoms {

  private DriverAtoms() {}

  /** Simulates the javascript events to click on a particular element. */
  public static Atom<Evaluation> webClick() {
    return new WebClickSimpleAtom();
  }

  /** Clears content from an editable element. */
  public static Atom<Evaluation> clearElement() {
    return new ClearElementSimpleAtom();
  }

  /** Simulates javascript key events sent to a certain element. */
  public static Atom<Evaluation> webKeys(final String text) {
    return new WebKeysSimpleAtom(checkNotNull(text));
  }

  /** Finds an element using the provided locatorType strategy. */
  public static Atom<ElementReference> findElement(final Locator locator, final String value) {
    return new FindElementTransformingAtom(
        new FindElementSimpleAtom(locator.getType(), value), castOrDie(ElementReference.class));
  }

  /** Finds the currently active element in the document. */
  public static Atom<ElementReference> selectActiveElement() {
    return new SelectActiveElementTransformingAtom(
        new ActiveElementSimpleAtom(), castOrDie(ElementReference.class));
  }

  /** Selects a subframe of the currently selected window by it's index. */
  public static Atom<WindowReference> selectFrameByIndex(int index) {
    return new SelectFrameByIndexTransformingAtom(
        new FrameByIndexSimpleAtom(index), castOrDie(WindowReference.class));
  }

  /** Selects a subframe of the given window by it's index. */
  public static Atom<WindowReference> selectFrameByIndex(int index, WindowReference root) {
    return new SelectFrameByIndexTransformingAtom(
        new FrameByIndexWithRootSimpleAtom(index, checkNotNull(root)),
        castOrDie(WindowReference.class));
  }

  /** Selects a subframe of the given window by it's name or id. */
  public static Atom<WindowReference> selectFrameByIdOrName(String idOrName, WindowReference root) {
    return new SelectFrameByIdOrNameTransformingAtom(
        new FrameByIdOrNameWithRootSimpleAtom(checkNotNull(idOrName), checkNotNull(root)),
        castOrDie(WindowReference.class));
  }

  /** Selects a subframe of the current window by it's name or id. */
  public static Atom<WindowReference> selectFrameByIdOrName(String idOrName) {
    return new SelectFrameByIdOrNameTransformingAtom(
        new FrameByIdOrNameSimpleAtom(checkNotNull(idOrName)), castOrDie(WindowReference.class));
  }

  /** Returns the visible text beneath a given DOM element. */
  public static Atom<String> getText() {
    return new GetTextTransformingAtom(new GetVisibleTextSimpleAtom(), castOrDie(String.class));
  }

  /** Returns {@code true} if the desired element is in view after scrolling. */
  public static Atom<Boolean> webScrollIntoView() {
    return new WebScrollIntoViewAtom(new WebScrollIntoViewSimpleAtom(), castOrDie(Boolean.class));
  }

  /** Finds multiple elements given a locator strategy. */
  public static Atom<List<ElementReference>> findMultipleElements(
      final Locator locator, final String value) {

    SimpleAtom findElementsScriptSimpleAtom =
        new FindElementsScriptSimpleAtom(locator.getType(), value);
    TransformingAtom.Transformer<Evaluation, List<ElementReference>> elementReferenceListAtom =
        new ElementReferenceListAtom(locator.getType(), value);

    return new FindMultipleElementsTransformingAtom(
        findElementsScriptSimpleAtom, elementReferenceListAtom);
  }

  private static Map<String, String> makeLocatorJSON(Locator locator, String value) {
    checkNotNull(locator);
    checkNotNull(value);
    Map<String, String> map = Maps.newHashMap();
    map.put(locator.getType(), value);
    return map;
  }

  @VisibleForTesting
  static final class FindElementSimpleAtom extends SimpleAtom {
    @RemoteMsgField(order = 0)
    final String locatorType;

    @RemoteMsgField(order = 1)
    final String value;

    @RemoteMsgConstructor
    FindElementSimpleAtom(String locatorType, String value) {
      super(WebDriverAtomScripts.FIND_ELEMENT_ANDROID, SimpleAtom.ElementReferencePlacement.LAST);
      this.locatorType = locatorType;
      this.value = value;
    }

    @Override
    protected List<Object> getNonContextualArguments() {
      final Map<String, String> locatorJson = makeLocatorJSON(Locator.forType(locatorType), value);
      return Lists.newArrayList((Object) locatorJson);
    }
  }

  @VisibleForTesting
  static final class FindElementTransformingAtom
      extends TransformingAtom<Evaluation, ElementReference> {
    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 0)
    private final Atom<Evaluation> findElementSimpleAtom;

    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 1)
    private final TransformingAtom.Transformer<Evaluation, ElementReference> castOrDieAtom;

    @RemoteMsgConstructor
    private FindElementTransformingAtom(
        Atom<Evaluation> findElementSimpleAtom,
        TransformingAtom.Transformer<Evaluation, ElementReference> castOrDieAtom) {
      super(findElementSimpleAtom, castOrDieAtom);
      this.findElementSimpleAtom = findElementSimpleAtom;
      this.castOrDieAtom = castOrDieAtom;
    }
  }

  @VisibleForTesting
  static final class ClearElementSimpleAtom extends SimpleAtom {
    @RemoteMsgConstructor
    private ClearElementSimpleAtom() {
      super(WebDriverAtomScripts.CLEAR_ANDROID);
    }

    @Override
    public void handleNoElementReference() {
      throw new RuntimeException("clearElement: Need an element to clear!");
    }
  }

  @VisibleForTesting
  static final class WebKeysSimpleAtom extends SimpleAtom {
    @RemoteMsgField(order = 0)
    private final String text;

    @RemoteMsgConstructor
    private WebKeysSimpleAtom(String text) {
      super(WebDriverAtomScripts.SEND_KEYS_ANDROID);
      this.text = text;
    }

    @Override
    public void handleNoElementReference() {
      throw new RuntimeException("webKeys: Need an element to type on!");
    }

    @Override
    public List<Object> getNonContextualArguments() {
      return Lists.newArrayList((Object) text);
    }
  }

  @VisibleForTesting
  static final class WebScrollIntoViewAtom extends TransformingAtom<Evaluation, Boolean> {
    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 0)
    private final Atom<Evaluation> scrollIntoViewSimpleAtom;

    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 1)
    private final TransformingAtom.Transformer<Evaluation, Boolean> castOrDieAtom;

    @RemoteMsgConstructor
    private WebScrollIntoViewAtom(
        Atom<Evaluation> scrollIntoViewSimpleAtom,
        TransformingAtom.Transformer<Evaluation, Boolean> castOrDieAtom) {
      super(scrollIntoViewSimpleAtom, castOrDieAtom);
      this.scrollIntoViewSimpleAtom = scrollIntoViewSimpleAtom;
      this.castOrDieAtom = castOrDieAtom;
    }
  }

  static final class WebScrollIntoViewSimpleAtom extends SimpleAtom {
    @RemoteMsgConstructor
    private WebScrollIntoViewSimpleAtom() {
      super(WebDriverAtomScripts.SCROLL_INTO_VIEW_ANDROID);
    }

    @Override
    public void handleNoElementReference() {
      throw new RuntimeException("scrollIntoView: need an element to scroll to");
    }
  }

  @VisibleForTesting
  static final class WebClickSimpleAtom extends SimpleAtom {
    @RemoteMsgConstructor
    private WebClickSimpleAtom() {
      super(WebDriverAtomScripts.CLICK_ANDROID);
    }

    @Override
    public void handleNoElementReference() {
      throw new RuntimeException("webClick: Need an element to click on!");
    }
  }

  @VisibleForTesting
  static final class GetTextTransformingAtom extends TransformingAtom<Evaluation, String> {
    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 0)
    private final Atom<Evaluation> getTextSimpleAtom;

    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 1)
    private final TransformingAtom.Transformer<Evaluation, String> castOrDieAtom;

    @RemoteMsgConstructor
    private GetTextTransformingAtom(
        Atom<Evaluation> findElementSimpleAtom,
        TransformingAtom.Transformer<Evaluation, String> castOrDieAtom) {
      super(findElementSimpleAtom, castOrDieAtom);
      this.getTextSimpleAtom = findElementSimpleAtom;
      this.castOrDieAtom = castOrDieAtom;
    }
  }

  @VisibleForTesting
  static final class GetVisibleTextSimpleAtom extends SimpleAtom {
    @RemoteMsgConstructor
    private GetVisibleTextSimpleAtom() {
      super(WebDriverAtomScripts.GET_VISIBLE_TEXT_ANDROID);
    }
  }

  @VisibleForTesting
  static final class ActiveElementSimpleAtom extends SimpleAtom {
    @RemoteMsgConstructor
    private ActiveElementSimpleAtom() {
      super(WebDriverAtomScripts.ACTIVE_ELEMENT_ANDROID);
    }
  }

  @VisibleForTesting
  static final class SelectActiveElementTransformingAtom
      extends TransformingAtom<Evaluation, ElementReference> {
    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 0)
    private final Atom<Evaluation> selectActiveElementSimpleAtom;

    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 1)
    private final TransformingAtom.Transformer<Evaluation, ElementReference> castOrDieAtom;

    @RemoteMsgConstructor
    private SelectActiveElementTransformingAtom(
        Atom<Evaluation> selectActiveElementSimpleAtom,
        TransformingAtom.Transformer<Evaluation, ElementReference> castOrDieAtom) {
      super(selectActiveElementSimpleAtom, castOrDieAtom);
      this.selectActiveElementSimpleAtom = selectActiveElementSimpleAtom;
      this.castOrDieAtom = castOrDieAtom;
    }
  }

  @VisibleForTesting
  static final class FrameByIndexSimpleAtom extends SimpleAtom {
    @RemoteMsgField(order = 0)
    private final int index;

    @RemoteMsgConstructor
    private FrameByIndexSimpleAtom(int index) {
      super(WebDriverAtomScripts.FRAME_BY_INDEX_ANDROID);
      this.index = index;
    }

    @Override
    public List<Object> getNonContextualArguments() {
      return Lists.newArrayList((Object) index);
    }
  }

  @VisibleForTesting
  static final class FrameByIndexWithRootSimpleAtom extends SimpleAtom {
    @RemoteMsgField(order = 0)
    private final int index;

    @RemoteMsgField(order = 1)
    private final WindowReference root;

    @RemoteMsgConstructor
    private FrameByIndexWithRootSimpleAtom(int index, WindowReference root) {
      super(WebDriverAtomScripts.FRAME_BY_INDEX_ANDROID);
      this.index = index;
      this.root = root;
    }

    @Override
    public List<Object> getNonContextualArguments() {
      List<Object> args = Lists.newArrayList((Object) index);
      args.add(root);
      return args;
    }
  }

  @VisibleForTesting
  static final class SelectFrameByIndexTransformingAtom
      extends TransformingAtom<Evaluation, WindowReference> {
    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 0)
    private final Atom<Evaluation> frameByIndexSimpleAtom;

    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 1)
    private final TransformingAtom.Transformer<Evaluation, WindowReference> castOrDieAtom;

    @RemoteMsgConstructor
    private SelectFrameByIndexTransformingAtom(
        Atom<Evaluation> selectActiveElementSimpleAtom,
        TransformingAtom.Transformer<Evaluation, WindowReference> castOrDieAtom) {
      super(selectActiveElementSimpleAtom, castOrDieAtom);
      this.frameByIndexSimpleAtom = selectActiveElementSimpleAtom;
      this.castOrDieAtom = castOrDieAtom;
    }
  }

  @VisibleForTesting
  static final class FrameByIdOrNameSimpleAtom extends SimpleAtom {
    @RemoteMsgField(order = 0)
    private final String idOrName;

    @RemoteMsgConstructor
    private FrameByIdOrNameSimpleAtom(String idOrName) {
      super(WebDriverAtomScripts.FRAME_BY_ID_OR_NAME_ANDROID);
      this.idOrName = idOrName;
    }

    @Override
    public List<Object> getNonContextualArguments() {
      return Lists.newArrayList((Object) idOrName);
    }
  }

  @VisibleForTesting
  static final class FrameByIdOrNameWithRootSimpleAtom extends SimpleAtom {
    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 0)
    private final String idOrName;

    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 1)
    private final WindowReference root;

    @RemoteMsgConstructor
    private FrameByIdOrNameWithRootSimpleAtom(String idOrName, WindowReference root) {
      super(WebDriverAtomScripts.FRAME_BY_ID_OR_NAME_ANDROID);
      this.idOrName = idOrName;
      this.root = root;
    }

    @Override
    public List<Object> getNonContextualArguments() {
      List<Object> args = Lists.newArrayList((Object) idOrName);
      args.add(root);
      return args;
    }
  }

  @VisibleForTesting
  static final class SelectFrameByIdOrNameTransformingAtom
      extends TransformingAtom<Evaluation, WindowReference> {
    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 0)
    private final Atom<Evaluation> frameByIndexOrNameSimpleAtom;

    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 1)
    private final TransformingAtom.Transformer<Evaluation, WindowReference> castOrDieAtom;

    @RemoteMsgConstructor
    private SelectFrameByIdOrNameTransformingAtom(
        Atom<Evaluation> selectActiveElementSimpleAtom,
        TransformingAtom.Transformer<Evaluation, WindowReference> castOrDieAtom) {
      super(selectActiveElementSimpleAtom, castOrDieAtom);
      this.frameByIndexOrNameSimpleAtom = selectActiveElementSimpleAtom;
      this.castOrDieAtom = castOrDieAtom;
    }
  }

  @VisibleForTesting
  static final class FindElementsScriptSimpleAtom extends SimpleAtom {
    @RemoteMsgField(order = 0)
    final String locatorType;

    @RemoteMsgField(order = 1)
    final String value;

    @RemoteMsgConstructor
    private FindElementsScriptSimpleAtom(String locatorType, String value) {
      super(WebDriverAtomScripts.FIND_ELEMENTS_ANDROID);
      this.locatorType = locatorType;
      this.value = value;
    }

    @Override
    public List<Object> getNonContextualArguments() {
      return Lists.newArrayList((Object) makeLocatorJSON(Locator.forType(locatorType), value));
    }
  }

  @VisibleForTesting
  static final class FindMultipleElementsTransformingAtom
      extends TransformingAtom<Evaluation, List<ElementReference>> {
    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 0)
    private final Atom<Evaluation> findElementsScriptSimpleAtom;

    @SuppressWarnings("unused") // called reflectively
    @RemoteMsgField(order = 1)
    private final TransformingAtom.Transformer<Evaluation, List<ElementReference>>
        elementReferenceListAtom;

    @RemoteMsgConstructor
    private FindMultipleElementsTransformingAtom(
        Atom<Evaluation> findElementsScriptSimpleAtom,
        TransformingAtom.Transformer<Evaluation, List<ElementReference>> elementReferenceListAtom) {
      super(findElementsScriptSimpleAtom, elementReferenceListAtom);
      this.findElementsScriptSimpleAtom = findElementsScriptSimpleAtom;
      this.elementReferenceListAtom = elementReferenceListAtom;
    }
  }

  static final class ElementReferenceListAtom
      implements TransformingAtom.Transformer<Evaluation, List<ElementReference>> {

    @RemoteMsgField(order = 0)
    final String locatorType;

    @RemoteMsgField(order = 1)
    final String value;

    @RemoteMsgConstructor
    private ElementReferenceListAtom(String locatorType, String value) {
      this.locatorType = locatorType;
      this.value = value;
    }

    @Override
    public List<ElementReference> apply(Evaluation e) {
      Object rawValues = e.getValue();
      if (null == rawValues) {
        return Lists.newArrayList();
      }
      if (rawValues instanceof Iterable) {
        List<ElementReference> references = Lists.newArrayList();
        for (Object rawValue : ((Iterable) rawValues)) {
          if (rawValue instanceof ElementReference) {
            references.add((ElementReference) rawValue);
          } else {
            throw new RuntimeException(
                String.format(
                    "Unexpected non-elementReference in findMultipleElements(%s, %s): "
                        + "(%s) all: %s ",
                    Locator.forType(locatorType).name(), value, rawValue, e));
          }
        }
        return references;
      } else {
        throw new RuntimeException(
            String.format(
                "Unexpected non-iterableType in findMultipleElements(%s, %s): "
                    + "return evaluation: %s ",
                Locator.forType(locatorType).name(), value, e));
      }
    }
  }
}