All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hubspot.chrome.devtools.client.ChromeDevToolsSession Maven / Gradle / Ivy

There is a newer version: 94.0.4606.61
Show newest version
package com.hubspot.chrome.devtools.client;

import java.io.IOException;
import java.net.URI;
import java.util.Base64;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.common.base.Predicates;
import com.hubspot.chrome.devtools.base.ChromeRequest;
import com.hubspot.chrome.devtools.base.ChromeResponse;
import com.hubspot.chrome.devtools.base.ChromeSessionCore;
import com.hubspot.chrome.devtools.client.core.accessibility.Accessibility;
import com.hubspot.chrome.devtools.client.core.animation.Animation;
import com.hubspot.chrome.devtools.client.core.applicationcache.ApplicationCache;
import com.hubspot.chrome.devtools.client.core.audits.Audits;
import com.hubspot.chrome.devtools.client.core.browser.Browser;
import com.hubspot.chrome.devtools.client.core.cachestorage.CacheStorage;
import com.hubspot.chrome.devtools.client.core.css.CSS;
import com.hubspot.chrome.devtools.client.core.database.Database;
import com.hubspot.chrome.devtools.client.core.debugger.Debugger;
import com.hubspot.chrome.devtools.client.core.deviceorientation.DeviceOrientation;
import com.hubspot.chrome.devtools.client.core.dom.BoxModel;
import com.hubspot.chrome.devtools.client.core.dom.DOM;
import com.hubspot.chrome.devtools.client.core.dom.NodeId;
import com.hubspot.chrome.devtools.client.core.dom.Quad;
import com.hubspot.chrome.devtools.client.core.domdebugger.DOMDebugger;
import com.hubspot.chrome.devtools.client.core.domsnapshot.DOMSnapshot;
import com.hubspot.chrome.devtools.client.core.emulation.Emulation;
import com.hubspot.chrome.devtools.client.core.headlessexperimental.HeadlessExperimental;
import com.hubspot.chrome.devtools.client.core.heapprofiler.HeapProfiler;
import com.hubspot.chrome.devtools.client.core.indexeddb.IndexedDB;
import com.hubspot.chrome.devtools.client.core.input.Input;
import com.hubspot.chrome.devtools.client.core.inspector.Inspector;
import com.hubspot.chrome.devtools.client.core.io.IO;
import com.hubspot.chrome.devtools.client.core.layertree.LayerTree;
import com.hubspot.chrome.devtools.client.core.log.Log;
import com.hubspot.chrome.devtools.client.core.memory.Memory;
import com.hubspot.chrome.devtools.client.core.network.Network;
import com.hubspot.chrome.devtools.client.core.overlay.Overlay;
import com.hubspot.chrome.devtools.client.core.page.FrameId;
import com.hubspot.chrome.devtools.client.core.page.NavigateResult;
import com.hubspot.chrome.devtools.client.core.page.Page;
import com.hubspot.chrome.devtools.client.core.performance.Performance;
import com.hubspot.chrome.devtools.client.core.profiler.Profiler;
import com.hubspot.chrome.devtools.client.core.runtime.CallArgument;
import com.hubspot.chrome.devtools.client.core.runtime.CallFunctionOnResult;
import com.hubspot.chrome.devtools.client.core.runtime.EvaluateResult;
import com.hubspot.chrome.devtools.client.core.runtime.RemoteObject;
import com.hubspot.chrome.devtools.client.core.runtime.RemoteObjectId;
import com.hubspot.chrome.devtools.client.core.runtime.Runtime;
import com.hubspot.chrome.devtools.client.core.security.Security;
import com.hubspot.chrome.devtools.client.core.serviceworker.ServiceWorker;
import com.hubspot.chrome.devtools.client.core.storage.Storage;
import com.hubspot.chrome.devtools.client.core.systeminfo.SystemInfo;
import com.hubspot.chrome.devtools.client.core.target.Target;
import com.hubspot.chrome.devtools.client.core.tethering.Tethering;
import com.hubspot.chrome.devtools.client.core.tracing.Tracing;
import com.hubspot.chrome.devtools.client.exceptions.ChromeDevToolsException;

public class ChromeDevToolsSession implements ChromeSessionCore {
  private final Logger LOG = LoggerFactory.getLogger(ChromeDevToolsSession.class);

  public static final long DEFAULT_TIMEOUT_MILLIS = 10000L;
  public static final long DEFAULT_PERIOD_MILLIS = 10L;

