Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.intuit.karate.driver.DevToolsDriver Maven / Gradle / Ivy
/*
* The MIT License
*
* Copyright 2022 Karate Labs Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.intuit.karate.driver;
import com.intuit.karate.Constants;
import com.intuit.karate.FileUtils;
import com.intuit.karate.Json;
import com.intuit.karate.JsonUtils;
import com.intuit.karate.Logger;
import com.intuit.karate.StringUtils;
import com.intuit.karate.core.Feature;
import com.intuit.karate.core.FeatureCall;
import com.intuit.karate.core.MockHandler;
import com.intuit.karate.core.ScenarioEngine;
import com.intuit.karate.core.Variable;
import com.intuit.karate.graal.JsValue;
import com.intuit.karate.http.HttpRequest;
import com.intuit.karate.http.ResourceType;
import com.intuit.karate.http.Response;
import com.intuit.karate.http.WebSocketClient;
import com.intuit.karate.http.WebSocketOptions;
import com.intuit.karate.shell.Command;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import com.jayway.jsonpath.PathNotFoundException;
import org.graalvm.polyglot.Value;
/**
*
* @author pthomas3
*/
public abstract class DevToolsDriver implements Driver {
protected final DriverOptions options;
protected final Command command;
protected final WebSocketClient client;
private boolean terminated;
private final DevToolsWait wait;
protected final String rootFrameId;
private Integer windowId;
private String windowState;
protected String sessionId;
protected String mainFrameId;
// iframe support
private Frame frame;
private final Map frameContexts = new HashMap();
private final Map frameSessions = new HashMap();
protected boolean domContentEventFired;
protected final Set framesStillLoading = new HashSet();
private boolean submit;
protected String currentDialogText;
private int nextId;
public int nextId() {
return ++nextId;
}
private MockHandler mockHandler;
protected final Logger logger;
protected DevToolsDriver(DriverOptions options, Command command, String webSocketUrl) {
logger = options.driverLogger;
this.options = options;
this.command = command;
if (options.isRemoteHost()) {
String host = options.host;
Integer port = options.port;
webSocketUrl = webSocketUrl.replace("ws://localhost/", "ws://" + host + ":" + port + "/");
}
this.wait = new DevToolsWait(this, options);
int pos = webSocketUrl.lastIndexOf('/');
rootFrameId = webSocketUrl.substring(pos + 1);
mainFrameId = rootFrameId;
logger.debug("root frame id: {}", rootFrameId);
WebSocketOptions wsOptions = new WebSocketOptions(webSocketUrl);
wsOptions.setMaxPayloadSize(options.maxPayloadSize);
wsOptions.setTextConsumer(text -> {
if (logger.isTraceEnabled()) {
logger.trace("<< {}", text);
} else {
// to avoid swamping the console when large base64 encoded binary responses happen
logger.debug("<< {}", StringUtils.truncate(text, 1024, true));
}
Map map = Json.of(text).value();
DevToolsMessage dtm = new DevToolsMessage(this, map);
receive(dtm);
});
client = new WebSocketClient(wsOptions, logger);
}
@Override
public Driver timeout(Integer millis) {
options.setTimeout(millis);
return this;
}
@Override
public Driver timeout() {
return timeout(null);
}
public DevToolsMessage method(String method) {
return new DevToolsMessage(this, method);
}
// this can be used for exploring / sending any raw message !
public Map send(Map map) {
DevToolsMessage dtm = new DevToolsMessage(this, map);
dtm.setId(nextId());
return sendAndWait(dtm, null).toMap();
}
public void send(DevToolsMessage dtm) {
String json = JsonUtils.toJson(dtm.toMap());
logger.debug(">> {}", json);
client.send(json);
}
public DevToolsMessage sendAndWait(DevToolsMessage dtm, Predicate condition) {
boolean wasSubmit = submit;
if (condition == null && submit) {
submit = false;
condition = DevToolsWait.ALL_FRAMES_LOADED;
}
// do stuff inside wait to avoid missing messages
DevToolsMessage result = wait.send(dtm, condition);
if (result == null && !wasSubmit) {
if (condition == DevToolsWait.ALL_FRAMES_LOADED) {
logger.error("failed to get reply for :" + dtm + ". Will try to check by running a script.");
boolean readyState = (Boolean) this.script("document.readyState === 'complete'");
if (!readyState) {
throw new RuntimeException("failed to get reply for: " + dtm);
} else {
logger.warn("document is ready, but no reply for: " + dtm + " with a ready event received. Will proceed.");
}
} else {
throw new RuntimeException("failed to get reply for: " + dtm);
}
}
return result;
}
public void receive(DevToolsMessage dtm) {
if (dtm.methodIs("Page.domContentEventFired")) {
domContentEventFired = true;
logger.trace("** set dom ready flag to true");
}
if (dtm.methodIs("Page.javascriptDialogOpening")) {
currentDialogText = dtm.getParam("message");
// this will stop waiting NOW
wait.setCondition(DevToolsWait.DIALOG_OPENING);
}
if (dtm.methodIs("Page.frameStartedLoading")) {
String frameLoadingId = dtm.getParam("frameId");
if (rootFrameId.equals(frameLoadingId)) { // root page is loading
domContentEventFired = false;
framesStillLoading.clear();
logger.trace("** root frame started loading, cleared all page state: {}", frameLoadingId);
} else {
framesStillLoading.add(frameLoadingId);
logger.trace("** frame started loading, added to in-progress list: {}", framesStillLoading);
}
}
if (dtm.methodIs("Page.frameStoppedLoading")) {
String frameLoadedId = dtm.getParam("frameId");
framesStillLoading.remove(frameLoadedId);
logger.trace("** frame stopped loading: {}, remaining in-progress: {}", frameLoadedId, framesStillLoading);
}
if (dtm.methodIs("Page.frameNavigated")) {
Frame newFrame = new Frame(dtm.getParam("frame.id"), dtm.getParam("frame.url"), dtm.getParam("frame.name"));
logger.trace("** detected new frame: {}", newFrame);
if (frame != null && (frame.name.equals(newFrame.name) || frame.url.equals(newFrame.url))) {
logger.trace("** auto switching frame: {} -> {}", frame, newFrame);
frame = newFrame;
}
}
if (dtm.methodIs("Runtime.executionContextCreated")) {
String newFrameId = dtm.getParam("context.auxData.frameId");
Integer contextId = dtm.getParam("context.id");
frameContexts.put(newFrameId, contextId);
logger.trace("** new frame execution context: {} - {}", newFrameId, contextId);
}
if (dtm.methodIs("Runtime.executionContextsCleared")) {
frame = null;
frameContexts.clear();
framesStillLoading.clear();
}
if (dtm.methodIs("Runtime.consoleAPICalled") && options.showBrowserLog) {
List values = dtm.getParam("args[*].value");
for (Object value : values) {
logger.debug("[console] {}", value);
}
}
if (dtm.methodIs("Fetch.requestPaused")) {
handleInterceptedRequest(dtm);
}
if (dtm.methodIs("Target.targetInfoChanged")) {
String frameId = dtm.getParam("targetInfo.targetId");
Integer frameContextId = frameContexts.get(frameId);
if (frameContextId != null) {
logger.trace("** removed stale execution context: {} - {}", frameId, frameContextId);
frameContexts.remove(frameId);
}
}
if (dtm.methodIs("Target.attachedToTarget")) {
String targetType = dtm.getParam("targetInfo.type");
if ("iframe".equals(targetType) || "frame".equals(targetType)) {
String targetSessionId = dtm.getParam("sessionId");
String targetFrameId = dtm.getParam("targetInfo.targetId");
frameSessions.put(targetFrameId, targetSessionId);
logger.trace("** new frame session: {} - {}", targetFrameId, targetSessionId);
}
}
// all needed state is set above before we get into conditional checks
wait.receive(dtm);
}
private void handleInterceptedRequest(DevToolsMessage dtm) {
String requestId = dtm.getParam("requestId");
String requestUrl = dtm.getParam("request.url");
if (mockHandler != null) {
String method = dtm.getParam("request.method");
Map headers = dtm.getParam("request.headers");
String postData = dtm.getParam("request.postData");
logger.debug("intercepting request for url: {}", requestUrl);
HttpRequest request = new HttpRequest();
request.setUrl(requestUrl);
request.setMethod(method);
headers.forEach((k, v) -> request.putHeader(k, v));
if (postData != null) {
request.setBody(FileUtils.toBytes(postData));
} else {
request.setBody(Constants.ZERO_BYTES);
}
Response response = mockHandler.handle(request.toRequest());
String responseBody = response.getBody() == null ? "" : Base64.getEncoder().encodeToString(response.getBody());
List responseHeaders = new ArrayList();
Map> map = response.getHeaders();
if (map != null) {
map.forEach((k, v) -> {
if (v != null && !v.isEmpty()) {
Map nv = new HashMap(2);
nv.put("name", k);
nv.put("value", v.get(0));
responseHeaders.add(nv);
}
});
}
method("Fetch.fulfillRequest")
.param("requestId", requestId)
.param("responseCode", response.getStatus())
.param("responseHeaders", responseHeaders)
.param("body", responseBody).sendWithoutWaiting();
} else {
logger.warn("no mock server, continuing paused request to url: {}", requestUrl);
method("Fetch.continueRequest").param("requestId", requestId).sendWithoutWaiting();
}
}
//==========================================================================
//
private DevToolsMessage evalOnce(String expression, boolean quickly, boolean fireAndForget, boolean returnByValue) {
DevToolsMessage toSend = method("Runtime.evaluate")
.param("expression", expression);
if (returnByValue) {
toSend.param("returnByValue", true);
}
Integer contextId = getFrameContext();
if (contextId != null) {
toSend.param("contextId", contextId);
}
if (quickly) {
toSend.setTimeout(options.getRetryInterval());
}
if (fireAndForget) {
toSend.sendWithoutWaiting();
return null;
}
return toSend.send();
}
protected DevToolsMessage eval(String expression) {
return evalInternal(expression, false, true);
}
protected DevToolsMessage evalQuickly(String expression) {
return evalInternal(expression, true, true);
}
protected String evalForObjectId(String expression) {
return options.retry(() -> {
DevToolsMessage dtm = evalInternal(expression, true, false);
return dtm.getResult("objectId");
}, returned -> returned != null, "eval for object id: " + expression, true);
}
private DevToolsMessage evalInternal(String expression, boolean quickly, boolean returnByValue) {
DevToolsMessage dtm = evalOnce(expression, quickly, false, returnByValue);
if (dtm.isResultError()) {
Map error = dtm.getError();
if (error != null) {
Object errorCode = error.get("code");
if (errorCode instanceof Integer) {
if ((Integer) errorCode == -32000) { // Object reference chain is too long
dtm.setResult(Variable.NULL);
return dtm;
}
}
}
String message = "js eval failed once:" + expression
+ ", error: " + dtm.getResult();
logger.warn(message);
options.sleep();
dtm = evalOnce(expression, quickly, false, returnByValue); // no wait condition for the re-try
if (dtm.isResultError()) {
message = "js eval failed twice:" + expression
+ ", error: " + dtm.getResult();
logger.error(message);
throw new RuntimeException(message);
}
}
return dtm;
}
protected void retryIfEnabled(String locator) {
if (options.isRetryEnabled()) {
waitFor(locator); // will throw exception if not found
}
if (options.highlight) {
// highlight(locator, options.highlightDuration); // instead of this
String highlightJs = options.highlight(locator, options.highlightDuration);
evalOnce(highlightJs, true, true, true); // do it safely, i.e. fire and forget
}
}
protected int getRootNodeId() {
return method("DOM.getDocument").param("depth", 0).send().getResult("root.nodeId");
}
@Override
public String elementId(String locator) {
return evalForObjectId(DriverOptions.selector(locator));
}
@Override
public List elementIds(String locator) {
List elements = locateAll(locator);
List objectIds = new ArrayList(elements.size());
for (Element e : elements) {
String objectId = evalForObjectId(e.getLocator());
objectIds.add(objectId);
}
return objectIds;
}
@Override
public DriverOptions getOptions() {
return options;
}
private void attachAndActivate(String targetId, boolean isFrame) {
DevToolsMessage dtm = method("Target.attachToTarget").param("targetId", targetId).param("flatten", true).send();
sessionId = dtm.getResult("sessionId");
frameSessions.put(targetId, sessionId);
if (!isFrame) {
mainFrameId = targetId;
}
method("Target.activateTarget").param("targetId", targetId).send();
method("Target.setDiscoverTargets").param("discover", true).send();
}
@Override
public void activate() {
attachAndActivate(rootFrameId, false);
}
protected void initWindowIdAndState() {
DevToolsMessage dtm = method("Browser.getWindowForTarget").param("targetId", rootFrameId).send();
if (!dtm.isResultError()) {
windowId = dtm.getResultVariable("windowId").getValue();
windowState = (String) dtm.getResultVariable("bounds").getValue().get("windowState");
}
}
@Override
public Map getDimensions() {
DevToolsMessage dtm = method("Browser.getWindowForTarget").param("targetId", rootFrameId).send();
Map map = dtm.getResultVariable("bounds").getValue();
Integer x = (Integer) map.remove("left");
Integer y = (Integer) map.remove("top");
map.put("x", x);
map.put("y", y);
return map;
}
@Override
public void setDimensions(Map map) {
Integer left = (Integer) map.remove("x");
Integer top = (Integer) map.remove("y");
map.put("left", left);
map.put("top", top);
Map temp = getDimensions();
temp.putAll(map);
temp.remove("windowState");
method("Browser.setWindowBounds")
.param("windowId", windowId)
.param("bounds", temp).send();
}
public void emulateDevice(int width, int height, String userAgent) {
method("Network.setUserAgentOverride").param("userAgent", userAgent).send();
method("Emulation.setDeviceMetricsOverride")
.param("width", width)
.param("height", height)
.param("deviceScaleFactor", 1)
.param("mobile", true)
.send();
}
@Override
public void close() {
method("Page.close").sendWithoutWaiting();
sessionId = frameSessions.get(rootFrameId);
}
@Override
public void quit() {
if (terminated) {
return;
}
terminated = true;
// don't wait, may fail and hang
method("Target.closeTarget").param("targetId", rootFrameId).sendWithoutWaiting();
// method("Browser.close").send();
client.close();
if (command != null) {
command.close(true);
}
getRuntime().engine.setDriverToNull();
}
@Override
public boolean isTerminated() {
return terminated;
}
@Override
public void setUrl(String url) {
method("Page.navigate").param("url", url)
.send(DevToolsWait.ALL_FRAMES_LOADED);
}
@Override
public void refresh() {
method("Page.reload").send(DevToolsWait.ALL_FRAMES_LOADED);
}
@Override
public void reload() {
method("Page.reload").param("ignoreCache", true).send();
}
private void history(int delta) {
DevToolsMessage dtm = method("Page.getNavigationHistory").send();
int currentIndex = dtm.getResultVariable("currentIndex").getValue();
List list = dtm.getResultVariable("entries").getValue();
int targetIndex = currentIndex + delta;
if (targetIndex < 0 || targetIndex == list.size()) {
return;
}
Map entry = list.get(targetIndex);
Integer id = (Integer) entry.get("id");
method("Page.navigateToHistoryEntry").param("entryId", id).send(DevToolsWait.ALL_FRAMES_LOADED);
}
@Override
public void back() {
history(-1);
}
@Override
public void forward() {
history(1);
}
private void setWindowState(String state) {
if (!"normal".equals(windowState)) {
method("Browser.setWindowBounds")
.param("windowId", windowId)
.param("bounds", Collections.singletonMap("windowState", "normal"))
.send();
windowState = "normal";
}
if (!state.equals(windowState)) {
method("Browser.setWindowBounds")
.param("windowId", windowId)
.param("bounds", Collections.singletonMap("windowState", state))
.send();
windowState = state;
}
}
@Override
public void maximize() {
setWindowState("maximized");
}
@Override
public void minimize() {
setWindowState("minimized");
}
@Override
public void fullscreen() {
setWindowState("fullscreen");
}
@Override
public Element click(String locator) {
retryIfEnabled(locator);
eval(DriverOptions.selector(locator) + ".click()");
return DriverElement.locatorExists(this, locator);
}
@Override
public Element select(String locator, String text) {
retryIfEnabled(locator);
eval(options.optionSelector(locator, text));
return DriverElement.locatorExists(this, locator);
}
@Override
public Element select(String locator, int index) {
retryIfEnabled(locator);
eval(options.optionSelector(locator, index));
return DriverElement.locatorExists(this, locator);
}
@Override
public Driver submit() {
submit = true;
return this;
}
@Override
public Element focus(String locator) {
retryIfEnabled(locator);
eval(options.focusJs(locator));
return DriverElement.locatorExists(this, locator);
}
@Override
public Element clear(String locator) {
eval(DriverOptions.selector(locator) + ".value = ''");
return DriverElement.locatorExists(this, locator);
}
private void sendKey(Character c, int modifiers, String type, Integer keyCode) {
DevToolsMessage dtm = method("Input.dispatchKeyEvent")
.param("modifiers", modifiers)
.param("type", type);
if (keyCode == null) {
if (c != null) {
dtm.param("text", c.toString());
}
} else {
switch (keyCode) {
case 9: // tab
if ("char".equals(type)) {
return; // special case
}
dtm.param("text", "");
break;
case 13: // enter
dtm.param("text", "\r"); // important ! \n does NOT work for chrome
break;
case 46: // dot
if ("rawKeyDown".equals(type)) {
dtm.param("type", "keyDown"); // special case
}
dtm.param("text", ".");
break;
default:
if (c != null) {
dtm.param("text", c.toString());
}
}
dtm.param("windowsVirtualKeyCode", keyCode);
}
dtm.send();
}
@Override
public Element input(String locator, String value) {
retryIfEnabled(locator);
// focus
eval(options.focusJs(locator));
Input input = new Input(value);
while (input.hasNext()) {
char c = input.next();
int modifiers = input.getModifierFlags();
Integer keyCode = Keys.code(c);
if (keyCode != null) {
switch (keyCode) {
case Keys.CODE_SHIFT:
case Keys.CODE_CONTROL:
case Keys.CODE_ALT:
case Keys.CODE_META:
if (input.release) {
sendKey(null, modifiers, "keyUp", keyCode);
} else {
sendKey(null, modifiers, "rawKeyDown", keyCode);
}
break;
default:
sendKey(c, modifiers, "rawKeyDown", keyCode);
sendKey(c, modifiers, "char", keyCode);
sendKey(c, modifiers, "keyUp", keyCode);
}
} else {
logger.warn("unknown character / key code: {}", c);
sendKey(c, modifiers, "char", null);
}
}
for (int keyCode : input.getKeyCodesToRelease()) {
sendKey(null, 0, "keyUp", keyCode);
}
return DriverElement.locatorExists(this, locator);
}
protected int currentMouseXpos;
protected int currentMouseYpos;
@Override
public void actions(List> sequence) {
boolean submitRequested = submit;
submit = false; // make sure only LAST action is handled as a submit()
for (Map map : sequence) {
List> actions = (List) map.get("actions");
if (actions == null) {
logger.warn("no actions property found: {}", sequence);
return;
}
Iterator> iterator = actions.iterator();
while (iterator.hasNext()) {
Map action = iterator.next();
String type = (String) action.get("type");
if (type == null) {
logger.warn("no type property found: {}", action);
continue;
}
String chromeType;
switch (type) {
case "pointerMove":
chromeType = "mouseMoved";
break;
case "pointerDown":
chromeType = "mousePressed";
break;
case "pointerUp":
chromeType = "mouseReleased";
break;
default:
logger.warn("unexpected action type: {}", action);
continue;
}
Integer x = (Integer) action.get("x");
Integer y = (Integer) action.get("y");
if (x != null) {
currentMouseXpos = x;
}
if (y != null) {
currentMouseYpos = y;
}
Integer duration = (Integer) action.get("duration");
DevToolsMessage toSend = method("Input.dispatchMouseEvent")
.param("type", chromeType)
.param("x", currentMouseXpos).param("y", currentMouseYpos);
if ("mousePressed".equals(chromeType) || "mouseReleased".equals(chromeType)) {
toSend.param("button", "left").param("clickCount", 1);
}
if (!iterator.hasNext() && submitRequested) {
submit = true;
}
toSend.send();
if (duration != null) {
options.sleep(duration);
}
}
}
}
@Override
public String text(String id) {
return property(id, "textContent");
}
@Override
public String html(String id) {
return property(id, "outerHTML");
}
@Override
public String value(String locator) {
return property(locator, "value");
}
@Override
public Element value(String locator, String value) {
retryIfEnabled(locator);
eval(DriverOptions.selector(locator) + ".value = '" + value + "'");
return DriverElement.locatorExists(this, locator);
}
@Override
public String attribute(String id, String name) {
retryIfEnabled(id);
DevToolsMessage dtm = eval(DriverOptions.selector(id) + ".getAttribute('" + name + "')");
return dtm.getResult().getAsString();
}
@Override
public String property(String id, String name) {
retryIfEnabled(id);
DevToolsMessage dtm = eval(DriverOptions.selector(id) + "['" + name + "']");
return dtm.getResult().getAsString();
}
@Override
public boolean enabled(String id) {
retryIfEnabled(id);
DevToolsMessage dtm = eval(DriverOptions.selector(id) + ".disabled");
return !dtm.getResult().isTrue();
}
@Override
public boolean waitUntil(String expression) {
return options.retry(() -> {
try {
return evalQuickly(expression).getResult().isTrue();
} catch (Exception e) {
logger.warn("waitUntil evaluate failed: {}", e.getMessage());
return false;
}
}, b -> b, "waitUntil (js)", true);
}
@Override
public Object script(String expression) {
return eval(expression).getResult().getValue();
}
@Override
public String getTitle() {
return eval("document.title").getResult().getAsString();
}
@Override
public String getUrl() {
return eval("document.location.href").getResult().getAsString();
}
@Override
public List getCookies() {
DevToolsMessage dtm = method("Network.getAllCookies").send();
return dtm.getResultVariable("cookies").getValue();
}
@Override
public Map cookie(String name) {
List list = getCookies();
if (list == null) {
return null;
}
for (Map map : list) {
if (map != null && name.equals(map.get("name"))) {
return map;
}
}
return null;
}
@Override
public void cookie(Map cookie) {
if (cookie.get("url") == null && cookie.get("domain") == null) {
cookie = new HashMap(cookie); // don't mutate test
cookie.put("url", getUrl());
}
method("Network.setCookie").params(cookie).send();
}
@Override
public void deleteCookie(String name) {
method("Network.deleteCookies").param("name", name).param("url", getUrl()).send();
}
@Override
public void clearCookies() {
method("Network.clearBrowserCookies").send();
}
@Override
public void dialog(boolean accept) {
dialog(accept, null);
}
@Override
public void dialog(boolean accept, String text) {
DevToolsMessage temp = method("Page.handleJavaScriptDialog").param("accept", accept);
if (text == null) {
temp.send();
} else {
temp.param("promptText", text).send();
}
}
@Override
public String getDialogText() {
return currentDialogText;
}
@Override
public byte[] pdf(Map options) {
DevToolsMessage dtm = method("Page.printToPDF").params(options).send();
String temp = dtm.getResultVariable("data").getAsString();
return Base64.getDecoder().decode(temp);
}
@Override
public byte[] screenshot(boolean embed) {
return screenshot(null, embed);
}
@Override
public Map position(String locator) {
return position(locator, false);
}
@Override
public Map position(String locator, boolean relative) {
boolean submitTemp = submit; // in case we are prepping for a submit().mouse(locator).click()
submit = false;
retryIfEnabled(locator);
Map map = eval(relative ? DriverOptions.getRelativePositionJs(locator) : DriverOptions.getPositionJs(locator)).getResult().getValue();
submit = submitTemp;
return map;
}
@Override
public byte[] screenshot(String id, boolean embed) {
DevToolsMessage dtm;
try {
if (id == null) {
dtm = method("Page.captureScreenshot").send();
} else {
Map map = position(id);
map.put("scale", 1);
dtm = method("Page.captureScreenshot").param("clip", map).send();
}
if (dtm == null) {
logger.error("unable to capture screenshot - no data returned");
return Constants.ZERO_BYTES;
}
String temp = dtm.getResultVariable("data").getAsString();
byte[] bytes = Base64.getDecoder().decode(temp);
if (embed) {
getRuntime().embed(bytes, ResourceType.PNG);
}
return bytes;
} catch (Exception e) { // rare case where message does not get a reply
logger.error("screenshot failed: {}", e.getMessage());
return Constants.ZERO_BYTES;
}
}
// chrome only
public byte[] screenshotFull() {
DevToolsMessage layout = method("Page.getLayoutMetrics").send();
Map size = layout.getResultVariable("contentSize").getValue();
Map map = options.newMapWithSelectedKeys(size, "height", "width");
map.put("x", 0);
map.put("y", 0);
map.put("scale", 1);
DevToolsMessage dtm = method("Page.captureScreenshot").param("clip", map).send();
if (dtm.isResultError()) {
logger.error("unable to capture screenshot: {}", dtm);
return new byte[0];
}
String temp = dtm.getResultVariable("data").getAsString();
return Base64.getDecoder().decode(temp);
}
@Override
public List getPages() {
DevToolsMessage dtm = method("Target.getTargets").send();
return dtm.getResultVariable("targetInfos.targetId").getValue();
}
@Override
public void switchPage(String titleOrUrl) {
if (titleOrUrl == null) {
return;
}
String targetId = options.retry(() -> {
DevToolsMessage dtm = method("Target.getTargets").send();
List targets = dtm.getResultVariable("targetInfos").getValue();
for (Map map : targets) {
String title = (String) map.get("title");
String url = (String) map.get("url");
if ((title != null && title.contains(titleOrUrl))
|| (url != null && url.contains(titleOrUrl))) {
return (String) map.get("targetId");
}
}
return null;
}, returned -> returned != null, "waiting to switch to tab: " + titleOrUrl, true);
attachAndActivate(targetId, false);
}
@Override
public void switchPage(int index) {
if (index == -1) {
return;
}
DevToolsMessage dtm = method("Target.getTargets").send();
List targets = dtm.getResultVariable("targetInfos").getValue();
if (index < targets.size()) {
Map target = targets.get(index);
String targetId = (String) target.get("targetId");
attachAndActivate(targetId, false);
} else {
logger.warn("failed to switch frame by index: {}", index);
}
}
@Override
public void switchFrame(int index) {
if (index == -1) {
frame = null;
sessionId = frameSessions.get(mainFrameId);
return;
}
List objectIds = elementIds("iframe,frame");
if (index < objectIds.size()) {
String objectId = objectIds.get(index);
if (!setExecutionContext(objectId)) {
logger.warn("failed to switch frame by index: {}", index);
}
} else {
logger.warn("unable to switch frame by index: {}", index);
}
}
@Override
public void switchFrame(String locator) {
if (locator == null) {
frame = null;
sessionId = frameSessions.get(mainFrameId);
return;
}
retryIfEnabled(locator);
String objectId = evalForObjectId(DriverOptions.selector(locator));
if (!setExecutionContext(objectId)) {
logger.warn("failed to switch frame by locator: {}", locator);
}
}
private Integer getFrameContext() {
if (frame == null) {
return null;
}
Integer result = frameContexts.get(frame.id);
logger.trace("** get frame context: {} - {}", frame, result);
return result;
}
private boolean setExecutionContext(String objectId) { // locator just for logging
DevToolsMessage dtm = method("DOM.describeNode")
.param("objectId", objectId)
.param("depth", 0)
.send();
String frameId = dtm.getResult("node.frameId");
if (frameId == null) {
return false;
}
dtm = method("Page.getFrameTree").send();
frame = null;
try {
List childFrames = dtm.getResult("frameTree.childFrames[*]");
List flattenFrameTree = getFrameTree(childFrames);
for (Map frameMap : flattenFrameTree) {
String frameMapTemp = (String) frameMap.get("id");
if (frameId.equals(frameMapTemp)) {
String frameUrl = (String) frameMap.get("url");
String frameName = (String) frameMap.get("name");
frame = new Frame(frameId, frameUrl, frameName);
logger.trace("** switched to frame: {}", frame);
break;
}
}
} catch (PathNotFoundException e) {
logger.trace("** childFrames not found. Will try to change to a different Target in Chrome.");
}
if (frame == null) {
// for some reason need to trigger Target.getTargets before attaching
dtm = method("Target.getTargets").send();
if (frameSessions.get(frameId) == null) {
// attempt to force attach (see: https://github.com/karatelabs/karate/pull/1944#issuecomment-1070793461)
attachAndActivate(frameId, true);
}
List> targetInfos = dtm.getResult("targetInfos");
for (Map targetInfo : targetInfos) {
String temp = (String) targetInfo.get("targetId");
String tempType = (String) targetInfo.get("type");
if (frameId.equals(temp) && ("iframe".equals(tempType) || "frame".equals(tempType))) {
String frameUrl = (String) targetInfo.get("url");
String frameName = (String) targetInfo.get("title");
frame = new Frame(frameId, frameUrl, frameName);
logger.trace("** switched to frame: {}", frame);
}
}
}
if (frame == null) {
return false;
}
if (frameSessions.get(frameId) != null) {
sessionId = frameSessions.get(frameId);
} else {
// attach to frame / target / process with the frame
attachAndActivate(frameId, true);
// a null sessionId indicates that we failed to attach directly to the frame
// this occurs on local frames that are already being debugged with the main frame
if (sessionId == null) {
sessionId = frameSessions.get(mainFrameId);
}
}
Integer contextId = getFrameContext();
if (contextId != null) {
return true;
}
dtm = method("Page.createIsolatedWorld").param("frameId", frameId).send();
contextId = dtm.getResult("executionContextId");
frameContexts.put(frameId, contextId);
return true;
}
private List getFrameTree(List frames) {
List resultFrames = new ArrayList<>();
for (Map frame : frames) {
Map currFrame = (Map) frame.get("frame");
List childFrames = (List) frame.get("childFrames");
if (currFrame != null) {
resultFrames.add((Map) frame.get("frame"));
}
if (childFrames != null) {
resultFrames.addAll(getFrameTree(childFrames));
}
}
return resultFrames;
}
public void enableNetworkEvents() {
method("Network.enable").send();
}
public void enablePageEvents() {
method("Page.enable").send();
}
public void enableRuntimeEvents() {
method("Runtime.enable").send();
}
public void intercept(Value value) {
Map config = (Map) JsValue.toJava(value);
config = new Variable(config).getValue();
intercept(config);
}
public void intercept(Map config) {
List patterns = (List) config.get("patterns");
if (patterns == null) {
throw new RuntimeException("missing array argument 'patterns': " + config);
}
if (mockHandler != null) {
throw new RuntimeException("'intercept()' can be called only once");
}
String mock = (String) config.get("mock");
if (mock == null) {
throw new RuntimeException("missing argument 'mock': " + config);
}
Object o = getRuntime().engine.fileReader.readFile(mock);
if (!(o instanceof FeatureCall)) {
throw new RuntimeException("'mock' is not a feature file: " + config + ", " + mock);
}
FeatureCall fc = (FeatureCall) o;
mockHandler = new MockHandler(fc.feature);
method("Fetch.enable").param("patterns", patterns).send();
}
public void inputFile(String locator, String... relativePaths) {
retryIfEnabled(locator);
List files = new ArrayList(relativePaths.length);
ScenarioEngine engine = ScenarioEngine.get();
for (String p : relativePaths) {
files.add(engine.fileReader.toAbsolutePath(p));
}
String objectId = evalForObjectId(DriverOptions.selector(locator));
method("DOM.setFileInputFiles").param("files", files).param("objectId", objectId).send();
}
public Object scriptAwait(String expression) {
DevToolsMessage toSend = method("Runtime.evaluate")
.param("expression", expression)
.param("returnByValue", true)
.param("awaitPromise", true);
Integer contextId = getFrameContext();
if (contextId != null) {
toSend.param("contextId", contextId);
}
return toSend.send().getResult().getValue();
}
}