com.intuit.karate.driver.WebDriver Maven / Gradle / Ivy
The newest version!
/*
* 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.Http;
import com.intuit.karate.Logger;
import com.intuit.karate.Json;
import com.intuit.karate.core.Variable;
import com.intuit.karate.http.ResourceType;
import com.intuit.karate.http.Response;
import com.intuit.karate.shell.Command;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* @author pthomas3
*/
public abstract class WebDriver implements Driver {
protected final DriverOptions options;
protected final Command command;
protected final Http http;
private final String sessionId;
private boolean terminated;
//private final String windowId;
protected boolean open = true;
protected Boolean specCompliant;
protected final Logger logger;
protected WebDriver(DriverOptions options) {
this.options = options;
this.logger = options.driverLogger;
command = options.startProcess();
http = options.getHttp();
Response response = http.path("session").post(options.getWebDriverSessionPayload());
if (response.getStatus() != 200) {
String message = "webdriver session create status " + response.getStatus() + ", " + response.getBodyAsString();
logger.error(message);
if (command != null) {
command.close(true);
}
throw new RuntimeException(message);
}
sessionId = response.json().getFirst("$..sessionId");
logger.debug("init session id: {}", sessionId);
http.url(http.urlBase + "/session/" + sessionId);
if (options.start) {
activate();
}
}
@Override
public Driver timeout(Integer millis) {
options.setTimeout(millis);
// this will "reset" to default if null was set above
http.configure("readTimeout", options.getTimeout() + "");
return this;
}
@Override
public Driver timeout() {
return timeout(null);
}
public String getSessionId() {
return sessionId;
}
// can be used directly if you know what you are doing !
public Http getHttp() {
return http;
}
private String getSubmitHash() {
return getUrl() + elementId("html");
}
protected T retryIfEnabled(String locator, Supplier action) {
if (options.isRetryEnabled()) {
waitFor(locator); // will throw exception if not found
}
if (options.highlight) {
highlight(locator, options.highlightDuration);
}
String before = options.getPreSubmitHash();
if (before != null) {
logger.trace("submit requested, will wait for page load after next action on : {}", locator);
options.setPreSubmitHash(null); // clear the submit flag
T result = action.get();
Integer retryInterval = options.getRetryInterval();
options.setRetryInterval(500); // reduce retry interval for this special case
options.retry(() -> getSubmitHash(), hash -> !before.equals(hash), "waiting for document to change", false);
options.setRetryInterval(retryInterval); // restore
return result;
} else {
return action.get();
}
}
protected boolean isJavaScriptError(Response res) {
return res.getStatus() != 200
&& !res.json().get("value").contains("unexpected alert open");
}
protected boolean isLocatorError(Response res) {
return res.getStatus() != 200;
}
protected boolean isCookieError(Response res) {
return res.getStatus() != 200;
}
private Element evalLocator(String locator, String dotExpression) {
eval(prefixReturn(DriverOptions.selector(locator) + "." + dotExpression));
// if the js above did not throw an exception, the element exists
return DriverElement.locatorExists(this, locator);
}
private Element evalFocus(String locator) {
eval(options.focusJs(locator));
// if the js above did not throw an exception, the element exists
return DriverElement.locatorExists(this, locator);
}
protected Variable eval(String expression, List args) {
Json json = Json.object().set("script", expression).set("args", (args == null) ? Collections.EMPTY_LIST : args);
Response res = http.path("execute", "sync").post(json);
if (isJavaScriptError(res)) {
logger.warn("javascript failed, will retry once: {}", res.getBodyAsString());
options.sleep();
res = http.path("execute", "sync").post(json);
if (isJavaScriptError(res)) {
String message = "javascript failed twice: " + res.getBodyAsString();
logger.error(message);
throw new RuntimeException(message);
}
}
return new Variable(res.json().get("value"));
}
protected Variable eval(String expression) {
return eval(expression, null);
}
protected List getElementKeys() {
// "element-6066-11e4-a52e-4f735466cecf" is the key to element in the W3C WebDriver standard
// "ELEMENT" is a deviation from the W3C standard
// explanation can be found here: https://github.com/karatelabs/karate/issues/1840#issuecomment-974688715
return Arrays.asList("element-6066-11e4-a52e-4f735466cecf", "ELEMENT");
}
protected String getJsonForInput(String text) {
return Json.object().set("text", text).toString();
}
protected String getJsonForLegacyInput(String text) {
return Json.of("{ value: [ '" + text + "' ] }").toString();
}
protected String getJsonForHandle(String text) {
return Json.object().set("handle", text).toString();
}
protected String getJsonForFrame(String text) {
return Json.object().set("id", text).toString();
}
protected String selectorPayload(String locator) {
if (locator.startsWith("{")) {
locator = DriverOptions.preProcessWildCard(locator);
}
Json json = Json.object();
if (locator.startsWith("/")) {
json.set("using", "xpath").set("value", locator);
} else {
json.set("using", "css selector").set("value", locator);
}
return json.toString();
}
@Override
public String elementId(String locator) {
String json = selectorPayload(locator);
Response res = http.path("element").postJson(json);
if (isLocatorError(res)) {
logger.warn("locator failed, will retry once: {}", res.getBodyAsString());
options.sleep();
res = http.path("element").postJson(json);
if (isLocatorError(res)) {
String message = "locator failed twice: " + res.getBodyAsString();
logger.error(message);
throw new RuntimeException(message);
}
}
List resultElements = res.json().>getAll("$..", getElementKeys()).stream()
.flatMap(List::stream)
.collect(Collectors.toList());
String resultElement = resultElements != null && !resultElements.isEmpty() ? resultElements.get(0) : null;
if (resultElement == null) {
String message = "locator failed to retrieve element returned by target driver: " + res.getBodyAsString();
logger.error(message);
throw new RuntimeException(message);
}
return resultElement;
}
@Override
public List elementIds(String locator) {
return http.path("elements")
.postJson(selectorPayload(locator)).json()
.>getAll("$..", getElementKeys()).stream()
.flatMap(List::stream)
.collect(Collectors.toList());
}
@Override
public DriverOptions getOptions() {
return options;
}
@Override
public void setUrl(String url) {
Json json = Json.object().set("url", url);
http.path("url").post(json);
}
@Override
public Map getDimensions() {
return http.path("window", "rect").get().json().get("value");
}
@Override
public void setDimensions(Map map) {
http.path("window", "rect").post(map);
}
@Override
public void refresh() {
http.path("refresh").postJson("{}");
}
@Override
public void reload() {
// not supported by webdriver
refresh();
}
@Override
public void back() {
http.path("back").postJson("{}");
}
@Override
public void forward() {
http.path("forward").postJson("{}");
}
@Override
public void maximize() {
http.path("window", "maximize").postJson("{}");
}
@Override
public void minimize() {
http.path("window", "minimize").postJson("{}");
}
@Override
public void fullscreen() {
http.path("window", "fullscreen").postJson("{}");
}
@Override
public Element focus(String locator) {
return retryIfEnabled(locator, () -> evalFocus(locator));
}
@Override
public Element clear(String locator) {
return retryIfEnabled(locator, () -> evalLocator(locator, "value = ''"));
}
@Override
public Element input(String locator, String value) {
return retryIfEnabled(locator, () -> {
String elementId;
if (locator.startsWith("(")) {
evalFocus(locator);
List elements = http.path("element", "active").get()
.json().>getAll("$..", getElementKeys()).stream()
.flatMap(List::stream)
.collect(Collectors.toList());;
elementId = elements != null && !elements.isEmpty() ? elements.get(0) : null;
} else {
elementId = elementId(locator);
}
Response response = http.path("element", elementId, "value").postJson(isSpecCompliant() ? getJsonForInput(value) : getJsonForLegacyInput(value));
if (checkForSpecCompliance()) {
if (response.json().get("$.value") != null) {
String responseMessage = response.json().get("$.value.message");
if (responseMessage.contains("invalid argument: 'value' must be a list")) {
http.path("element", elementId, "value").postJson(getJsonForLegacyInput(value));
specCompliant = false;
} else {
specCompliant = true;
}
} else {
// did not complain that value should be a list so assume W3C WebDriver compliant moving forward
specCompliant = true;
}
}
return DriverElement.locatorExists(this, locator);
});
}
@Override
public Element click(String locator) {
return retryIfEnabled(locator, () -> evalLocator(locator, "click()"));
}
@Override
public Driver submit() {
options.setPreSubmitHash(getSubmitHash());
return this;
}
@Override
public Element select(String locator, String text) {
return retryIfEnabled(locator, () -> {
eval(options.optionSelector(locator, text));
// if the js above did not throw an exception, the element exists
return DriverElement.locatorExists(this, locator);
});
}
@Override
public Element select(String locator, int index) {
return retryIfEnabled(locator, () -> {
eval(options.optionSelector(locator, index));
// if the js above did not throw an exception, the element exists
return DriverElement.locatorExists(this, locator);
});
}
@Override
public void actions(List