  private final ChromeWebSocketClient websocket;
  private final ObjectMapper objectMapper;
  private final ExecutorService executorService;
  private final UUID id;

  private final Map chromeEventListeners;

  public ChromeDevToolsSession(URI uri,
                               ObjectMapper objectMapper,
                               ExecutorService executorService,
                               long actionTimeoutMillis) {
    this.chromeEventListeners = new ConcurrentHashMap<>();
    this.websocket = new ChromeWebSocketClient(uri, objectMapper, chromeEventListeners, executorService, actionTimeoutMillis);
    this.objectMapper = objectMapper;
    this.executorService = executorService;
    this.id = UUID.randomUUID();

    try {
      this.websocket.connectBlocking();
    } catch (Throwable t) {
      throw new ChromeDevToolsException(String.format("Could not connect to uri %s", uri), t);
    }
  }

  @Override
  public void send(ChromeRequest request) {
    sendChromeRequest(request);
    websocket.getResponse(request.getId());
  }

  @Override
  public  T send(ChromeRequest request, TypeReference valueType) {
    sendChromeRequest(request);
    ChromeResponse response = websocket.getResponse(request.getId());
    return parseChromeResponse(response, valueType);
  }

  public CompletableFuture sendAsync(ChromeRequest request) {
    return CompletableFuture.runAsync(() -> {
      sendChromeRequest(request);
      websocket.getResponse(request.getId());
    }, executorService);
  }

  @Override
  public  CompletableFuture sendAsync(ChromeRequest request, TypeReference valueType) {
    return CompletableFuture.supplyAsync(() -> {
      sendChromeRequest(request);
      return parseChromeResponse(websocket.getResponse(request.getId()), valueType);
    }, executorService);
  }

  private void sendChromeRequest(ChromeRequest request) {
    try {
      String json = objectMapper.writeValueAsString(request);
      LOG.trace("Sending request: {}", json);
      websocket.send(json);
    } catch (IOException e) {
      throw new ChromeDevToolsException(e);
    }
  }

  private  T parseChromeResponse(ChromeResponse response, TypeReference valueType) {
    // Most methods return a single element of data. To eliminate the user needing to access this
    // single element via a pass through method, we skip the root node and map the element's data right
    // into the data structure we want (i.e. we don't parse the root node itself).
    //
    //   e.g. { "result" : { "browserContextId" : "some_id" } }
    //                               ^--- ignore      ^--- consume directly so user can act directly on string
    //
    //       Allows the user to do `callingMethod()` instead of `callingMethod().getBrowserContextId()`.
    //
    // If this fails, then the method is one of the few cases where there are multiple
    // results that need to be mapped into a parent data structure.
    //
    //   e.g. { "result" : { "protocolVersion" : "1.2.3" }, { "jsVersion" : "6.6.8" } }
    //                    |__________________________________________________________|
    //                                       `--------- must consume all so user can select which element to work with
    //
    //       Here the user must do `callingMethod().getProtocolVersion()` or `someMethod().getJsVersion()`.
    Iterator elements = response.getResult().elements();
    JsonNode first = elements.next();
    try {
      // We do our best to predict which kind of result to consume the response as, but there's
      // a small chance that a multi-result response has optional, absent members, and we try and
      // fail to parse it as a single-result response, which is why we catch the inner JsonMappingException.
      if (elements.hasNext()) {
        return objectMapper.readValue(response.getResult().toString(), valueType);
      } else {
        return objectMapper.readValue(objectMapper.treeAsTokens(first), valueType);
      }
    } catch (JsonMappingException e) {
      try {
        return objectMapper.readValue(response.getResult().toString(), valueType);
      } catch (IOException e1) {
        throw new ChromeDevToolsException(e1);
      }
    } catch (IOException e2) {
      throw new ChromeDevToolsException(e2);
    }
  }

  @Override
  public void close() throws Exception {
    chromeEventListeners.clear();
    websocket.closeBlocking();
  }

  public boolean isConnected() {
    return websocket.isOpen();
  }

  public void waitDocumentReady() {
    waitDocumentReady(DEFAULT_TIMEOUT_MILLIS, DEFAULT_PERIOD_MILLIS);
  }

  public void waitDocumentReady(long timeoutMillis) {
    waitDocumentReady(timeoutMillis, DEFAULT_PERIOD_MILLIS);
  }

