diff --git a/common/src/web/downloads/download.html b/common/src/web/downloads/download.html
new file mode 100644
index 0000000000000..8c69ffed5e9d5
--- /dev/null
+++ b/common/src/web/downloads/download.html
@@ -0,0 +1,40 @@
+
+
+
+
+ Downloads
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/common/src/web/downloads/file_1.txt b/common/src/web/downloads/file_1.txt
new file mode 100644
index 0000000000000..8ab686eafeb1f
--- /dev/null
+++ b/common/src/web/downloads/file_1.txt
@@ -0,0 +1 @@
+Hello, World!
diff --git a/common/src/web/downloads/file_2.jpg b/common/src/web/downloads/file_2.jpg
new file mode 100644
index 0000000000000..402237cbdd6cf
Binary files /dev/null and b/common/src/web/downloads/file_2.jpg differ
diff --git a/java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java b/java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java
index 8c55229761c2d..56b00bee09e63 100644
--- a/java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java
+++ b/java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java
@@ -64,6 +64,10 @@ public boolean matches(Capabilities stereotype, Capabilities capabilities) {
return false;
}
+ if (!managedDownloadsEnabled(stereotype, capabilities)) {
+ return false;
+ }
+
if (!platformVersionMatch(stereotype, capabilities)) {
return false;
}
@@ -106,6 +110,19 @@ private Boolean initialMatch(Capabilities stereotype, Capabilities capabilities)
.orElse(true);
}
+ private Boolean managedDownloadsEnabled(Capabilities stereotype, Capabilities capabilities) {
+ // First lets check if user wanted a Node with managed downloads enabled
+ Object raw = capabilities.getCapability("se:downloadsEnabled");
+ if (raw == null || !Boolean.parseBoolean(raw.toString())) {
+ // User didn't ask. So lets move on to the next matching criteria
+ return true;
+ }
+ // User wants managed downloads enabled to be done on this Node, let's check the stereotype
+ raw = stereotype.getCapability("se:downloadsEnabled");
+ // Try to match what the user requested
+ return raw != null && Boolean.parseBoolean(raw.toString());
+ }
+
private Boolean platformVersionMatch(Capabilities stereotype, Capabilities capabilities) {
/*
This platform version match is not W3C compliant but users can add Appium servers as
diff --git a/java/src/org/openqa/selenium/grid/node/Node.java b/java/src/org/openqa/selenium/grid/node/Node.java
index c6368f2b4fb85..bd4d498d1c461 100644
--- a/java/src/org/openqa/selenium/grid/node/Node.java
+++ b/java/src/org/openqa/selenium/grid/node/Node.java
@@ -18,9 +18,11 @@
package org.openqa.selenium.grid.node;
import com.google.common.collect.ImmutableMap;
+
import org.openqa.selenium.BuildInfo;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.NoSuchSessionException;
+import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.grid.data.CreateSessionRequest;
import org.openqa.selenium.grid.data.CreateSessionResponse;
import org.openqa.selenium.grid.data.NodeId;
@@ -41,13 +43,13 @@
import org.openqa.selenium.remote.tracing.SpanDecorator;
import org.openqa.selenium.remote.tracing.Tracer;
import org.openqa.selenium.status.HasReadyState;
-import org.openqa.selenium.WebDriverException;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
+import java.util.UUID;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
@@ -210,16 +212,21 @@ public ImmutableMap getOsInfo() {
return OS_INFO;
}
- public abstract Either newSession(CreateSessionRequest sessionRequest);
+ public abstract Either newSession(
+ CreateSessionRequest sessionRequest);
public abstract HttpResponse executeWebDriverCommand(HttpRequest req);
public abstract Session getSession(SessionId id) throws NoSuchSessionException;
- public TemporaryFilesystem getTemporaryFilesystem(SessionId id) throws IOException {
+ public TemporaryFilesystem getUploadsFilesystem(SessionId id) throws IOException {
throw new UnsupportedOperationException();
}
+ public TemporaryFilesystem getDownloadsFilesystem(UUID uuid) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
public abstract HttpResponse uploadFile(HttpRequest req, SessionId id);
public abstract HttpResponse downloadFile(HttpRequest req, SessionId id);
diff --git a/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java b/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java
index c004c1c206514..d05c67ce32836 100644
--- a/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java
+++ b/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java
@@ -230,12 +230,16 @@ public class NodeFlags implements HasRoles {
private String nodeImplementation = DEFAULT_NODE_IMPLEMENTATION;
@Parameter(
- names = {"--downloads-path"},
- description = "The default location wherein all browser triggered file downloads would be "
- + "available to be retrieved from. This is usually the directory that you configure in "
- + "your browser as the default location for storing downloaded files.")
- @ConfigValue(section = NODE_SECTION, name = "downloads-path", example = "")
- private String downloadsPath = "";
+ names = {"--enable-managed-downloads"},
+ arity = 1,
+ description = "When enabled, the Grid node will automatically do the following: " +
+ "1. Creates a directory named '$HOME/.cache/selenium/downloads/' which "
+ + "will now represent the directory into which files downloaded by "
+ + "Chrome/Firefox/Edge browser will be under. " +
+ "2. For every new session, a sub-directory will be created/deleted so that "
+ + "all files that were downloaded for a given session are stored in.")
+ @ConfigValue(section = NODE_SECTION, name = "enable-managed-downloads", example = "false")
+ public Boolean managedDownloadsEnabled;
@Override
public Set getRoles() {
diff --git a/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java b/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java
index ef8647a28afb1..ca52c43c6c836 100644
--- a/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java
+++ b/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java
@@ -40,6 +40,7 @@
import org.openqa.selenium.json.JsonOutput;
import org.openqa.selenium.net.NetworkUtils;
import org.openqa.selenium.net.Urls;
+import org.openqa.selenium.remote.Browser;
import org.openqa.selenium.remote.service.DriverService;
import java.io.File;
@@ -149,8 +150,9 @@ public Optional getPublicGridUri() {
}
}
- public Optional getDownloadsPath() {
- return config.get(NODE_SECTION, "downloads-path");
+ public boolean isManagedDownloadsEnabled() {
+ return config.getBool(NODE_SECTION, "enable-managed-downloads")
+ .orElse(Boolean.FALSE);
}
public Node getNode() {
@@ -634,9 +636,17 @@ private Capabilities enhanceStereotype(Capabilities capabilities) {
.setCapability("se:vncEnabled", true)
.setCapability("se:noVncPort", noVncPort());
}
+ if (isManagedDownloadsEnabled() && canConfigureDownloadsDir(capabilities)) {
+ capabilities = new PersistentCapabilities(capabilities)
+ .setCapability("se:downloadsEnabled", true);
+ }
return capabilities;
}
+ private boolean canConfigureDownloadsDir(Capabilities caps) {
+ return Browser.FIREFOX.is(caps) || Browser.CHROME.is(caps) || Browser.EDGE.is(caps);
+ }
+
private void report(Map.Entry> entry) {
StringBuilder caps = new StringBuilder();
try (JsonOutput out = JSON.newOutput(caps)) {
diff --git a/java/src/org/openqa/selenium/grid/node/local/LocalNode.java b/java/src/org/openqa/selenium/grid/node/local/LocalNode.java
index 58d942de950de..e22a96a0cdbbb 100644
--- a/java/src/org/openqa/selenium/grid/node/local/LocalNode.java
+++ b/java/src/org/openqa/selenium/grid/node/local/LocalNode.java
@@ -26,7 +26,6 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import java.util.Arrays;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.ImmutableCapabilities;
import org.openqa.selenium.MutableCapabilities;
@@ -63,6 +62,7 @@
import org.openqa.selenium.io.TemporaryFilesystem;
import org.openqa.selenium.io.Zip;
import org.openqa.selenium.json.Json;
+import org.openqa.selenium.remote.Browser;
import org.openqa.selenium.remote.SessionId;
import org.openqa.selenium.remote.http.HttpMethod;
import org.openqa.selenium.remote.http.HttpRequest;
@@ -76,12 +76,14 @@
import java.io.File;
import java.io.IOException;
+import java.io.Serializable;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -116,6 +118,7 @@ public class LocalNode extends Node {
private static final Json JSON = new Json();
private static final Logger LOG = Logger.getLogger(LocalNode.class.getName());
+
private final EventBus bus;
private final URI externalUri;
private final URI gridUri;
@@ -124,13 +127,15 @@ public class LocalNode extends Node {
private final int maxSessionCount;
private final int configuredSessionCount;
private final boolean cdpEnabled;
- private final String downloadsPath;
+ private final boolean managedDownloadsEnabled;
private final boolean bidiEnabled;
private final AtomicBoolean drainAfterSessions = new AtomicBoolean();
private final List factories;
private final Cache currentSessions;
- private final Cache tempFileSystems;
+ private final Cache uploadsTempFileSystem;
+ private final Cache downloadsTempFileSystem;
+ private final Cache sessionToDownloadsDir;
private final AtomicInteger pendingSessions = new AtomicInteger();
private final AtomicInteger sessionCount = new AtomicInteger();
@@ -149,7 +154,7 @@ protected LocalNode(
Duration heartbeatPeriod,
List factories,
Secret registrationSecret,
- String downloadsPath) {
+ boolean managedDownloadsEnabled) {
super(tracer, new NodeId(UUID.randomUUID()), uri, registrationSecret);
this.bus = Require.nonNull("Event bus", bus);
@@ -166,7 +171,7 @@ protected LocalNode(
this.sessionCount.set(drainAfterSessionCount);
this.cdpEnabled = cdpEnabled;
this.bidiEnabled = bidiEnabled;
- this.downloadsPath = Optional.ofNullable(downloadsPath).orElse("");
+ this.managedDownloadsEnabled = managedDownloadsEnabled;
this.healthCheck = healthCheck == null ?
() -> {
@@ -176,7 +181,7 @@ protected LocalNode(
String.format("%s is %s", uri, status.getAvailability()));
} : healthCheck;
- this.tempFileSystems = CacheBuilder.newBuilder()
+ this.uploadsTempFileSystem = CacheBuilder.newBuilder()
.expireAfterAccess(sessionTimeout)
.ticker(ticker)
.removalListener((RemovalListener) notification -> {
@@ -186,6 +191,26 @@ protected LocalNode(
})
.build();
+ this.downloadsTempFileSystem = CacheBuilder.newBuilder()
+ .expireAfterAccess(sessionTimeout)
+ .ticker(ticker)
+ .removalListener((RemovalListener) notification -> {
+ TemporaryFilesystem tempFS = notification.getValue();
+ tempFS.deleteTemporaryFiles();
+ tempFS.deleteBaseDir();
+ })
+ .build();
+
+ this.sessionToDownloadsDir = CacheBuilder.newBuilder()
+ .expireAfterAccess(sessionTimeout)
+ .ticker(ticker)
+ .removalListener((RemovalListener) notification -> {
+ if (notification.getValue() != null) {
+ this.downloadsTempFileSystem.invalidate(notification.getValue());
+ }
+ })
+ .build();
+
this.currentSessions = CacheBuilder.newBuilder()
.expireAfterAccess(sessionTimeout)
.ticker(ticker)
@@ -203,16 +228,27 @@ protected LocalNode(
sessionCleanupNodeService.scheduleAtFixedRate(
GuardedRunnable.guard(currentSessions::cleanUp), 30, 30, TimeUnit.SECONDS);
- ScheduledExecutorService tempFileCleanupNodeService =
+ ScheduledExecutorService uploadTempFileCleanupNodeService =
Executors.newSingleThreadScheduledExecutor(
r -> {
Thread thread = new Thread(r);
thread.setDaemon(true);
- thread.setName("TempFile Cleanup Node " + externalUri);
+ thread.setName("UploadTempFile Cleanup Node " + externalUri);
return thread;
});
- tempFileCleanupNodeService.scheduleAtFixedRate(
- GuardedRunnable.guard(tempFileSystems::cleanUp), 30, 30, TimeUnit.SECONDS);
+ uploadTempFileCleanupNodeService.scheduleAtFixedRate(
+ GuardedRunnable.guard(uploadsTempFileSystem::cleanUp), 30, 30, TimeUnit.SECONDS);
+
+ ScheduledExecutorService downloadTempFileCleanupNodeService =
+ Executors.newSingleThreadScheduledExecutor(
+ r -> {
+ Thread thread = new Thread(r);
+ thread.setDaemon(true);
+ thread.setName("DownloadTempFile Cleanup Node " + externalUri);
+ return thread;
+ });
+ downloadTempFileCleanupNodeService.scheduleAtFixedRate(
+ GuardedRunnable.guard(downloadsTempFileSystem::cleanUp), 30, 30, TimeUnit.SECONDS);
ScheduledExecutorService heartbeatNodeService =
Executors.newSingleThreadScheduledExecutor(
@@ -247,8 +283,10 @@ private void stopTimedOutSession(RemovalNotification not
}
// Attempt to stop the session
slot.stop();
- // Invalidate temp file system
- this.tempFileSystems.invalidate(id);
+ // Invalidate uploads temp file system
+ this.uploadsTempFileSystem.invalidate(id);
+ // Invalidate downloads temp file system
+ this.sessionToDownloadsDir.invalidate(id);
// Decrement pending sessions if Node is draining
if (this.isDraining()) {
int done = pendingSessions.decrementAndGet();
@@ -283,6 +321,15 @@ public int getCurrentSessionCount() {
return Math.toIntExact(currentSessions.size());
}
+ @VisibleForTesting
+ public UUID getDownloadsIdForSession(SessionId id) {
+ UUID uuid = sessionToDownloadsDir.getIfPresent(id);
+ if (uuid == null) {
+ throw new NoSuchSessionException("Cannot find session with id: " + id);
+ }
+ return uuid;
+ }
+
@ManagedAttribute(name = "MaxSessions")
public int getMaxSessionCount() {
return maxSessionCount;
@@ -381,10 +428,21 @@ public Either newSession(CreateSessio
new RetrySessionRequestException("No slot matched the requested capabilities."));
}
+ UUID uuidForSessionDownloads = UUID.randomUUID();
+ Capabilities desiredCapabilities = sessionRequest.getDesiredCapabilities();
+ if (managedDownloadsRequested(desiredCapabilities)) {
+ Capabilities enhanced = setDownloadsDirectory(uuidForSessionDownloads, desiredCapabilities);
+ enhanced = desiredCapabilities.merge(enhanced);
+ sessionRequest = new CreateSessionRequest(sessionRequest.getDownstreamDialects(),
+ enhanced,
+ sessionRequest.getMetadata());
+ }
+
Either possibleSession = slotToUse.apply(sessionRequest);
if (possibleSession.isRight()) {
ActiveSession session = possibleSession.right();
+ sessionToDownloadsDir.put(session.getId(), uuidForSessionDownloads);
currentSessions.put(session.getId(), slotToUse);
checkSessionCount();
@@ -407,7 +465,7 @@ public Either newSession(CreateSessio
externalUri,
slotToUse.isSupportingCdp(),
slotToUse.isSupportingBiDi(),
- sessionRequest.getDesiredCapabilities());
+ desiredCapabilities);
String sessionCreatedMessage = "Session created by the Node";
LOG.info(String.format("%s. Id: %s, Caps: %s", sessionCreatedMessage, sessionId, caps));
@@ -425,6 +483,49 @@ public Either newSession(CreateSessio
}
}
+ private boolean managedDownloadsRequested(Capabilities capabilities) {
+ Object downloadsEnabled = capabilities.getCapability("se:downloadsEnabled");
+ return managedDownloadsEnabled && downloadsEnabled != null &&
+ Boolean.parseBoolean(downloadsEnabled.toString());
+ }
+
+ private Capabilities setDownloadsDirectory(UUID uuid, Capabilities caps) {
+ File tempDir;
+ try {
+ TemporaryFilesystem tempFS = getDownloadsFilesystem(uuid);
+// tempDir = tempFS.createTempDir("download", "file");
+ tempDir = tempFS.createTempDir("download", "");
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ if (Browser.CHROME.is(caps) || Browser.EDGE.is(caps)) {
+ ImmutableMap map = ImmutableMap.of(
+ "download.prompt_for_download", false,
+ "download.default_directory", tempDir.getAbsolutePath());
+ String optionsKey = Browser.CHROME.is(caps) ? "goog:chromeOptions" : "ms:edgeOptions";
+ return appendPrefs(caps, optionsKey, map);
+ }
+ if (Browser.FIREFOX.is(caps)) {
+ ImmutableMap map = ImmutableMap.of(
+ "browser.download.folderList", 2,
+ "browser.download.dir", tempDir.getAbsolutePath());
+ return appendPrefs(caps, "moz:firefoxOptions", map);
+ }
+ return caps;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Capabilities appendPrefs(Capabilities caps,
+ String optionsKey, Map map) {
+ Map currentOptions = (Map) Optional
+ .ofNullable(caps.getCapability(optionsKey))
+ .orElse(new HashMap<>());
+
+ ((Map) currentOptions
+ .computeIfAbsent("prefs", k -> new HashMap<>())).putAll(map);
+ return caps;
+ }
+
@Override
public boolean isSessionOwner(SessionId id) {
Require.nonNull("Session ID", id);
@@ -449,10 +550,20 @@ public Session getSession(SessionId id) throws NoSuchSessionException {
}
@Override
- public TemporaryFilesystem getTemporaryFilesystem(SessionId id) throws IOException {
+ public TemporaryFilesystem getUploadsFilesystem(SessionId id) throws IOException {
try {
- return tempFileSystems.get(id, () -> TemporaryFilesystem.getTmpFsBasedOn(
- TemporaryFilesystem.getDefaultTmpFS().createTempDir("session", id.toString())));
+ return uploadsTempFileSystem.get(id, () -> TemporaryFilesystem.getTmpFsBasedOn(
+ TemporaryFilesystem.getDefaultTmpFS().createTempDir("session", id.toString())));
+ } catch (ExecutionException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public TemporaryFilesystem getDownloadsFilesystem(UUID uuid) throws IOException {
+ try {
+ return downloadsTempFileSystem.get(uuid, () -> TemporaryFilesystem.getTmpFsBasedOn(
+ TemporaryFilesystem.getDefaultTmpFS().createTempDir("uuid", uuid.toString())));
} catch (ExecutionException e) {
throw new IOException(e);
}
@@ -484,35 +595,36 @@ public HttpResponse downloadFile(HttpRequest req, SessionId id) {
if (slot != null && slot.getSession() instanceof DockerSession) {
return executeWebDriverCommand(req);
}
- if (this.downloadsPath.isEmpty()) {
- String msg = "Please specify the path wherein the files downloaded using the browser "
- + "would be available via the command line arg [--downloads-path] and restart the node";
+ if (!this.managedDownloadsEnabled) {
+ String msg = "Please enable management of downloads via the command line arg "
+ + "[--enable-managed-downloads] and restart the node";
throw new WebDriverException(msg);
}
- File dir = new File(this.downloadsPath);
- if (!dir.exists()) {
- throw new WebDriverException(
- String.format("Cannot locate downloads directory %s.", downloadsPath));
+ UUID uuid = sessionToDownloadsDir.getIfPresent(id);
+ if (uuid == null) {
+ throw new NoSuchSessionException("Cannot find session with id: " + id);
}
- if (!dir.isDirectory()) {
- throw new WebDriverException(String.format("Invalid directory: %s.", downloadsPath));
+ TemporaryFilesystem tempFS = downloadsTempFileSystem.getIfPresent(uuid);
+ if (tempFS == null) {
+ throw new WebDriverException("Cannot find downloads file system for session id: " + id);
}
+ File downloadsDirectory = Optional
+ .ofNullable(tempFS.getBaseDir().listFiles())
+ .orElse(new File[]{})[0];
if (req.getMethod().equals(HttpMethod.GET)) {
//User wants to list files that can be downloaded
List collected = Arrays.stream(
- Optional.ofNullable(dir.listFiles()).orElse(new File[]{})
+ Optional.ofNullable(downloadsDirectory.listFiles()).orElse(new File[]{})
).map(File::getName).collect(Collectors.toList());
ImmutableMap data = ImmutableMap.of("names", collected);
- ImmutableMap> result = ImmutableMap.of("value",data);
+ ImmutableMap> result = ImmutableMap.of("value", data);
return new HttpResponse().setContent(asJson(result));
}
-
String raw = string(req);
if (raw.isEmpty()) {
throw new WebDriverException(
"Please specify file to download in payload as {\"name\": \"fileToDownload\"}");
}
-
Map incoming = JSON.toType(raw, Json.MAP_TYPE);
String filename = Optional.ofNullable(incoming.get("name"))
.map(Object::toString)
@@ -521,11 +633,13 @@ public HttpResponse downloadFile(HttpRequest req, SessionId id) {
"Please specify file to download in payload as {\"name\": \"fileToDownload\"}"));
try {
File[] allFiles = Optional.ofNullable(
- dir.listFiles((dir1, name) -> name.equals(filename))
+ downloadsDirectory.listFiles((dir, name) -> name.equals(filename))
).orElse(new File[]{});
if (allFiles.length == 0) {
throw new WebDriverException(
- String.format("Cannot find file [%s] in directory %s.", filename, downloadsPath));
+ String.format("Cannot find file [%s] in directory %s.",
+ filename,
+ downloadsDirectory.getAbsolutePath()));
}
if (allFiles.length != 1) {
throw new WebDriverException(
@@ -556,8 +670,8 @@ public HttpResponse uploadFile(HttpRequest req, SessionId id) {
File tempDir;
try {
- TemporaryFilesystem tempfs = getTemporaryFilesystem(id);
- tempDir = tempfs.createTempDir("upload", "file");
+ TemporaryFilesystem tempFS = getUploadsFilesystem(id);
+ tempDir = tempFS.createTempDir("upload", "file");
Zip.unzip((String) incoming.get("file"), tempDir);
} catch (IOException e) {
@@ -567,7 +681,7 @@ public HttpResponse uploadFile(HttpRequest req, SessionId id) {
File[] allFiles = tempDir.listFiles();
if (allFiles == null) {
throw new WebDriverException(
- String.format("Cannot access temporary directory for uploaded files %s", tempDir));
+ String.format("Cannot access temporary directory for uploaded files %s", tempDir));
}
if (allFiles.length != 1) {
throw new WebDriverException(
@@ -622,20 +736,20 @@ private Session createExternalSession(ActiveSession other,
}
});
toUse = new PersistentCapabilities(cdpFiltered).setCapability("se:cdpEnabled", false);
- }
+ }
- // Add se:bidi if necessary to send the bidi url back
- if ((isSupportingBiDi || toUse.getCapability("se:bidi") != null) && bidiEnabled) {
- String bidiPath = String.format("/session/%s/se/bidi", other.getId());
- toUse = new PersistentCapabilities(toUse).setCapability("se:bidi", rewrite(bidiPath));
- } else {
- // Remove any se:bidi* from the response, BiDi is not supported nor enabled
- MutableCapabilities bidiFiltered = new MutableCapabilities();
- toUse.asMap().forEach((key, value) -> {
- if (!key.startsWith("se:bidi")) {
- bidiFiltered.setCapability(key, value);
- }
- });
+ // Add se:bidi if necessary to send the bidi url back
+ if ((isSupportingBiDi || toUse.getCapability("se:bidi") != null) && bidiEnabled) {
+ String bidiPath = String.format("/session/%s/se/bidi", other.getId());
+ toUse = new PersistentCapabilities(toUse).setCapability("se:bidi", rewrite(bidiPath));
+ } else {
+ // Remove any se:bidi* from the response, BiDi is not supported nor enabled
+ MutableCapabilities bidiFiltered = new MutableCapabilities();
+ toUse.asMap().forEach((key, value) -> {
+ if (!key.startsWith("se:bidi")) {
+ bidiFiltered.setCapability(key, value);
+ }
+ });
toUse = new PersistentCapabilities(bidiFiltered).setCapability("se:bidiEnabled", false);
}
@@ -772,7 +886,7 @@ public static class Builder {
private Duration sessionTimeout = Duration.ofSeconds(NodeOptions.DEFAULT_SESSION_TIMEOUT);
private HealthCheck healthCheck;
private Duration heartbeatPeriod = Duration.ofSeconds(NodeOptions.DEFAULT_HEARTBEAT_PERIOD);
- private String downloadsPath = "";
+ private boolean managedDownloadsEnabled = false;
private Builder(
Tracer tracer,
@@ -827,8 +941,8 @@ public Builder heartbeatPeriod(Duration heartbeatPeriod) {
return this;
}
- public Builder downloadsPath(String path) {
- this.downloadsPath = path;
+ public Builder enableManagedDownloads(boolean enable) {
+ this.managedDownloadsEnabled = enable;
return this;
}
@@ -848,7 +962,7 @@ public LocalNode build() {
heartbeatPeriod,
factories.build(),
registrationSecret,
- downloadsPath);
+ managedDownloadsEnabled);
}
public Advanced advanced() {
diff --git a/java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java b/java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java
index 6475845c593e7..6942f7a7aaf00 100644
--- a/java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java
+++ b/java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java
@@ -71,13 +71,12 @@ public static Node create(Config config) {
.drainAfterSessionCount(nodeOptions.getDrainAfterSessionCount())
.enableCdp(nodeOptions.isCdpEnabled())
.enableBiDi(nodeOptions.isBiDiEnabled())
+ .enableManagedDownloads(nodeOptions.isManagedDownloadsEnabled())
.heartbeatPeriod(nodeOptions.getHeartbeatPeriod());
List> builders = new ArrayList<>();
ServiceLoader.load(DriverService.Builder.class).forEach(builders::add);
- nodeOptions.getDownloadsPath().ifPresent(builder::downloadsPath);
-
nodeOptions
.getSessionFactories(
caps -> createSessionFactory(tracer, clientFactory, sessionTimeout, builders, caps))
diff --git a/java/src/org/openqa/selenium/io/TemporaryFilesystem.java b/java/src/org/openqa/selenium/io/TemporaryFilesystem.java
index 43c09c32f4e80..e7994bc90c262 100644
--- a/java/src/org/openqa/selenium/io/TemporaryFilesystem.java
+++ b/java/src/org/openqa/selenium/io/TemporaryFilesystem.java
@@ -29,10 +29,10 @@
/**
* A wrapper around temporary filesystem behaviour.
*/
-public class
-TemporaryFilesystem {
+public class TemporaryFilesystem {
private final Set temporaryFiles = new CopyOnWriteArraySet<>();
+
private final File baseDir;
// Thread safety reviewed
private final Thread shutdownHook = new Thread(this::deleteTemporaryFiles);
@@ -73,7 +73,7 @@ private TemporaryFilesystem(File baseDir) {
if (!baseDir.exists()) {
throw new UncheckedIOException(
- new IOException("Unable to find tmp dir: " + baseDir.getAbsolutePath()));
+ new IOException("Unable to find tmp dir: " + baseDir.getAbsolutePath()));
}
if (!baseDir.canWrite()) {
throw new UncheckedIOException(
@@ -166,4 +166,8 @@ public boolean deleteBaseDir() {
}
return wasDeleted;
}
+
+ public File getBaseDir() {
+ return baseDir;
+ }
}
diff --git a/java/test/org/openqa/selenium/grid/data/DefaultSlotMatcherTest.java b/java/test/org/openqa/selenium/grid/data/DefaultSlotMatcherTest.java
index 9468df855175d..006f47860596e 100644
--- a/java/test/org/openqa/selenium/grid/data/DefaultSlotMatcherTest.java
+++ b/java/test/org/openqa/selenium/grid/data/DefaultSlotMatcherTest.java
@@ -71,6 +71,43 @@ void matchesBrowser() {
assertThat(slotMatcher.matches(stereotype, capabilities)).isTrue();
}
+ @Test
+ void matchDownloadsForRegularTestMatchingAgainstADownloadAwareNode() {
+ Capabilities stereotype = new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME, "chrome",
+ "se:downloadsEnabled", true
+ );
+ Capabilities capabilities = new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME, "chrome"
+ );
+ assertThat(slotMatcher.matches(stereotype, capabilities)).isTrue();
+ }
+
+ @Test
+ void matchDownloadsForAutoDownloadTestMatchingAgainstADownloadAwareNode() {
+ Capabilities stereotype = new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME, "chrome",
+ "se:downloadsEnabled", true
+ );
+ Capabilities capabilities = new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME, "chrome",
+ "se:downloadsEnabled", true
+ );
+ assertThat(slotMatcher.matches(stereotype, capabilities)).isTrue();
+ }
+
+ @Test
+ void ensureNoMatchFOrDownloadAwareTestMatchingAgainstOrdinaryNode() {
+ Capabilities stereotype = new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME, "chrome"
+ );
+ Capabilities capabilities = new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME, "chrome",
+ "se:downloadsEnabled", true
+ );
+ assertThat(slotMatcher.matches(stereotype, capabilities)).isFalse();
+ }
+
@Test
void matchesEmptyBrowser() {
Capabilities stereotype = new ImmutableCapabilities(
diff --git a/java/test/org/openqa/selenium/grid/gridui/OverallGridTest.java b/java/test/org/openqa/selenium/grid/gridui/OverallGridTest.java
index a0926ac89dc29..41a6a7e4c36a8 100644
--- a/java/test/org/openqa/selenium/grid/gridui/OverallGridTest.java
+++ b/java/test/org/openqa/selenium/grid/gridui/OverallGridTest.java
@@ -126,13 +126,13 @@ private Server> createStandalone() {
private void waitUntilReady(Server> server) {
try (HttpClient client = HttpClient.Factory.createDefault().createClient(server.getUrl())) {
new FluentWait<>(client)
- .withTimeout(Duration.ofSeconds(5))
- .until(
- c -> {
- HttpResponse response = c.execute(new HttpRequest(GET, "/status"));
- Map status = Values.get(response, MAP_TYPE);
- return status != null && Boolean.TRUE.equals(status.get("ready"));
- });
+ .withTimeout(Duration.ofSeconds(5))
+ .until(
+ c -> {
+ HttpResponse response = c.execute(new HttpRequest(GET, "/status"));
+ Map status = Values.get(response, MAP_TYPE);
+ return status != null && Boolean.TRUE.equals(status.get("ready"));
+ });
}
}
diff --git a/java/test/org/openqa/selenium/grid/node/NodeTest.java b/java/test/org/openqa/selenium/grid/node/NodeTest.java
index d51f290540efb..fc5d0feeb6e22 100644
--- a/java/test/org/openqa/selenium/grid/node/NodeTest.java
+++ b/java/test/org/openqa/selenium/grid/node/NodeTest.java
@@ -17,23 +17,9 @@
package org.openqa.selenium.grid.node;
-import static java.time.Duration.ofSeconds;
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.assertj.core.api.InstanceOfAssertFactories.MAP;
-import static org.openqa.selenium.json.Json.MAP_TYPE;
-import static org.openqa.selenium.remote.http.Contents.string;
-import static org.openqa.selenium.remote.http.HttpMethod.GET;
-import static org.openqa.selenium.remote.http.HttpMethod.POST;
-
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -85,17 +71,31 @@
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
+import static java.time.Duration.ofSeconds;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.InstanceOfAssertFactories.MAP;
+import static org.openqa.selenium.json.Json.MAP_TYPE;
+import static org.openqa.selenium.remote.http.Contents.string;
+import static org.openqa.selenium.remote.http.HttpMethod.GET;
+import static org.openqa.selenium.remote.http.HttpMethod.POST;
+
class NodeTest {
private Tracer tracer;
@@ -117,12 +117,16 @@ public void setUp(TestInfo testInfo) throws URISyntaxException {
bus = new GuavaEventBus();
registrationSecret = new Secret("sussex charmer");
+ boolean isDownloadsTestCase = testInfo.getDisplayName().equalsIgnoreCase("DownloadsTestCase");
stereotype = new ImmutableCapabilities("browserName", "cheese");
caps = new ImmutableCapabilities("browserName", "cheese");
+ if (isDownloadsTestCase) {
+ stereotype = new ImmutableCapabilities("browserName", "chrome", "se:downloadsEnabled", true);
+ caps = new ImmutableCapabilities("browserName", "chrome", "se:downloadsEnabled", true);
+ }
uri = new URI("http://localhost:1234");
- File downloadsPath = new File(System.getProperty("java.io.tmpdir"));
class Handler extends Session implements HttpHandler {
private Handler(Capabilities capabilities) {
@@ -135,13 +139,14 @@ public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
}
}
+
Builder builder = LocalNode.builder(tracer, bus, uri, uri, registrationSecret)
.add(caps, new TestSessionFactory((id, c) -> new Handler(c)))
.add(caps, new TestSessionFactory((id, c) -> new Handler(c)))
.add(caps, new TestSessionFactory((id, c) -> new Handler(c)))
.maximumConcurrentSessions(2);
- if (!testInfo.getDisplayName().equalsIgnoreCase("BootWithoutDownloadsDir")) {
- builder = builder.downloadsPath(downloadsPath.getAbsolutePath());
+ if (isDownloadsTestCase) {
+ builder = builder.enableManagedDownloads(true);
}
local = builder.build();
@@ -485,7 +490,7 @@ void canUploadAFile() throws IOException {
req.setContent(() -> new ByteArrayInputStream(payload.getBytes()));
node.execute(req);
- File baseDir = getTemporaryFilesystemBaseDir(local.getTemporaryFilesystem(session.getId()));
+ File baseDir = getTemporaryFilesystemBaseDir(local.getUploadsFilesystem(session.getId()));
assertThat(baseDir.listFiles()).hasSize(1);
File uploadDir = baseDir.listFiles()[0];
assertThat(uploadDir.listFiles()).hasSize(1);
@@ -496,6 +501,7 @@ void canUploadAFile() throws IOException {
}
@Test
+ @DisplayName("DownloadsTestCase")
void canDownloadAFile() throws IOException {
Either response =
node.newSession(createSessionRequest(caps));
@@ -504,108 +510,181 @@ void canDownloadAFile() throws IOException {
String hello = "Hello, world!";
HttpRequest req = new HttpRequest(POST, String.format("/session/%s/se/files", session.getId()));
- File zip = createTmpFile(hello);
- String payload = new Json().toJson(Collections.singletonMap("name", zip.getName()));
+
+ //Let's simulate as if we downloaded a file via a test case
+ String zip = simulateFileDownload(session.getId(),hello);
+
+ String payload = new Json().toJson(Collections.singletonMap("name", zip));
req.setContent(() -> new ByteArrayInputStream(payload.getBytes()));
HttpResponse rsp = node.execute(req);
Map raw = new Json().toType(string(rsp), Json.MAP_TYPE);
- node.stop(session.getId());
- assertThat(raw).isNotNull();
- File baseDir = getTemporaryFilesystemBaseDir(TemporaryFilesystem.getDefaultTmpFS());
- Map map = Optional.ofNullable(
- raw.get("value")
- ).map(data -> (Map) data)
- .orElseThrow(() -> new IllegalStateException("Could not find value attribute"));
- String encodedContents = map.get("contents").toString();
- Zip.unzip(encodedContents, baseDir);
- Path path = new File(baseDir.getAbsolutePath() + "/" + map.get("filename")).toPath();
- String decodedContents = String.join("", Files.readAllLines(path));
- assertThat(decodedContents).isEqualTo(hello);
+ try {
+ assertThat(raw).isNotNull();
+ File baseDir = getTemporaryFilesystemBaseDir(TemporaryFilesystem.getDefaultTmpFS());
+ Map map = Optional.ofNullable(
+ raw.get("value")
+ ).map(data -> (Map) data)
+ .orElseThrow(() -> new IllegalStateException("Could not find value attribute"));
+ String encodedContents = map.get("contents").toString();
+ Zip.unzip(encodedContents, baseDir);
+ Path path = new File(baseDir.getAbsolutePath() + "/" + map.get("filename")).toPath();
+ String decodedContents = String.join("", Files.readAllLines(path));
+ assertThat(decodedContents).isEqualTo(hello);
+ } finally {
+ node.stop(session.getId());
+ }
+ }
+
+ @Test
+ @DisplayName("DownloadsTestCase")
+ void canDownloadMultipleFile() throws IOException {
+ Either response =
+ node.newSession(createSessionRequest(caps));
+ assertThatEither(response).isRight();
+ Session session = response.right().getSession();
+ String hello = "Hello, world!";
+
+ HttpRequest req = new HttpRequest(POST, String.format("/session/%s/se/files", session.getId()));
+
+ //Let's simulate as if we downloaded a file via a test case
+ String zip = simulateFileDownload(session.getId(), hello);
+
+ // This file we are going to leave in the downloads directory of the session
+ // Just to check if we can clean up all the files for the session
+ simulateFileDownload(session.getId(),"Goodbye, world!");
+
+ String payload = new Json().toJson(Collections.singletonMap("name", zip));
+ req.setContent(() -> new ByteArrayInputStream(payload.getBytes()));
+ HttpResponse rsp = node.execute(req);
+ Map raw = new Json().toType(string(rsp), Json.MAP_TYPE);
+ try {
+ assertThat(raw).isNotNull();
+ File baseDir = getTemporaryFilesystemBaseDir(TemporaryFilesystem.getDefaultTmpFS());
+ Map map = Optional.ofNullable(
+ raw.get("value")
+ ).map(data -> (Map) data)
+ .orElseThrow(() -> new IllegalStateException("Could not find value attribute"));
+ String encodedContents = map.get("contents").toString();
+ Zip.unzip(encodedContents, baseDir);
+ Path path = new File(baseDir.getAbsolutePath() + "/" + map.get("filename")).toPath();
+ String decodedContents = String.join("", Files.readAllLines(path));
+ assertThat(decodedContents).isEqualTo(hello);
+ } finally {
+ UUID downloadsId = local.getDownloadsIdForSession(session.getId());
+ File someDir = getTemporaryFilesystemBaseDir(local.getDownloadsFilesystem(downloadsId));
+ node.stop(session.getId());
+ assertThat(someDir).doesNotExist();
+ }
}
@Test
- void canListFilesToDownload() {
+ @DisplayName("DownloadsTestCase")
+ void canListFilesToDownload() throws IOException {
Either response =
node.newSession(createSessionRequest(caps));
assertThatEither(response).isRight();
Session session = response.right().getSession();
String hello = "Hello, world!";
- File zip = createTmpFile(hello);
+ String zip = simulateFileDownload(session.getId(), hello);
HttpRequest req = new HttpRequest(GET, String.format("/session/%s/se/files", session.getId()));
HttpResponse rsp = node.execute(req);
Map raw = new Json().toType(string(rsp), Json.MAP_TYPE);
- node.stop(session.getId());
- assertThat(raw).isNotNull();
- Map map = Optional.ofNullable(
- raw.get("value")
- ).map(data -> (Map) data)
- .orElseThrow(() -> new IllegalStateException("Could not find value attribute"));
- List files = (List) map.get("names");
- assertThat(files).contains(zip.getName());
+ try {
+ assertThat(raw).isNotNull();
+ Map map = Optional.ofNullable(
+ raw.get("value")
+ ).map(data -> (Map) data)
+ .orElseThrow(() -> new IllegalStateException("Could not find value attribute"));
+ List files = (List) map.get("names");
+ assertThat(files).contains(zip);
+ } finally {
+ node.stop(session.getId());
+ }
}
@Test
- @DisplayName("BootWithoutDownloadsDir")
void canDownloadFileThrowsErrorMsgWhenDownloadsDirNotSpecified() {
Either response =
node.newSession(createSessionRequest(caps));
assertThatEither(response).isRight();
Session session = response.right().getSession();
- createTmpFile("Hello, world!");
-
- HttpRequest req = new HttpRequest(POST, String.format("/session/%s/se/files", session.getId()));
- String msg = "Please specify the path wherein the files downloaded using the browser "
- + "would be available via the command line arg [--downloads-path] and restart the node";
- assertThatThrownBy(() -> node.execute(req))
- .hasMessageContaining(msg);
+ try {
+ createTmpFile("Hello, world!");
+ HttpRequest req = new HttpRequest(POST,
+ String.format("/session/%s/se/files", session.getId()));
+ String msg = "Please enable management of downloads via the command line arg "
+ + "[--enable-managed-downloads] and restart the node";
+ assertThatThrownBy(() -> node.execute(req))
+ .hasMessageContaining(msg);
+ } finally {
+ node.stop(session.getId());
+ }
}
@Test
+ @DisplayName("DownloadsTestCase")
void canDownloadFileThrowsErrorMsgWhenPayloadIsMissing() {
Either response =
node.newSession(createSessionRequest(caps));
assertThatEither(response).isRight();
Session session = response.right().getSession();
- createTmpFile("Hello, world!");
-
- HttpRequest req = new HttpRequest(POST, String.format("/session/%s/se/files", session.getId()));
- String msg = "Please specify file to download in payload as {\"name\": \"fileToDownload\"}";
- assertThatThrownBy(() -> node.execute(req))
- .hasMessageContaining(msg);
+ try {
+ createTmpFile("Hello, world!");
+
+ HttpRequest req = new HttpRequest(POST,
+ String.format("/session/%s/se/files", session.getId()));
+ String msg = "Please specify file to download in payload as {\"name\": \"fileToDownload\"}";
+ assertThatThrownBy(() -> node.execute(req))
+ .hasMessageContaining(msg);
+ } finally {
+ node.stop(session.getId());
+ }
}
@Test
+ @DisplayName("DownloadsTestCase")
void canDownloadFileThrowsErrorMsgWhenWrongPayloadIsGiven() {
Either response =
node.newSession(createSessionRequest(caps));
assertThatEither(response).isRight();
Session session = response.right().getSession();
- createTmpFile("Hello, world!");
-
- HttpRequest req = new HttpRequest(POST, String.format("/session/%s/se/files", session.getId()));
- String payload = new Json().toJson(Collections.singletonMap("my-file", "README.md"));
- req.setContent(() -> new ByteArrayInputStream(payload.getBytes()));
-
- String msg = "Please specify file to download in payload as {\"name\": \"fileToDownload\"}";
- assertThatThrownBy(() -> node.execute(req))
- .hasMessageContaining(msg);
+ try {
+ createTmpFile("Hello, world!");
+
+ HttpRequest req = new HttpRequest(POST,
+ String.format("/session/%s/se/files", session.getId()));
+ String payload = new Json().toJson(Collections.singletonMap("my-file", "README.md"));
+ req.setContent(() -> new ByteArrayInputStream(payload.getBytes()));
+
+ String msg = "Please specify file to download in payload as {\"name\": \"fileToDownload\"}";
+ assertThatThrownBy(() -> node.execute(req))
+ .hasMessageContaining(msg);
+ } finally {
+ node.stop(session.getId());
+ }
}
@Test
+ @DisplayName("DownloadsTestCase")
void canDownloadFileThrowsErrorMsgWhenFileNotFound() {
Either response =
node.newSession(createSessionRequest(caps));
assertThatEither(response).isRight();
Session session = response.right().getSession();
- createTmpFile("Hello, world!");
-
- HttpRequest req = new HttpRequest(POST, String.format("/session/%s/se/files", session.getId()));
- String payload = new Json().toJson(Collections.singletonMap("name", "README.md"));
- req.setContent(() -> new ByteArrayInputStream(payload.getBytes()));
-
- String msg = "Cannot find file [README.md] in directory";
- assertThatThrownBy(() -> node.execute(req))
- .hasMessageContaining(msg);
+ try {
+ createTmpFile("Hello, world!");
+
+ HttpRequest req = new HttpRequest(POST,
+ String.format("/session/%s/se/files", session.getId()));
+ String payload = new Json().toJson(Collections.singletonMap("name", "README.md"));
+ req.setContent(() -> new ByteArrayInputStream(payload.getBytes()));
+
+ String msg = "Cannot find file [README.md] in directory";
+ assertThatThrownBy(() -> node.execute(req))
+ .hasMessageContaining(msg);
+ } finally {
+ node.stop(session.getId());
+ }
}
@Test
@@ -708,6 +787,7 @@ private File createFile(String content, File directory) {
}
}
+
private File createTmpFile(String content) {
try {
File f = File.createTempFile("webdriver", "tmp");
@@ -728,9 +808,23 @@ private File getTemporaryFilesystemBaseDir(TemporaryFilesystem tempFS) {
private CreateSessionRequest createSessionRequest(Capabilities caps) {
return new CreateSessionRequest(
- ImmutableSet.copyOf(Dialect.values()),
- caps,
- ImmutableMap.of());
+ ImmutableSet.copyOf(Dialect.values()),
+ caps,
+ ImmutableMap.of());
+ }
+
+ private String simulateFileDownload(SessionId id, String text) throws IOException {
+ File zip = createTmpFile(text);
+ UUID downloadsId = local.getDownloadsIdForSession(id);
+ File someDir = getTemporaryFilesystemBaseDir(local.getDownloadsFilesystem(downloadsId));
+ File downloadsDirectory = Optional.ofNullable(someDir.listFiles()).orElse(new File[]{})[0];
+ File target = new File(downloadsDirectory, zip.getName());
+ boolean renamed = zip.renameTo(target);
+ if (!renamed) {
+ throw new IllegalStateException("Could not move " + zip.getName() + " to directory " +
+ target.getParentFile().getAbsolutePath());
+ }
+ return zip.getName();
}
private static class MyClock extends Clock {
diff --git a/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java b/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java
index 32ff26648b4ea..abcaf4438132f 100644
--- a/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java
+++ b/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java
@@ -27,9 +27,12 @@
import org.openqa.selenium.Platform;
import org.openqa.selenium.SessionNotCreatedException;
import org.openqa.selenium.WebDriverException;
+import org.openqa.selenium.WebDriverInfo;
import org.openqa.selenium.chrome.ChromeDriverInfo;
import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.edge.EdgeDriverInfo;
import org.openqa.selenium.firefox.FirefoxOptions;
+import org.openqa.selenium.firefox.GeckoDriverInfo;
import org.openqa.selenium.grid.config.Config;
import org.openqa.selenium.grid.config.ConfigException;
import org.openqa.selenium.grid.config.MapConfig;
@@ -37,9 +40,11 @@
import org.openqa.selenium.grid.data.CreateSessionRequest;
import org.openqa.selenium.grid.node.ActiveSession;
import org.openqa.selenium.grid.node.SessionFactory;
+import org.openqa.selenium.ie.InternetExplorerDriverInfo;
import org.openqa.selenium.internal.Either;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.net.NetworkUtils;
+import org.openqa.selenium.safari.SafariDriverInfo;
import java.io.StringReader;
import java.net.URI;
@@ -87,6 +92,62 @@ void canConfigureNodeWithDriverDetection() {
.orElseThrow(() -> new AssertionError("Unable to find Chrome info"));
}
+ @Test
+ void ensureAutoDownloadsFlagIsAutoInjectedIntoChromeStereoCapabilitiesWhenEnabledForNode() {
+ boolean isEnabled = isDownloadEnabled(new ChromeDriverInfo(), "ChromeDriverInfo");
+ assertThat(isEnabled).isTrue();
+ }
+
+ @Test
+ void ensureAutoDownloadsFlagIsAutoInjectedIntoFirefoxStereoCapabilitiesWhenEnabledForNode() {
+ boolean isEnabled = isDownloadEnabled(new GeckoDriverInfo(), "GeckoDriverInfo");
+ assertThat(isEnabled).isTrue();
+ }
+
+ @Test
+ void ensureAutoDownloadsFlagIsAutoInjectedIntoEdgeStereoCapabilitiesWhenEnabledForNode() {
+ assumeTrue(Platform.getCurrent().is(Platform.WINDOWS));
+ boolean isEnabled = isDownloadEnabled(new EdgeDriverInfo(), "EdgeDriverInfo");
+ assertThat(isEnabled).isTrue();
+ }
+
+ @Test
+ void ensureAutoDownloadsFlagIsNOTAutoInjectedIntoIEStereoCapabilitiesWhenEnabledForNode() {
+ assumeTrue(Platform.getCurrent().is(Platform.WINDOWS));
+ assumeFalse(Boolean.parseBoolean(System.getenv("GITHUB_ACTIONS")),
+ "We don't have driver servers in PATH when we run unit tests");
+ boolean isEnabled = isDownloadEnabled(new InternetExplorerDriverInfo(), "InternetExplorerDriverInfo");
+ assertThat(isEnabled).isFalse();
+ }
+
+ @Test
+ void ensureAutoDownloadsFlagIsNOTAutoInjectedIntoSafariStereoCapabilitiesWhenEnabledForNode() {
+ boolean isEnabled = isDownloadEnabled(new SafariDriverInfo(), "SafariDriverInfo");
+ assertThat(isEnabled).isFalse();
+ }
+
+ boolean isDownloadEnabled(WebDriverInfo driver, String customMsg) {
+ assumeTrue(driver.isAvailable(), customMsg + " needs to be available");
+ Config config = new MapConfig(singletonMap("node", ImmutableMap.of("detect-drivers", "true",
+ "selenium-manager", false,
+ "enable-manage-downloads", true)));
+ List reported = new ArrayList<>();
+ new NodeOptions(config).getSessionFactories(caps -> {
+ reported.add(caps);
+ return Collections.singleton(HelperFactory.create(config, caps));
+ });
+ String expected = driver.getDisplayName();
+
+ Capabilities found = reported.stream()
+ .filter(driver::isSupporting)
+ .filter(caps -> expected.equalsIgnoreCase(caps.getBrowserName()))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("Unable to find " + customMsg + " info"));
+ return Optional.ofNullable(found.getCapability("se:downloadsEnabled"))
+ .map(value -> Boolean.parseBoolean(value.toString()))
+ .orElse(Boolean.FALSE);
+ }
+
@Test
void shouldDetectCorrectDriversOnWindows() {
assumeTrue(Platform.getCurrent().is(Platform.WINDOWS));
diff --git a/java/test/org/openqa/selenium/grid/router/BUILD.bazel b/java/test/org/openqa/selenium/grid/router/BUILD.bazel
index 33c641069a8b6..900956ec6f62c 100644
--- a/java/test/org/openqa/selenium/grid/router/BUILD.bazel
+++ b/java/test/org/openqa/selenium/grid/router/BUILD.bazel
@@ -6,6 +6,7 @@ LARGE_TESTS = [
"DistributedCdpTest.java",
"NewSessionCreationTest.java",
"RemoteWebDriverBiDiTest.java",
+ "RemoteWebDriverDownloadTest.java",
"StressTest.java",
]
diff --git a/java/test/org/openqa/selenium/grid/router/RemoteWebDriverDownloadTest.java b/java/test/org/openqa/selenium/grid/router/RemoteWebDriverDownloadTest.java
new file mode 100644
index 0000000000000..3eda408ad595b
--- /dev/null
+++ b/java/test/org/openqa/selenium/grid/router/RemoteWebDriverDownloadTest.java
@@ -0,0 +1,160 @@
+// Licensed to the Software Freedom Conservancy (SFC) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The SFC licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.openqa.selenium.grid.router;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Capabilities;
+import org.openqa.selenium.PersistentCapabilities;
+import org.openqa.selenium.environment.webserver.NettyAppServer;
+import org.openqa.selenium.grid.config.TomlConfig;
+import org.openqa.selenium.grid.router.DeploymentTypes.Deployment;
+import org.openqa.selenium.grid.server.Server;
+import org.openqa.selenium.io.Zip;
+import org.openqa.selenium.json.Json;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.remote.SessionId;
+import org.openqa.selenium.remote.http.HttpClient;
+import org.openqa.selenium.remote.http.HttpRequest;
+import org.openqa.selenium.remote.http.HttpResponse;
+import org.openqa.selenium.testing.Ignore;
+import org.openqa.selenium.testing.Safely;
+import org.openqa.selenium.testing.TearDownFixture;
+import org.openqa.selenium.testing.drivers.Browser;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.URL;
+import java.nio.file.Files;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.openqa.selenium.remote.http.Contents.asJson;
+import static org.openqa.selenium.remote.http.Contents.string;
+import static org.openqa.selenium.remote.http.HttpMethod.GET;
+import static org.openqa.selenium.remote.http.HttpMethod.POST;
+import static org.openqa.selenium.testing.drivers.Browser.IE;
+import static org.openqa.selenium.testing.drivers.Browser.SAFARI;
+
+class RemoteWebDriverDownloadTest {
+
+ private Server> server;
+ private NettyAppServer appServer;
+ private Capabilities capabilities;
+ private final List tearDowns = new LinkedList<>();
+ private final ExecutorService executor =
+ Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
+
+
+ @BeforeEach
+ public void setupServers() {
+ Browser browser = Browser.detect();
+ assert browser != null;
+ capabilities = new PersistentCapabilities(browser.getCapabilities())
+ .setCapability("se:downloadsEnabled", true);
+
+ Deployment deployment = DeploymentTypes.STANDALONE.start(
+ browser.getCapabilities(),
+ new TomlConfig(new StringReader(
+ "[node]\n" +
+ "selenium-manager = true\n" +
+ "enable-managed-downloads = true\n" +
+ "driver-implementation = " + browser.displayName())));
+ tearDowns.add(deployment);
+
+ server = deployment.getServer();
+ appServer = new NettyAppServer();
+ tearDowns.add(() -> appServer.stop());
+ appServer.start();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ tearDowns.parallelStream().forEach(Safely::safelyCall);
+ executor.shutdownNow();
+ }
+
+ @Test
+ @Ignore(IE)
+ @Ignore(SAFARI)
+ void testCanListDownloadedFiles() throws InterruptedException {
+ URL gridUrl = server.getUrl();
+ RemoteWebDriver driver = new RemoteWebDriver(gridUrl, capabilities);
+ driver.get(appServer.whereIs("downloads/download.html"));
+ driver.findElement(By.id("file-1")).click();
+ driver.findElement(By.id("file-2")).click();
+ SessionId sessionId = driver.getSessionId();
+
+ // Waiting for the file to be remotely downloaded
+ TimeUnit.SECONDS.sleep(3);
+
+ HttpRequest request = new HttpRequest(GET, String.format("/session/%s/se/files", sessionId));
+ try (HttpClient client = HttpClient.Factory.createDefault().createClient(gridUrl)) {
+ HttpResponse response = client.execute(request);
+ Map jsonResponse = new Json().toType(string(response), Json.MAP_TYPE);
+ @SuppressWarnings("unchecked")
+ Map value = (Map) jsonResponse.get("value");
+ @SuppressWarnings("unchecked")
+ List names = (List) value.get("names");
+ assertThat(names).contains("file_1.txt", "file_2.jpg");
+ } finally {
+ driver.quit();
+ }
+ }
+
+ @Test
+ @Ignore(IE)
+ @Ignore(SAFARI)
+ void testCanDownloadFiles() throws InterruptedException, IOException {
+ URL gridUrl = server.getUrl();
+ RemoteWebDriver driver = new RemoteWebDriver(gridUrl, capabilities);
+ driver.get(appServer.whereIs("downloads/download.html"));
+ driver.findElement(By.id("file-1")).click();
+ SessionId sessionId = driver.getSessionId();
+
+ // Waiting for the file to be remotely downloaded
+ TimeUnit.SECONDS.sleep(3);
+
+ HttpRequest request = new HttpRequest(POST, String.format("/session/%s/se/files", sessionId));
+ request.setContent(asJson(ImmutableMap.of("name", "file_1.txt")));
+ try (HttpClient client = HttpClient.Factory.createDefault().createClient(gridUrl)) {
+ HttpResponse response = client.execute(request);
+ Map jsonResponse = new Json().toType(string(response), Json.MAP_TYPE);
+ @SuppressWarnings("unchecked")
+ Map value = (Map) jsonResponse.get("value");
+ String zippedContents = value.get("contents").toString();
+ File downloadDir = Zip.unzipToTempDir(zippedContents, "download", "");
+ File downloadedFile = Optional.ofNullable(downloadDir.listFiles()).orElse(new File[]{})[0];
+ String fileContent = String.join("", Files.readAllLines(downloadedFile.toPath()));
+ assertThat(fileContent).isEqualTo("Hello, World!");
+ } finally {
+ driver.quit();
+ }
+ }
+}