Best Selenium code snippet using org.openqa.selenium.grid.data.NodeDrainComplete
Source:LocalNode.java
...33import org.openqa.selenium.grid.data.Availability;34import org.openqa.selenium.grid.data.CreateSessionRequest;35import org.openqa.selenium.grid.data.CreateSessionResponse;36import org.openqa.selenium.grid.data.NodeAddedEvent;37import org.openqa.selenium.grid.data.NodeDrainComplete;38import org.openqa.selenium.grid.data.NodeDrainStarted;39import org.openqa.selenium.grid.data.NodeHeartBeatEvent;40import org.openqa.selenium.grid.data.NodeId;41import org.openqa.selenium.grid.data.NodeStatus;42import org.openqa.selenium.grid.data.Session;43import org.openqa.selenium.grid.data.SessionClosedEvent;44import org.openqa.selenium.grid.data.Slot;45import org.openqa.selenium.grid.data.SlotId;46import org.openqa.selenium.grid.jmx.JMXHelper;47import org.openqa.selenium.grid.jmx.ManagedAttribute;48import org.openqa.selenium.grid.jmx.ManagedService;49import org.openqa.selenium.grid.node.ActiveSession;50import org.openqa.selenium.grid.node.HealthCheck;51import org.openqa.selenium.grid.node.Node;52import org.openqa.selenium.grid.node.SessionFactory;53import org.openqa.selenium.grid.node.config.NodeOptions;54import org.openqa.selenium.grid.security.Secret;55import org.openqa.selenium.internal.Either;56import org.openqa.selenium.internal.Require;57import org.openqa.selenium.io.TemporaryFilesystem;58import org.openqa.selenium.io.Zip;59import org.openqa.selenium.json.Json;60import org.openqa.selenium.remote.SessionId;61import org.openqa.selenium.remote.http.HttpRequest;62import org.openqa.selenium.remote.http.HttpResponse;63import org.openqa.selenium.remote.tracing.AttributeKey;64import org.openqa.selenium.remote.tracing.EventAttribute;65import org.openqa.selenium.remote.tracing.EventAttributeValue;66import org.openqa.selenium.remote.tracing.Span;67import org.openqa.selenium.remote.tracing.Status;68import org.openqa.selenium.remote.tracing.Tracer;69import java.io.File;70import java.io.IOException;71import java.io.UncheckedIOException;72import java.net.URI;73import java.net.URISyntaxException;74import java.time.Clock;75import java.time.Duration;76import java.time.Instant;77import java.util.HashMap;78import java.util.List;79import java.util.Map;80import java.util.Optional;81import java.util.Set;82import java.util.UUID;83import java.util.concurrent.ExecutionException;84import java.util.concurrent.atomic.AtomicBoolean;85import java.util.concurrent.atomic.AtomicInteger;86import java.util.logging.Logger;87import java.util.stream.Collectors;88import static com.google.common.collect.ImmutableSet.toImmutableSet;89import static org.openqa.selenium.grid.data.Availability.DRAINING;90import static org.openqa.selenium.grid.data.Availability.UP;91import static org.openqa.selenium.grid.node.CapabilityResponseEncoder.getEncoder;92import static org.openqa.selenium.remote.HttpSessionId.getSessionId;93import static org.openqa.selenium.remote.RemoteTags.CAPABILITIES;94import static org.openqa.selenium.remote.RemoteTags.SESSION_ID;95import static org.openqa.selenium.remote.http.Contents.asJson;96import static org.openqa.selenium.remote.http.Contents.string;97import static org.openqa.selenium.remote.http.HttpMethod.DELETE;98@ManagedService(objectName = "org.seleniumhq.grid:type=Node,name=LocalNode",99 description = "Node running the webdriver sessions.")100public class LocalNode extends Node {101 private static final Json JSON = new Json();102 private static final Logger LOG = Logger.getLogger(LocalNode.class.getName());103 private final EventBus bus;104 private final URI externalUri;105 private final URI gridUri;106 private final Duration heartbeatPeriod;107 private final HealthCheck healthCheck;108 private final int maxSessionCount;109 private final List<SessionSlot> factories;110 private final Cache<SessionId, SessionSlot> currentSessions;111 private final Cache<SessionId, TemporaryFilesystem> tempFileSystems;112 private final Regularly regularly;113 private final AtomicInteger pendingSessions = new AtomicInteger();114 private final AtomicBoolean heartBeatStarted = new AtomicBoolean(false);115 private LocalNode(116 Tracer tracer,117 EventBus bus,118 URI uri,119 URI gridUri,120 HealthCheck healthCheck,121 int maxSessionCount,122 Ticker ticker,123 Duration sessionTimeout,124 Duration heartbeatPeriod,125 List<SessionSlot> factories,126 Secret registrationSecret) {127 super(tracer, new NodeId(UUID.randomUUID()), uri, registrationSecret);128 this.bus = Require.nonNull("Event bus", bus);129 this.externalUri = Require.nonNull("Remote node URI", uri);130 this.gridUri = Require.nonNull("Grid URI", gridUri);131 this.maxSessionCount = Math.min(Require.positive("Max session count", maxSessionCount), factories.size());132 this.heartbeatPeriod = heartbeatPeriod;133 this.factories = ImmutableList.copyOf(factories);134 Require.nonNull("Registration secret", registrationSecret);135 this.healthCheck = healthCheck == null ?136 () -> new HealthCheck.Result(137 isDraining() ? DRAINING : UP,138 String.format("%s is %s", uri, isDraining() ? "draining" : "up")) :139 healthCheck;140 this.currentSessions = CacheBuilder.newBuilder()141 .expireAfterAccess(sessionTimeout)142 .ticker(ticker)143 .removalListener((RemovalListener<SessionId, SessionSlot>) notification -> {144 // If we were invoked explicitly, then return: we know what we're doing.145 if (!notification.wasEvicted()) {146 return;147 }148 killSession(notification.getValue());149 })150 .build();151 this.tempFileSystems = CacheBuilder.newBuilder()152 .expireAfterAccess(sessionTimeout)153 .ticker(ticker)154 .removalListener((RemovalListener<SessionId, TemporaryFilesystem>) notification -> {155 TemporaryFilesystem tempFS = notification.getValue();156 tempFS.deleteTemporaryFiles();157 tempFS.deleteBaseDir();158 })159 .build();160 this.regularly = new Regularly("Local Node: " + externalUri);161 regularly.submit(currentSessions::cleanUp, Duration.ofSeconds(30), Duration.ofSeconds(30));162 regularly.submit(tempFileSystems::cleanUp, Duration.ofSeconds(30), Duration.ofSeconds(30));163 bus.addListener(NodeAddedEvent.listener(nodeId -> {164 if (getId().equals(nodeId)) {165 // Lets avoid to create more than one "Regularly" when the Node registers again.166 if (!heartBeatStarted.getAndSet(true)) {167 regularly.submit(168 () -> bus.fire(new NodeHeartBeatEvent(getStatus())), heartbeatPeriod, heartbeatPeriod);169 }170 }171 }));172 bus.addListener(SessionClosedEvent.listener(id -> {173 try {174 this.stop(id);175 } catch (NoSuchSessionException ignore) {176 }177 if (this.isDraining()) {178 int done = pendingSessions.decrementAndGet();179 if (done <= 0) {180 LOG.info("Firing node drain complete message");181 bus.fire(new NodeDrainComplete(this.getId()));182 }183 }184 }));185 new JMXHelper().register(this);186 }187 public static Builder builder(188 Tracer tracer,189 EventBus bus,190 URI uri,191 URI gridUri,192 Secret registrationSecret) {193 return new Builder(tracer, bus, uri, gridUri, registrationSecret);194 }195 @Override196 public boolean isReady() {197 return bus.isReady();198 }199 @VisibleForTesting200 @ManagedAttribute(name = "CurrentSessions")201 public int getCurrentSessionCount() {202 // It seems wildly unlikely we'll overflow an int203 return Math.toIntExact(currentSessions.size());204 }205 @ManagedAttribute(name = "MaxSessions")206 public int getMaxSessionCount() {207 return maxSessionCount;208 }209 @ManagedAttribute(name = "Status")210 public Availability getAvailability() {211 return isDraining() ? DRAINING : UP;212 }213 @ManagedAttribute(name = "TotalSlots")214 public int getTotalSlots() {215 return factories.size();216 }217 @ManagedAttribute(name = "UsedSlots")218 public long getUsedSlots() {219 return factories.stream().filter(sessionSlot -> !sessionSlot.isAvailable()).count();220 }221 @ManagedAttribute(name = "Load")222 public float getLoad() {223 long inUse = factories.stream().filter(sessionSlot -> !sessionSlot.isAvailable()).count();224 return inUse / (float) maxSessionCount * 100f;225 }226 @ManagedAttribute(name = "RemoteNodeUri")227 public URI getExternalUri() {228 return this.getUri();229 }230 @ManagedAttribute(name = "GridUri")231 public URI getGridUri() {232 return this.gridUri;233 }234 @ManagedAttribute(name = "NodeId")235 public String getNodeId() {236 return getId().toString();237 }238 @Override239 public boolean isSupporting(Capabilities capabilities) {240 return factories.parallelStream().anyMatch(factory -> factory.test(capabilities));241 }242 @Override243 public Either<WebDriverException, CreateSessionResponse> newSession(CreateSessionRequest sessionRequest) {244 Require.nonNull("Session request", sessionRequest);245 try (Span span = tracer.getCurrentContext().createSpan("node.new_session")) {246 Map<String, EventAttributeValue> attributeMap = new HashMap<>();247 attributeMap248 .put(AttributeKey.LOGGER_CLASS.getKey(), EventAttribute.setValue(getClass().getName()));249 attributeMap.put("session.request.capabilities",250 EventAttribute.setValue(sessionRequest.getDesiredCapabilities().toString()));251 attributeMap.put("session.request.downstreamdialect",252 EventAttribute.setValue(sessionRequest.getDownstreamDialects().toString()));253 int currentSessionCount = getCurrentSessionCount();254 span.setAttribute("current.session.count", currentSessionCount);255 attributeMap.put("current.session.count", EventAttribute.setValue(currentSessionCount));256 if (getCurrentSessionCount() >= maxSessionCount) {257 span.setAttribute("error", true);258 span.setStatus(Status.RESOURCE_EXHAUSTED);259 attributeMap.put("max.session.count", EventAttribute.setValue(maxSessionCount));260 span.addEvent("Max session count reached", attributeMap);261 return Either.left(new RetrySessionRequestException("Max session count reached."));262 }263 if (isDraining()) {264 span.setStatus(Status.UNAVAILABLE.withDescription("The node is draining. Cannot accept new sessions."));265 return Either.left(266 new RetrySessionRequestException("The node is draining. Cannot accept new sessions."));267 }268 // Identify possible slots to use as quickly as possible to enable concurrent session starting269 SessionSlot slotToUse = null;270 synchronized (factories) {271 for (SessionSlot factory : factories) {272 if (!factory.isAvailable() || !factory.test(sessionRequest.getDesiredCapabilities())) {273 continue;274 }275 factory.reserve();276 slotToUse = factory;277 break;278 }279 }280 if (slotToUse == null) {281 span.setAttribute("error", true);282 span.setStatus(Status.NOT_FOUND);283 span.addEvent("No slot matched the requested capabilities. ", attributeMap);284 return Either.left(285 new RetrySessionRequestException("No slot matched the requested capabilities."));286 }287 Either<WebDriverException, ActiveSession> possibleSession = slotToUse.apply(sessionRequest);288 if (possibleSession.isRight()) {289 ActiveSession session = possibleSession.right();290 currentSessions.put(session.getId(), slotToUse);291 SessionId sessionId = session.getId();292 Capabilities caps = session.getCapabilities();293 SESSION_ID.accept(span, sessionId);294 CAPABILITIES.accept(span, caps);295 String downstream = session.getDownstreamDialect().toString();296 String upstream = session.getUpstreamDialect().toString();297 String sessionUri = session.getUri().toString();298 span.setAttribute(AttributeKey.DOWNSTREAM_DIALECT.getKey(), downstream);299 span.setAttribute(AttributeKey.UPSTREAM_DIALECT.getKey(), upstream);300 span.setAttribute(AttributeKey.SESSION_URI.getKey(), sessionUri);301 // The session we return has to look like it came from the node, since we might be dealing302 // with a webdriver implementation that only accepts connections from localhost303 Session externalSession = createExternalSession(304 session,305 externalUri,306 slotToUse.isSupportingCdp() || caps.getCapability("se:cdp") != null);307 return Either.right(new CreateSessionResponse(308 externalSession,309 getEncoder(session.getDownstreamDialect()).apply(externalSession)));310 } else {311 slotToUse.release();312 span.setAttribute("error", true);313 span.addEvent("Unable to create session with the driver", attributeMap);314 return Either.left(possibleSession.left());315 }316 }317 }318 @Override319 public boolean isSessionOwner(SessionId id) {320 Require.nonNull("Session ID", id);321 return currentSessions.getIfPresent(id) != null;322 }323 @Override324 public Session getSession(SessionId id) throws NoSuchSessionException {325 Require.nonNull("Session ID", id);326 SessionSlot slot = currentSessions.getIfPresent(id);327 if (slot == null) {328 throw new NoSuchSessionException("Cannot find session with id: " + id);329 }330 return createExternalSession(slot.getSession(), externalUri, slot.isSupportingCdp());331 }332 @Override333 public TemporaryFilesystem getTemporaryFilesystem(SessionId id) throws IOException {334 try {335 return tempFileSystems.get(id, () -> TemporaryFilesystem.getTmpFsBasedOn(336 TemporaryFilesystem.getDefaultTmpFS().createTempDir("session", id.toString())));337 } catch (ExecutionException e) {338 throw new IOException(e);339 }340 }341 @Override342 public HttpResponse executeWebDriverCommand(HttpRequest req) {343 // True enough to be good enough344 SessionId id = getSessionId(req.getUri()).map(SessionId::new)345 .orElseThrow(() -> new NoSuchSessionException("Cannot find session: " + req));346 SessionSlot slot = currentSessions.getIfPresent(id);347 if (slot == null) {348 throw new NoSuchSessionException("Cannot find session with id: " + id);349 }350 HttpResponse toReturn = slot.execute(req);351 if (req.getMethod() == DELETE && req.getUri().equals("/session/" + id)) {352 stop(id);353 }354 return toReturn;355 }356 @Override357 public HttpResponse uploadFile(HttpRequest req, SessionId id) {358 Map<String, Object> incoming = JSON.toType(string(req), Json.MAP_TYPE);359 File tempDir;360 try {361 TemporaryFilesystem tempfs = getTemporaryFilesystem(id);362 tempDir = tempfs.createTempDir("upload", "file");363 Zip.unzip((String) incoming.get("file"), tempDir);364 } catch (IOException e) {365 throw new UncheckedIOException(e);366 }367 // Select the first file368 File[] allFiles = tempDir.listFiles();369 if (allFiles == null) {370 throw new WebDriverException(371 String.format("Cannot access temporary directory for uploaded files %s", tempDir));372 }373 if (allFiles.length != 1) {374 throw new WebDriverException(375 String.format("Expected there to be only 1 file. There were: %s", allFiles.length));376 }377 ImmutableMap<String, Object> result = ImmutableMap.of(378 "value", allFiles[0].getAbsolutePath());379 return new HttpResponse().setContent(asJson(result));380 }381 @Override382 public void stop(SessionId id) throws NoSuchSessionException {383 Require.nonNull("Session ID", id);384 SessionSlot slot = currentSessions.getIfPresent(id);385 if (slot == null) {386 throw new NoSuchSessionException("Cannot find session with id: " + id);387 }388 killSession(slot);389 tempFileSystems.invalidate(id);390 }391 private Session createExternalSession(ActiveSession other, URI externalUri, boolean isSupportingCdp) {392 Capabilities toUse = ImmutableCapabilities.copyOf(other.getCapabilities());393 // Rewrite the se:options if necessary to send the cdp url back394 if (isSupportingCdp) {395 String cdpPath = String.format("/session/%s/se/cdp", other.getId());396 toUse = new PersistentCapabilities(toUse).setCapability("se:cdp", rewrite(cdpPath));397 }398 return new Session(other.getId(), externalUri, other.getStereotype(), toUse, Instant.now());399 }400 private URI rewrite(String path) {401 try {402 return new URI(403 "ws",404 gridUri.getUserInfo(),405 gridUri.getHost(),406 gridUri.getPort(),407 path,408 null,409 null);410 } catch (URISyntaxException e) {411 throw new RuntimeException(e);412 }413 }414 private void killSession(SessionSlot slot) {415 currentSessions.invalidate(slot.getSession().getId());416 // Attempt to stop the session417 if (!slot.isAvailable()) {418 slot.stop();419 }420 }421 @Override422 public NodeStatus getStatus() {423 Set<Slot> slots = factories.stream()424 .map(slot -> {425 Instant lastStarted = Instant.EPOCH;426 Optional<Session> session = Optional.empty();427 if (!slot.isAvailable()) {428 ActiveSession activeSession = slot.getSession();429 if (activeSession != null) {430 lastStarted = activeSession.getStartTime();431 session = Optional.of(432 new Session(433 activeSession.getId(),434 activeSession.getUri(),435 slot.getStereotype(),436 activeSession.getCapabilities(),437 activeSession.getStartTime()));438 }439 }440 return new Slot(441 new SlotId(getId(), slot.getId()),442 slot.getStereotype(),443 lastStarted,444 session);445 })446 .collect(toImmutableSet());447 return new NodeStatus(448 getId(),449 externalUri,450 maxSessionCount,451 slots,452 isDraining() ? DRAINING : UP,453 heartbeatPeriod,454 getNodeVersion(),455 getOsInfo());456 }457 @Override458 public HealthCheck getHealthCheck() {459 return healthCheck;460 }461 @Override462 public void drain() {463 bus.fire(new NodeDrainStarted(getId()));464 draining = true;465 int currentSessionCount = getCurrentSessionCount();466 if (currentSessionCount == 0) {467 LOG.info("Firing node drain complete message");468 bus.fire(new NodeDrainComplete(getId()));469 } else {470 pendingSessions.set(currentSessionCount);471 }472 }473 private Map<String, Object> toJson() {474 return ImmutableMap.of(475 "id", getId(),476 "uri", externalUri,477 "maxSessions", maxSessionCount,478 "draining", isDraining(),479 "capabilities", factories.stream()480 .map(SessionSlot::getStereotype)481 .collect(Collectors.toSet()));482 }...
Source:OneShotNode.java
...27import org.openqa.selenium.grid.config.Config;28import org.openqa.selenium.grid.config.ConfigException;29import org.openqa.selenium.grid.data.CreateSessionRequest;30import org.openqa.selenium.grid.data.CreateSessionResponse;31import org.openqa.selenium.grid.data.NodeDrainComplete;32import org.openqa.selenium.grid.data.NodeDrainStarted;33import org.openqa.selenium.grid.data.NodeId;34import org.openqa.selenium.grid.data.NodeStatus;35import org.openqa.selenium.grid.data.Session;36import org.openqa.selenium.grid.data.SessionClosedEvent;37import org.openqa.selenium.grid.data.Slot;38import org.openqa.selenium.grid.data.SlotId;39import org.openqa.selenium.grid.log.LoggingOptions;40import org.openqa.selenium.grid.node.HealthCheck;41import org.openqa.selenium.grid.node.Node;42import org.openqa.selenium.grid.node.config.NodeOptions;43import org.openqa.selenium.grid.security.Secret;44import org.openqa.selenium.grid.server.BaseServerOptions;45import org.openqa.selenium.grid.server.EventBusOptions;46import org.openqa.selenium.internal.Require;47import org.openqa.selenium.json.Json;48import org.openqa.selenium.remote.CommandExecutor;49import org.openqa.selenium.remote.RemoteWebDriver;50import org.openqa.selenium.remote.SessionId;51import org.openqa.selenium.remote.http.HttpClient;52import org.openqa.selenium.remote.http.HttpRequest;53import org.openqa.selenium.remote.http.HttpResponse;54import org.openqa.selenium.remote.tracing.Tracer;55import java.lang.reflect.Field;56import java.net.URI;57import java.net.URISyntaxException;58import java.time.Instant;59import java.util.HashMap;60import java.util.Map;61import java.util.Optional;62import java.util.ServiceLoader;63import java.util.TreeMap;64import java.util.UUID;65import java.util.logging.Logger;66import java.util.stream.StreamSupport;67import static java.nio.charset.StandardCharsets.UTF_8;68import static org.openqa.selenium.grid.data.Availability.DRAINING;69import static org.openqa.selenium.grid.data.Availability.UP;70import static org.openqa.selenium.json.Json.MAP_TYPE;71import static org.openqa.selenium.remote.http.HttpMethod.DELETE;72/**73 * An implementation of {@link Node} that marks itself as draining immediately74 * after starting, and which then shuts down after usage. This will allow an75 * appropriately configured Kubernetes cluster to start a new node once the76 * session is finished.77 */78public class OneShotNode extends Node {79 private static final Logger LOG = Logger.getLogger(OneShotNode.class.getName());80 private static final Json JSON = new Json();81 private final EventBus events;82 private final WebDriverInfo driverInfo;83 private final Capabilities stereotype;84 private final URI gridUri;85 private final UUID slotId = UUID.randomUUID();86 private RemoteWebDriver driver;87 private SessionId sessionId;88 private HttpClient client;89 private Capabilities capabilities;90 private Instant sessionStart = Instant.EPOCH;91 private OneShotNode(92 Tracer tracer,93 EventBus events,94 Secret registrationSecret,95 NodeId id,96 URI uri,97 URI gridUri,98 Capabilities stereotype,99 WebDriverInfo driverInfo) {100 super(tracer, id, uri, registrationSecret);101 this.events = Require.nonNull("Event bus", events);102 this.gridUri = Require.nonNull("Public Grid URI", gridUri);103 this.stereotype = ImmutableCapabilities.copyOf(Require.nonNull("Stereotype", stereotype));104 this.driverInfo = Require.nonNull("Driver info", driverInfo);105 }106 public static Node create(Config config) {107 LoggingOptions loggingOptions = new LoggingOptions(config);108 EventBusOptions eventOptions = new EventBusOptions(config);109 BaseServerOptions serverOptions = new BaseServerOptions(config);110 NodeOptions nodeOptions = new NodeOptions(config);111 Map<String, Object> raw = new Json().toType(112 config.get("k8s", "stereotype")113 .orElseThrow(() -> new ConfigException("Unable to find node stereotype")),114 MAP_TYPE);115 Capabilities stereotype = new ImmutableCapabilities(raw);116 Optional<String> driverName = config.get("k8s", "driver_name").map(String::toLowerCase);117 // Find the webdriver info corresponding to the driver name118 WebDriverInfo driverInfo = StreamSupport.stream(ServiceLoader.load(WebDriverInfo.class).spliterator(), false)119 .filter(info -> info.isSupporting(stereotype))120 .filter(info -> driverName.map(name -> name.equals(info.getDisplayName().toLowerCase())).orElse(true))121 .findFirst()122 .orElseThrow(() -> new ConfigException(123 "Unable to find matching driver for %s and %s", stereotype, driverName.orElse("any driver")));124 LOG.info(String.format("Creating one-shot node for %s with stereotype %s", driverInfo, stereotype));125 LOG.info("Grid URI is: " + nodeOptions.getPublicGridUri());126 return new OneShotNode(127 loggingOptions.getTracer(),128 eventOptions.getEventBus(),129 serverOptions.getRegistrationSecret(),130 new NodeId(UUID.randomUUID()),131 serverOptions.getExternalUri(),132 nodeOptions.getPublicGridUri().orElseThrow(() -> new ConfigException("Unable to determine public grid address")),133 stereotype,134 driverInfo);135 }136 @Override137 public Optional<CreateSessionResponse> newSession(CreateSessionRequest sessionRequest) {138 if (driver != null) {139 throw new IllegalStateException("Only expected one session at a time");140 }141 Optional<WebDriver> driver = driverInfo.createDriver(sessionRequest.getCapabilities());142 if (!driver.isPresent()) {143 return Optional.empty();144 }145 if (!(driver.get() instanceof RemoteWebDriver)) {146 driver.get().quit();147 return Optional.empty();148 }149 this.driver = (RemoteWebDriver) driver.get();150 this.sessionId = this.driver.getSessionId();151 this.client = extractHttpClient(this.driver);152 this.capabilities = rewriteCapabilities(this.driver);153 this.sessionStart = Instant.now();154 LOG.info("Encoded response: " + JSON.toJson(ImmutableMap.of(155 "value", ImmutableMap.of(156 "sessionId", sessionId,157 "capabilities", capabilities))));158 events.fire(new NodeDrainStarted(getId()));159 return Optional.of(160 new CreateSessionResponse(161 getSession(sessionId),162 JSON.toJson(ImmutableMap.of(163 "value", ImmutableMap.of(164 "sessionId", sessionId,165 "capabilities", capabilities))).getBytes(UTF_8)));166 }167 private HttpClient extractHttpClient(RemoteWebDriver driver) {168 CommandExecutor executor = driver.getCommandExecutor();169 try {170 Field client = null;171 Class<?> current = executor.getClass();172 while (client == null && (current != null || Object.class.equals(current))) {173 client = findClientField(current);174 current = current.getSuperclass();175 }176 if (client == null) {177 throw new IllegalStateException("Unable to find client field in " + executor.getClass());178 }179 if (!HttpClient.class.isAssignableFrom(client.getType())) {180 throw new IllegalStateException("Client field is not assignable to http client");181 }182 client.setAccessible(true);183 return (HttpClient) client.get(executor);184 } catch (ReflectiveOperationException e) {185 throw new IllegalStateException(e);186 }187 }188 private Field findClientField(Class<?> clazz) {189 try {190 return clazz.getDeclaredField("client");191 } catch (NoSuchFieldException e) {192 return null;193 }194 }195 private Capabilities rewriteCapabilities(RemoteWebDriver driver) {196 // Rewrite the se:options if necessary197 Object rawSeleniumOptions = driver.getCapabilities().getCapability("se:options");198 if (rawSeleniumOptions == null || rawSeleniumOptions instanceof Map) {199 @SuppressWarnings("unchecked") Map<String, Object> original = (Map<String, Object>) rawSeleniumOptions;200 Map<String, Object> updated = new TreeMap<>(original == null ? new HashMap<>() : original);201 String cdpPath = String.format("/session/%s/se/cdp", driver.getSessionId());202 updated.put("cdp", rewrite(cdpPath));203 return new PersistentCapabilities(driver.getCapabilities()).setCapability("se:options", updated);204 }205 return ImmutableCapabilities.copyOf(driver.getCapabilities());206 }207 private URI rewrite(String path) {208 try {209 return new URI(210 gridUri.getScheme(),211 gridUri.getUserInfo(),212 gridUri.getHost(),213 gridUri.getPort(),214 path,215 null,216 null);217 } catch (URISyntaxException e) {218 throw new RuntimeException(e);219 }220 }221 @Override222 public HttpResponse executeWebDriverCommand(HttpRequest req) {223 LOG.info("Executing " + req);224 HttpResponse res = client.execute(req);225 if (DELETE.equals(req.getMethod()) && req.getUri().equals("/session/" + sessionId)) {226 // Ensure the response is sent before we viciously kill the node227 new Thread(228 () -> {229 try {230 Thread.sleep(500);231 } catch (InterruptedException e) {232 Thread.currentThread().interrupt();233 throw new RuntimeException(e);234 }235 LOG.info("Stopping session: " + sessionId);236 stop(sessionId);237 },238 "Node clean up: " + getId())239 .start();240 }241 return res;242 }243 @Override244 public Session getSession(SessionId id) throws NoSuchSessionException {245 if (!isSessionOwner(id)) {246 throw new NoSuchSessionException("Unable to find session with id: " + id);247 }248 return new Session(249 sessionId,250 getUri(),251 stereotype,252 capabilities,253 sessionStart); }254 @Override255 public HttpResponse uploadFile(HttpRequest req, SessionId id) {256 return null;257 }258 @Override259 public void stop(SessionId id) throws NoSuchSessionException {260 LOG.info("Stop has been called: " + id);261 Require.nonNull("Session ID", id);262 if (!isSessionOwner(id)) {263 throw new NoSuchSessionException("Unable to find session " + id);264 }265 LOG.info("Quitting session " + id);266 try {267 driver.quit();268 } catch (Exception e) {269 // It's possible that the driver has already quit.270 }271 events.fire(new SessionClosedEvent(id));272 LOG.info("Firing node drain complete message");273 events.fire(new NodeDrainComplete(getId()));274 }275 @Override276 public boolean isSessionOwner(SessionId id) {277 return driver != null && sessionId.equals(id);278 }279 @Override280 public boolean isSupporting(Capabilities capabilities) {281 return driverInfo.isSupporting(capabilities);282 }283 @Override284 public NodeStatus getStatus() {285 return new NodeStatus(286 getId(),287 getUri(),...
Source:GridModel.java
...17package org.openqa.selenium.grid.distributor.local;18import com.google.common.collect.ImmutableSet;19import org.openqa.selenium.events.EventBus;20import org.openqa.selenium.grid.data.Availability;21import org.openqa.selenium.grid.data.NodeDrainComplete;22import org.openqa.selenium.grid.data.NodeDrainStarted;23import org.openqa.selenium.grid.data.NodeId;24import org.openqa.selenium.grid.data.NodeRemovedEvent;25import org.openqa.selenium.grid.data.NodeStatus;26import org.openqa.selenium.grid.data.NodeStatusEvent;27import org.openqa.selenium.grid.data.Session;28import org.openqa.selenium.grid.data.SessionClosedEvent;29import org.openqa.selenium.grid.data.Slot;30import org.openqa.selenium.grid.data.SlotId;31import org.openqa.selenium.grid.security.Secret;32import org.openqa.selenium.internal.Require;33import org.openqa.selenium.remote.SessionId;34import java.time.Instant;35import java.util.HashSet;36import java.util.Iterator;37import java.util.Map;38import java.util.Optional;39import java.util.Set;40import java.util.concurrent.ConcurrentHashMap;41import java.util.concurrent.locks.Lock;42import java.util.concurrent.locks.ReadWriteLock;43import java.util.concurrent.locks.ReentrantReadWriteLock;44import java.util.logging.Logger;45import static org.openqa.selenium.grid.data.Availability.DOWN;46import static org.openqa.selenium.grid.data.Availability.DRAINING;47import static org.openqa.selenium.grid.data.Availability.UP;48public class GridModel {49 private static final Logger LOG = Logger.getLogger(GridModel.class.getName());50 private static final SessionId RESERVED = new SessionId("reserved");51 private final ReadWriteLock lock = new ReentrantReadWriteLock(/* fair */ true);52 private final Map<Availability, Set<NodeStatus>> nodes = new ConcurrentHashMap<>();53 private final EventBus events;54 public GridModel(EventBus events, Secret registrationSecret) {55 this.events = Require.nonNull("Event bus", events);56 Require.nonNull("Registration secret", registrationSecret);57 events.addListener(NodeDrainStarted.listener(nodeId -> setAvailability(nodeId, DRAINING)));58 events.addListener(NodeDrainComplete.listener(this::remove));59 events.addListener(NodeRemovedEvent.listener(this::remove));60 events.addListener(NodeStatusEvent.listener(status -> refresh(status)));61 events.addListener(SessionClosedEvent.listener(this::release));62 }63 public GridModel add(NodeStatus node) {64 Require.nonNull("Node", node);65 Lock writeLock = lock.writeLock();66 writeLock.lock();67 try {68 // If we've already added the node, remove it.69 for (Set<NodeStatus> nodes : nodes.values()) {70 Iterator<NodeStatus> iterator = nodes.iterator();71 while (iterator.hasNext()) {72 NodeStatus next = iterator.next();...
Source:LocalDistributor.java
...26import org.openqa.selenium.grid.data.CreateSessionRequest;27import org.openqa.selenium.grid.data.CreateSessionResponse;28import org.openqa.selenium.grid.data.DistributorStatus;29import org.openqa.selenium.grid.data.NodeAddedEvent;30import org.openqa.selenium.grid.data.NodeDrainComplete;31import org.openqa.selenium.grid.data.NodeId;32import org.openqa.selenium.grid.data.NodeRemovedEvent;33import org.openqa.selenium.grid.data.NodeStatus;34import org.openqa.selenium.grid.data.NodeStatusEvent;35import org.openqa.selenium.grid.data.Slot;36import org.openqa.selenium.grid.data.SlotId;37import org.openqa.selenium.grid.distributor.Distributor;38import org.openqa.selenium.grid.distributor.selector.DefaultSlotSelector;39import org.openqa.selenium.grid.log.LoggingOptions;40import org.openqa.selenium.grid.node.HealthCheck;41import org.openqa.selenium.grid.node.Node;42import org.openqa.selenium.grid.node.remote.RemoteNode;43import org.openqa.selenium.grid.security.Secret;44import org.openqa.selenium.grid.server.BaseServerOptions;45import org.openqa.selenium.grid.server.EventBusOptions;46import org.openqa.selenium.grid.server.NetworkOptions;47import org.openqa.selenium.grid.sessionmap.SessionMap;48import org.openqa.selenium.grid.sessionmap.config.SessionMapOptions;49import org.openqa.selenium.internal.Require;50import org.openqa.selenium.remote.http.HttpClient;51import org.openqa.selenium.remote.tracing.Tracer;52import org.openqa.selenium.status.HasReadyState;53import java.time.Duration;54import java.util.ArrayList;55import java.util.HashMap;56import java.util.List;57import java.util.Map;58import java.util.Optional;59import java.util.Set;60import java.util.concurrent.locks.Lock;61import java.util.concurrent.locks.ReadWriteLock;62import java.util.concurrent.locks.ReentrantReadWriteLock;63import java.util.function.Supplier;64import java.util.logging.Level;65import java.util.logging.Logger;66import static com.google.common.collect.ImmutableSet.toImmutableSet;67import static org.openqa.selenium.grid.data.Availability.DOWN;68import static org.openqa.selenium.grid.data.Availability.DRAINING;69public class LocalDistributor extends Distributor {70 private static final Logger LOG = Logger.getLogger(LocalDistributor.class.getName());71 private final Tracer tracer;72 private final EventBus bus;73 private final HttpClient.Factory clientFactory;74 private final SessionMap sessions;75 private final Secret registrationSecret;76 private final Regularly hostChecker = new Regularly("distributor host checker");77 private final Map<NodeId, Runnable> allChecks = new HashMap<>();78 private final ReadWriteLock lock = new ReentrantReadWriteLock(/* fair */ true);79 private final GridModel model;80 private final Map<NodeId, Node> nodes;81 public LocalDistributor(82 Tracer tracer,83 EventBus bus,84 HttpClient.Factory clientFactory,85 SessionMap sessions,86 Secret registrationSecret) {87 super(tracer, clientFactory, new DefaultSlotSelector(), sessions, registrationSecret);88 this.tracer = Require.nonNull("Tracer", tracer);89 this.bus = Require.nonNull("Event bus", bus);90 this.clientFactory = Require.nonNull("HTTP client factory", clientFactory);91 this.sessions = Require.nonNull("Session map", sessions);92 this.model = new GridModel(bus, registrationSecret);93 this.nodes = new HashMap<>();94 this.registrationSecret = Require.nonNull("Registration secret", registrationSecret);95 bus.addListener(NodeStatusEvent.listener(this::register));96 bus.addListener(NodeStatusEvent.listener(model::refresh));97 bus.addListener(NodeDrainComplete.listener(this::remove));98 }99 public static Distributor create(Config config) {100 Tracer tracer = new LoggingOptions(config).getTracer();101 EventBus bus = new EventBusOptions(config).getEventBus();102 HttpClient.Factory clientFactory = new NetworkOptions(config).getHttpClientFactory(tracer);103 SessionMap sessions = new SessionMapOptions(config).getSessionMap();104 BaseServerOptions serverOptions = new BaseServerOptions(config);105 return new LocalDistributor(tracer, bus, clientFactory, sessions, serverOptions.getRegistrationSecret());106 }107 @Override108 public boolean isReady() {109 try {110 return ImmutableSet.of(bus, sessions).parallelStream()111 .map(HasReadyState::isReady)...
Source:NodeServer.java
...26import org.openqa.selenium.grid.TemplateGridCommand;27import org.openqa.selenium.grid.config.Config;28import org.openqa.selenium.grid.config.Role;29import org.openqa.selenium.grid.data.NodeAddedEvent;30import org.openqa.selenium.grid.data.NodeDrainComplete;31import org.openqa.selenium.grid.data.NodeStatusEvent;32import org.openqa.selenium.grid.log.LoggingOptions;33import org.openqa.selenium.grid.node.HealthCheck;34import org.openqa.selenium.grid.node.Node;35import org.openqa.selenium.grid.node.ProxyNodeCdp;36import org.openqa.selenium.grid.node.config.NodeOptions;37import org.openqa.selenium.grid.server.BaseServerOptions;38import org.openqa.selenium.grid.server.EventBusOptions;39import org.openqa.selenium.grid.server.NetworkOptions;40import org.openqa.selenium.grid.server.Server;41import org.openqa.selenium.netty.server.NettyServer;42import org.openqa.selenium.remote.http.Contents;43import org.openqa.selenium.remote.http.HttpClient;44import org.openqa.selenium.remote.http.HttpHandler;45import org.openqa.selenium.remote.http.HttpResponse;46import org.openqa.selenium.remote.http.Route;47import org.openqa.selenium.remote.tracing.Tracer;48import java.time.Duration;49import java.time.temporal.ChronoUnit;50import java.util.Collections;51import java.util.Set;52import java.util.concurrent.Executors;53import java.util.logging.Logger;54import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;55import static java.net.HttpURLConnection.HTTP_NO_CONTENT;56import static org.openqa.selenium.grid.config.StandardGridRoles.EVENT_BUS_ROLE;57import static org.openqa.selenium.grid.config.StandardGridRoles.HTTPD_ROLE;58import static org.openqa.selenium.grid.config.StandardGridRoles.NODE_ROLE;59import static org.openqa.selenium.grid.data.Availability.DOWN;60import static org.openqa.selenium.remote.http.Route.get;61@AutoService(CliCommand.class)62public class NodeServer extends TemplateGridCommand {63 private static final Logger LOG = Logger.getLogger(NodeServer.class.getName());64 @Override65 public String getName() {66 return "node";67 }68 @Override69 public String getDescription() {70 return "Adds this server as a node in the selenium grid.";71 }72 @Override73 public Set<Role> getConfigurableRoles() {74 return ImmutableSet.of(EVENT_BUS_ROLE, HTTPD_ROLE, NODE_ROLE);75 }76 @Override77 public Set<Object> getFlagObjects() {78 return Collections.emptySet();79 }80 @Override81 protected String getSystemPropertiesConfigPrefix() {82 return "node";83 }84 @Override85 protected Config getDefaultConfig() {86 return new DefaultNodeConfig();87 }88 @Override89 protected void execute(Config config) {90 LoggingOptions loggingOptions = new LoggingOptions(config);91 Tracer tracer = loggingOptions.getTracer();92 EventBusOptions events = new EventBusOptions(config);93 EventBus bus = events.getEventBus();94 NetworkOptions networkOptions = new NetworkOptions(config);95 HttpClient.Factory clientFactory = networkOptions.getHttpClientFactory(tracer);96 BaseServerOptions serverOptions = new BaseServerOptions(config);97 LOG.info("Reporting self as: " + serverOptions.getExternalUri());98 NodeOptions nodeOptions = new NodeOptions(config);99 Node node = nodeOptions.getNode();100 HttpHandler readinessCheck = req -> {101 if (node.getStatus().hasCapacity()) {102 return new HttpResponse()103 .setStatus(HTTP_NO_CONTENT);104 }105 return new HttpResponse()106 .setStatus(HTTP_INTERNAL_ERROR)107 .setHeader("Content-Type", MediaType.PLAIN_TEXT_UTF_8.toString())108 .setContent(Contents.utf8String("No capacity available"));109 };110 bus.addListener(NodeAddedEvent.listener(nodeId -> {111 if (node.getId().equals(nodeId)) {112 LOG.info("Node has been added");113 }114 }));115 bus.addListener(NodeDrainComplete.listener(nodeId -> {116 if (!node.getId().equals(nodeId)) {117 return;118 }119 // Wait a beat before shutting down so the final response from the120 // node can escape.121 new Thread(122 () -> {123 try {124 Thread.sleep(1000);125 } catch (InterruptedException e) {126 // Swallow, the next thing we're doing is shutting down127 }128 LOG.info("Shutting down");129 System.exit(0);...
Source:NodeDrainComplete.java
...17package org.openqa.selenium.grid.data;18import org.openqa.selenium.events.Event;19import org.openqa.selenium.events.Type;20import java.util.UUID;21public class NodeDrainComplete extends Event {22 public static final Type NODE_DRAIN_COMPLETE = new Type("node-drain-complete");23 public NodeDrainComplete(UUID id) {24 super(NODE_DRAIN_COMPLETE, id);25 }26}...
NodeDrainComplete
Using AI Code Generation
1package org.openqa.selenium.grid.data;2import org.openqa.selenium.internal.Require;3import java.util.Objects;4public class NodeDrainComplete {5 private final NodeId nodeId;6 public NodeDrainComplete(NodeId nodeId) {7 this.nodeId = Require.nonNull("Node id", nodeId);8 }9 public NodeId getNodeId() {10 return nodeId;11 }12 public boolean equals(Object o) {13 if (!(o instanceof NodeDrainComplete)) {14 return false;15 }16 NodeDrainComplete that = (NodeDrainComplete) o;17 return Objects.equals(nodeId, that.nodeId);18 }19 public int hashCode() {20 return Objects.hash(nodeId);21 }22}23package org.openqa.selenium.grid.data;24import org.openqa.selenium.internal.Require;25import java.util.Objects;26public class NodeDrainComplete {27 private final NodeId nodeId;28 public NodeDrainComplete(NodeId nodeId) {29 this.nodeId = Require.nonNull("Node id", nodeId);30 }31 public NodeId getNodeId() {32 return nodeId;33 }34 public boolean equals(Object o) {35 if (!(o instanceof NodeDrainComplete)) {36 return false;37 }38 NodeDrainComplete that = (NodeDrainComplete) o;39 return Objects.equals(nodeId, that.nodeId);40 }41 public int hashCode() {42 return Objects.hash(nodeId);43 }44}45package org.openqa.selenium.grid.data;46import org.openqa.selenium.internal.Require;47import java.util.Objects;48public class NodeDrainComplete {49 private final NodeId nodeId;50 public NodeDrainComplete(NodeId nodeId) {51 this.nodeId = Require.nonNull("Node id", nodeId);52 }53 public NodeId getNodeId() {54 return nodeId;55 }56 public boolean equals(Object o) {57 if (!(o instanceof NodeDrainComplete)) {58 return false;59 }60 NodeDrainComplete that = (NodeDrainComplete) o;61 return Objects.equals(nodeId, that.nodeId);62 }63 public int hashCode() {64 return Objects.hash(nodeId);65 }66}
NodeDrainComplete
Using AI Code Generation
1import org.openqa.selenium.grid.data.NodeDrainComplete;2import org.openqa.selenium.grid.data.NodeDrainStarted;3import org.openqa.selenium.grid.data.NodeDrainStarting;4import org.openqa.selenium.grid.data.NodeEvent;5import org.openqa.selenium.grid.data.NodeStatus;6import org.openqa.selenium.grid.data.NodeStatusEvent;7import org.openqa.selenium.grid.data.NodeUp;8import org.openqa.selenium.grid.data.NodeUsageRecord;9import org.openqa.selenium.grid.data.NodeUsageRecordEvent;10import org.openqa.selenium.grid.data.StandaloneNode;11import org.openqa.selenium.grid.data.StandaloneNodeFactory;12import org.openqa.selenium.grid.data.StandaloneNodeFactory.NodeSupplier;13import org.openqa.selenium.grid.data.StandaloneNodeFactory.NodeSupplierBuilder;14import org.openqa.selenium.grid.data.StandaloneNodeFactory.NodeSupplierBuilder.NodeSupplierBuilderImpl;15import org.openqa.selenium.grid.data.StandaloneNodeFactory.NodeSupplierImpl;16import org.openqa.selenium.grid.data.StandaloneNodeImpl;17import org.openqa.selenium.grid.data.StandaloneNodeStatus;18import org.openqa.selenium.grid.data.StandaloneNodeStatusEvent;19import org.openqa.selenium.grid.data.StandaloneNodeUsageRecord;20import org.openqa.selenium.grid.data.StandaloneNodeUsageRecordEvent;21import org.openqa.selenium.grid.data.StandaloneNodeUsageRecordEventImpl;22import org.openqa.selenium.grid.data.StandaloneNodeUsageRecordImpl;23import org.openqa.selenium.grid.data.StandaloneSession;24import org.openqa.selenium.grid.data.StandaloneSessionEvent;25import org.openqa.selenium.grid.data.StandaloneSessionImpl;26import org.openqa.selenium.grid.data.StandaloneSessionRequest;27import org.openqa.selenium.grid.data.StandaloneSessionRequestEvent;28import org.openqa.selenium.grid.data.StandaloneSessionRequestImpl;29import org.openqa.selenium.grid.data.StandaloneSessionRequestImpl.StandaloneSessionRequestBuilder;30import org.openqa.selenium.grid.data.StandaloneSessionRequestImpl.StandaloneSessionRequestBuilderImpl;31import org.openqa.selenium.grid.data.StandaloneSessionRequestImpl.StandaloneSessionRequestImplBuilder;32import org.openqa.selenium.grid.data.StandaloneSessionRequestImpl.StandaloneSessionRequestImplBuilderImpl;33import org.openqa.selenium.grid.data.StandaloneSessionRequestImpl.StandaloneSessionRequestImplBuilderImpl.StandaloneSessionRequestImplBuilderImplBuilder;34import org.openqa.selenium.grid.data.StandaloneSessionRequestImpl.StandaloneSessionRequestImplBuilderImpl.StandaloneSessionRequestImplBuilderImplBuilderImpl;35import org.openqa.selenium.grid.data.StandaloneSessionRequestImpl.StandaloneSessionRequestImplBuilderImpl.StandaloneSessionRequestImplBuilderImplBuilderImpl.StandaloneSessionRequestImplBuilderImplBuilderImplBuilder;36import org.openqa.selenium.grid.data.StandaloneSession
NodeDrainComplete
Using AI Code Generation
1NodeDrainComplete drainComplete = new NodeDrainComplete(nodeId, drainedAt);2NodeDrainStarted drainStarted = new NodeDrainStarted(nodeId, drainedAt);3NodeDrainStartedEvent drainStartedEvent = new NodeDrainStartedEvent(nodeId, drainedAt);4NodeDrainCompleteEvent drainCompleteEvent = new NodeDrainCompleteEvent(nodeId, drainedAt);5NodeDrainStartedEvent drainStartedEvent = new NodeDrainStartedEvent(nodeId, drainedAt);6NodeDrainCompleteEvent drainCompleteEvent = new NodeDrainCompleteEvent(nodeId, drainedAt);7NodeDrainStartedEvent drainStartedEvent = new NodeDrainStartedEvent(nodeId, drainedAt);8NodeDrainCompleteEvent drainCompleteEvent = new NodeDrainCompleteEvent(nodeId, drainedAt);9NodeDrainStartedEvent drainStartedEvent = new NodeDrainStartedEvent(nodeId, drainedAt);10NodeDrainCompleteEvent drainCompleteEvent = new NodeDrainCompleteEvent(nodeId, drainedAt);11NodeDrainStartedEvent drainStartedEvent = new NodeDrainStartedEvent(nodeId, drainedAt);12NodeDrainCompleteEvent drainCompleteEvent = new NodeDrainCompleteEvent(nodeId, drainedAt);13NodeDrainStartedEvent drainStartedEvent = new NodeDrainStartedEvent(nodeId, drainedAt);
LambdaTest’s Selenium 4 tutorial is covering every aspects of Selenium 4 testing with examples and best practices. Here you will learn basics, such as how to upgrade from Selenium 3 to Selenium 4, to some advanced concepts, such as Relative locators and Selenium Grid 4 for Distributed testing. Also will learn new features of Selenium 4, such as capturing screenshots of specific elements, opening a new tab or window on the browser, and new protocol adoptions.
Upgrading From Selenium 3 To Selenium 4?: In this chapter, learn in detail how to update Selenium 3 to Selenium 4 for Java binding. Also, learn how to upgrade while using different build tools such as Maven or Gradle and get comprehensive guidance for upgrading Selenium.
What’s New In Selenium 4 & What’s Being Deprecated? : Get all information about new implementations in Selenium 4, such as W3S protocol adaption, Optimized Selenium Grid, and Enhanced Selenium IDE. Also, learn what is deprecated for Selenium 4, such as DesiredCapabilites and FindsBy methods, etc.
Selenium 4 With Python: Selenium supports all major languages, such as Python, C#, Ruby, and JavaScript. In this chapter, learn how to install Selenium 4 for Python and the features of Python in Selenium 4, such as Relative locators, Browser manipulation, and Chrom DevTool protocol.
Selenium 4 Is Now W3C Compliant: JSON Wireframe protocol is retiring from Selenium 4, and they are adopting W3C protocol to learn in detail about the advantages and impact of these changes.
How To Use Selenium 4 Relative Locator? : Selenium 4 came with new features such as Relative Locators that allow constructing locators with reference and easily located constructors nearby. Get to know its different use cases with examples.
Selenium Grid 4 Tutorial For Distributed Testing: Selenium Grid 4 allows you to perform tests over different browsers, OS, and device combinations. It also enables parallel execution browser testing, reads up on various features of Selenium Grid 4 and how to download it, and runs a test on Selenium Grid 4 with best practices.
Selenium Video Tutorials: Binge on video tutorials on Selenium by industry experts to get step-by-step direction from automating basic to complex test scenarios with Selenium.
LambdaTest also provides certification for Selenium testing to accelerate your career in Selenium automation testing.
Get 100 minutes of automation test minutes FREE!!