  public void waitDocumentReady(long timeoutMillis, long periodMillis) {
    Retryer retryer = RetryerBuilder.newBuilder()
        .retryIfResult(Predicates.equalTo(false))
        .withStopStrategy(StopStrategies.stopAfterDelay(timeoutMillis))
        .withWaitStrategy(WaitStrategies.fixedWait(periodMillis, TimeUnit.MILLISECONDS))
        .build();
    try {
      retryer.call(() -> (Boolean) evaluate("document.readyState === \"complete\"").result.getValue());
    } catch (ExecutionException| RetryException e) {
      throw new ChromeDevToolsException(e);
    }
  }

  public boolean waitUntil(Predicate predicate) {
    return waitUntil(predicate, DEFAULT_TIMEOUT_MILLIS);
  }

  public boolean waitUntil(Predicate predicate, long timeoutMillis) {
    return waitUntil(predicate, timeoutMillis, DEFAULT_PERIOD_MILLIS);
  }

  public boolean waitUntil(Predicate predicate, long timeoutMillis, long periodMillis) {
    Retryer retryer = RetryerBuilder.newBuilder()
        .retryIfResult(Predicates.equalTo(false))
        .withStopStrategy(StopStrategies.stopAfterDelay(timeoutMillis))
        .withWaitStrategy(WaitStrategies.fixedWait(periodMillis, TimeUnit.MILLISECONDS))
        .build();

    try {
      retryer.call(() -> predicate.test(this));
    } catch (ExecutionException | RetryException e) {
      return false;
    }
    return true;
  }

  public NavigateResult navigate(String url) {
    return getPage().navigate(url, null, null, null);
  }

  public String getUrl() {
    return getDOM().getDocument(null, null).getDocumentURL();
  }

  public EvaluateResult evaluate(String javascript) {
    return getRuntime().evaluate(javascript, null, null, null, null, null, null, null, null);
  }

  public FrameId getFrameId() {
    return getDOM().getDocument(null, null).getFrameId();
  }

  public List getNodeIds(String selector) {
    return getDOM().querySelectorAll(getDOM().getDocument(1, null).getNodeId(), selector);
  }

  public NodeId getNodeId(String selector) {
    List nodeIds = getNodeIds(selector);
    return nodeIds.isEmpty() ? null : nodeIds.get(0);
  }

  public byte[] captureScreenshot() {
    return captureScreenshot(FileExtension.PNG);
  }

  public byte[] captureScreenshot(FileExtension extension) {
    String data = getPage().captureScreenshot(extension.name().toLowerCase(), null, null, null);
    return Base64.getDecoder().decode(data);
  }

  public byte[] printToPDF() {
    String data = getPage().printToPDF(null, null, null, null,
        null, null, null, null, null, null,
        null, null, null, null, null);
    return Base64.getDecoder().decode(data);
  }

  public String getId() {
    return id.toString();
  }

  public void addEventListener(String listenerId, ChromeEventListener chromeEventListener) {
    if (chromeEventListener != null && listenerId != null) {
      chromeEventListeners.put(listenerId, chromeEventListener);
    } else {
      LOG.warn("Event listener or listenerId was null, not adding");
    }
  }

  public void removeEventListener(String listenerId) {
    if (listenerId != null) {
      chromeEventListeners.remove(listenerId);
    }
  }

  public Object getProperty(String selector, String property) {
    RemoteObjectId remoteObjectId = getObjectId(selector);
    if (remoteObjectId == null) {
      return null;
    }
    System.out.println(remoteObjectId);
    return getValueFromObjectId(remoteObjectId, property);
  }

  public Object getValueFromObjectId(RemoteObjectId remoteObjectId, String property) {
    CallFunctionOnResult callfunction = getRuntime().callFunctionOn(
        "function(property) { return property.split('.').reduce((o, i) => o[i], this); }",
        remoteObjectId,
        Collections.singletonList(CallArgument.builder().setValue(property).build()),
        false, true, false, false, null, null, null
    );
    if (callfunction == null || callfunction.result == null) {
      return null;
    }
    RemoteObject result = callfunction.result;
    getRuntime().releaseObject(remoteObjectId);
    return result.getValue();
  }

  public RemoteObjectId getObjectId(String selector) {
    DOM dom = getDOM();
    NodeId root = dom.getDocument(null, null).getNodeId();
    if (root == null) {
      return null;
    }
    NodeId selectedNodeId = dom.querySelector(root, selector);
    if (selectedNodeId == null) {
      return null;
    }
    RemoteObject remoteObject = dom.resolveNode(selectedNodeId, null, null);
    if (remoteObject == null) {
      return null;
    }
    return remoteObject.getObjectId();
  }

  public String getLocation() {
    return getDOM().getDocument(null, null).getDocumentURL();
  }

  public boolean click(String selector) {
    NodeId selectedNodeId = getNodeId(selector);
    if (selectedNodeId == null) {
      return false;
    }
    BoxModel boxModel = getDOM().getBoxModel(selectedNodeId, null, null);
    if (boxModel == null) {
      return false;
    }
    Quad content = boxModel.getContent();
    if (content == null || content.getValue().isEmpty() || content.getValue().size() < 2) {
      return false;
    }
    double left = Math.floor(content.getValue().get(0).doubleValue());
    double top  = Math.floor(content.getValue().get(1).doubleValue());
    int clickCount = 1;
    Input input = getInput();
    input.dispatchMouseEvent("mousePressed", left, top, null, null, "left", clickCount, null, null);
    input.dispatchMouseEvent("mouseReleased", left, top, null, null, "left", clickCount, null, null);
    return true;
  }

  @Override
  public String toString() {
    return "ChromeSessionID:" + getId();
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == null) {
      return false;
    }
    if (!ChromeDevToolsSession.class.isAssignableFrom(obj.getClass())) {
      return false;
    }
    final ChromeDevToolsSession other = (ChromeDevToolsSession) obj;
    return getId().equals(other.getId());
  }

  @Override
  public int hashCode() {
    return Objects.hash(websocket);
  }

  // Note: I've left out two deprecated classes. This could make it harder for users
  // to transition, but if they're transitioning to this anyways, they may as well.
  public Accessibility getAccessibility() { return new Accessibility(this, objectMapper); }
  public Animation getAnimation() { return new Animation(this, objectMapper); }
  public ApplicationCache getApplicationCache() { return new ApplicationCache(this, objectMapper); }
  public Audits getAudits() { return new Audits(this, objectMapper); }
  public Browser getBrowser() { return new Browser(this, objectMapper); }
  public CacheStorage getCacheStorage() { return new CacheStorage(this, objectMapper); }
  public CSS getCSS() { return new CSS(this, objectMapper); }
  public Database getDatabase() { return new Database(this, objectMapper); }
  public Debugger getDebugger() { return new Debugger(this, objectMapper); }
  public DeviceOrientation getDeviceOrientation() { return new DeviceOrientation(this, objectMapper); }
  public DOM getDOM() { return new DOM(this, objectMapper); }
  public DOMDebugger getDOMDebugger() { return new DOMDebugger(this, objectMapper); }
  public DOMSnapshot getDOMSnapshot() { return new DOMSnapshot(this, objectMapper); }
  public Emulation getEmulation() { return new Emulation(this, objectMapper); }
  public HeadlessExperimental getHeadlessExperimental() { return new HeadlessExperimental(this, objectMapper); }
  public HeapProfiler getHeapProfiler() { return new HeapProfiler(this, objectMapper); }
  public IndexedDB getIndexedDB() { return new IndexedDB(this, objectMapper); }
  public Input getInput() { return new Input(this, objectMapper); }
  public Inspector getInspector() { return new Inspector(this, objectMapper); }
  public IO getIO() { return new IO(this, objectMapper); }
  public LayerTree getLayerTree() { return new LayerTree(this, objectMapper); }
  public Log getLog() { return new Log(this, objectMapper); }
  public Memory getMemory() { return new Memory(this, objectMapper); }
  public Network getNetwork() { return new Network(this, objectMapper); }
  public Overlay getOverlay() { return new Overlay(this, objectMapper); }
  public Page getPage() { return new Page(this, objectMapper); }
  public Performance getPerformance() { return new Performance(this, objectMapper); }
  public Profiler getProfiler() { return new Profiler(this, objectMapper); }
  public Runtime getRuntime() { return new Runtime(this, objectMapper); }
  public Security getSecurity() { return new Security(this, objectMapper); }
  public ServiceWorker getServiceWorker() { return new ServiceWorker(this, objectMapper); }
  public Storage getStorage() { return new Storage(this, objectMapper); }
  public SystemInfo getSystemInfo() { return new SystemInfo(this, objectMapper); }
  public Target getTarget() { return new Target(this, objectMapper); }
  public Tethering getTethering() { return new Tethering(this, objectMapper); }
  public Tracing getTracing() { return new Tracing(this, objectMapper); }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy