diff --git a/docs/modules/plugins/pages/plugin-web-app.adoc b/docs/modules/plugins/pages/plugin-web-app.adoc
index 099c6a5ab5..6dd2bcbff2 100644
--- a/docs/modules/plugins/pages/plugin-web-app.adoc
+++ b/docs/modules/plugins/pages/plugin-web-app.adoc
@@ -1669,6 +1669,43 @@ Then value `$value` is selected in slider located by `$locator`
Then value `50` is selected in slider located by `id(test_slider)`
----
+=== Download file from browser downloads page
+
+Downloads the file from the browser downloads page and saves its content to the specified variable.
+
+IMPORTANT: The step is only supported by Chrome browser.
+
+Actions performed at this step:
+
+1. Switch the browser to the download manager page.
+2. Search for the latest downloaded file with a name that matches the provided regex.
+3. Wait until the file download is complete.
+4. Store file content into a variable with the specific scope.
+5. Close the download manager page and return to the previously opened page.
+
+[source,gherkin]
+----
+When I download file with name matching `$regex` from browser downloads and save its content to $scopes variable `$variableName`
+----
+
+* `$regex` - regular expression to filter downloaded files by name
+* `$scopes` - the set (comma separated list of scopes e.g.: STORY, NEXT_BATCHES) of the variable scopes. Allowed values are STEP, SCENARIO, STORY and NEXT_BATCHES
+* `$variableName` - the variable name to save file content
+
+Timeout for file downloading is configurable via properties. The default timeout is 3 minutes.
+
+[source, properties]
+----
+ui.chrome.file-download-timeout=PT3M
+----
+
+.Save data of the previously downloaded file into property
+[source,gherkin]
+----
+When I download file with name matching `report` from browser downloads and save its content to SCENARIO variable `testReport`
+----
+
+
=== Mobile Emulation
==== Emulate mobile device
diff --git a/vividus-plugin-web-app/src/main/java/org/vividus/steps/ui/web/ChromeExperimentalSteps.java b/vividus-plugin-web-app/src/main/java/org/vividus/steps/ui/web/ChromeExperimentalSteps.java
new file mode 100644
index 0000000000..76a39e6e9f
--- /dev/null
+++ b/vividus-plugin-web-app/src/main/java/org/vividus/steps/ui/web/ChromeExperimentalSteps.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2019-2024 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.vividus.steps.ui.web;
+
+import java.time.Duration;
+import java.util.Base64;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.jbehave.core.annotations.When;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.Browser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.vividus.context.VariableContext;
+import org.vividus.selenium.manager.IWebDriverManager;
+import org.vividus.softassert.ISoftAssert;
+import org.vividus.ui.action.IWaitActions;
+import org.vividus.ui.monitor.TakeScreenshotOnFailure;
+import org.vividus.ui.web.action.WebJavascriptActions;
+import org.vividus.variable.VariableScope;
+
+@TakeScreenshotOnFailure
+public class ChromeExperimentalSteps
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(ChromeExperimentalSteps.class);
+
+ private static final String CHROME_DOWNLOADS_PAGE = "chrome://downloads";
+
+ private WebJavascriptActions javascriptActions;
+ private IWebDriverManager webDriverManager;
+ private ISoftAssert softAssert;
+ private VariableContext variableContext;
+ private IWaitActions waitActions;
+ private PageSteps pageSteps;
+ private WindowSteps windowSteps;
+
+ private Duration fileDownloadTimeout;
+
+ /**
+ * Warning! This step can be used only for desktop/chrome profile.
+ * Downloads the file from the website and saves its content to the specified variable.
+ *
+ * Actions performed at this step:
+ *
+ *
Switch the browser to the download manager page;
+ *
Search for the latest downloaded file with a name that matches the provided regex;
+ *
Wait until the file download is complete;
+ *
Store file content into a variable with the specific scope;
+ *
Close the download manager page and return to the previously opened page;
+ *
+ * @param regex Regular expression to filter downloaded files by name.
+ * @param scopes The set (comma separated list of scopes e.g.: STORY, NEXT_BATCHES) of the variable
+ * scopes.
+ * Available scopes:
+ *
+ *
STEP - the variable will be available only within the step,
+ *
SCENARIO - the variable will be available only within the scenario,
+ *
STORY - the variable will be available within the whole story,
+ *
NEXT_BATCHES - the variable will be available starting from next batch
+ *
+ * @param variableName The variable name to store the path to the temporary file with the file content.
+ */
+ @When("I download file with name matching `$regex` from browser downloads"
+ + " and save its content to $scopes variable `$variableName`")
+ public void downloadFile(String regex, Set scopes, String variableName)
+ {
+ Validate.isTrue(webDriverManager.isBrowserAnyOf(Browser.CHROME),
+ "The step is supported only on Chrome browser.");
+ pageSteps.openPageInNewTab(CHROME_DOWNLOADS_PAGE);
+ try
+ {
+ Optional filePathOpt = searchForFile(regex);
+ if (filePathOpt.isEmpty())
+ {
+ List downloadedFiles = javascriptActions
+ .executeScriptFromResource(ChromeExperimentalSteps.class, "chrome-download-files-list.js");
+ String errorMessage = downloadedFiles.isEmpty() ? "There are no files on the browser downloads page"
+ : "Unable to find any file matching regex [" + regex
+ + "] among files on the browser downloads page: " + String.join(", ", downloadedFiles);
+ softAssert.recordFailedAssertion(errorMessage);
+ return;
+ }
+ String filePath = filePathOpt.get();
+ LOGGER.atInfo().addArgument(() -> FilenameUtils.getName(filePath))
+ .log("Waiting for the {} file to download");
+ if (!isFileDownloadComplete(filePath))
+ {
+ return;
+ }
+ LOGGER.info("Download for the {} file is completed", filePath);
+ String content = getFileContent(filePath);
+ variableContext.putVariable(scopes, variableName, Base64.getDecoder().decode(content));
+ }
+ finally
+ {
+ windowSteps.closeCurrentTab();
+ }
+ }
+
+ private String getFileContent(String filePath)
+ {
+ WebElement input = (WebElement) javascriptActions.executeScriptFromResource(ChromeExperimentalSteps.class,
+ "chrome-download-create-input.js");
+ input.sendKeys(filePath);
+
+ String content = javascriptActions.executeAsyncScriptFromResource(ChromeExperimentalSteps.class,
+ "chrome-download-get-file-content.js", input);
+ return StringUtils.substringAfter(content, "base64,");
+ }
+
+ private boolean isFileDownloadComplete(String filePath)
+ {
+ return waitActions.wait(javascriptActions, fileDownloadTimeout,
+ js -> js.executeScriptFromResource(ChromeExperimentalSteps.class, "chrome-download-wait.js", filePath),
+ true).isWaitPassed();
+ }
+
+ private Optional searchForFile(String fileNameRegex)
+ {
+ String filePath = javascriptActions.executeScriptFromResource(ChromeExperimentalSteps.class,
+ "chrome-download-search-file.js", fileNameRegex);
+ return Optional.ofNullable(filePath);
+ }
+
+ public void setJavascriptActions(WebJavascriptActions javascriptActions)
+ {
+ this.javascriptActions = javascriptActions;
+ }
+
+ public void setSoftAssert(ISoftAssert softAssert)
+ {
+ this.softAssert = softAssert;
+ }
+
+ public void setWebDriverManager(IWebDriverManager webDriverManager)
+ {
+ this.webDriverManager = webDriverManager;
+ }
+
+ public void setWaitActions(IWaitActions waitActions)
+ {
+ this.waitActions = waitActions;
+ }
+
+ public void setVariableContext(VariableContext variableContext)
+ {
+ this.variableContext = variableContext;
+ }
+
+ public void setPageSteps(PageSteps pageSteps)
+ {
+ this.pageSteps = pageSteps;
+ }
+
+ public void setFileDownloadTimeout(Duration fileDownloadTimeout)
+ {
+ this.fileDownloadTimeout = fileDownloadTimeout;
+ }
+
+ public void setWindowSteps(WindowSteps windowSteps)
+ {
+ this.windowSteps = windowSteps;
+ }
+}
diff --git a/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-create-input.js b/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-create-input.js
new file mode 100644
index 0000000000..7625ee2881
--- /dev/null
+++ b/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-create-input.js
@@ -0,0 +1,6 @@
+var input = window.document.createElement('INPUT');
+input.setAttribute('type', 'file');
+input.setAttribute('id', 'downloadedFileContent');
+input.hidden = true;
+input.onchange = function (e) { e.stopPropagation() };
+return window.document.documentElement.appendChild(input);
diff --git a/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-files-list.js b/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-files-list.js
new file mode 100644
index 0000000000..fdf0873bda
--- /dev/null
+++ b/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-files-list.js
@@ -0,0 +1,5 @@
+return document.querySelector('downloads-manager')
+ .shadowRoot
+ .querySelector('#downloadsList')
+ .items
+ .map(e => e.fileName)
diff --git a/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-get-file-content.js b/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-get-file-content.js
new file mode 100644
index 0000000000..1f5e5294bc
--- /dev/null
+++ b/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-get-file-content.js
@@ -0,0 +1,6 @@
+var input = arguments[0], callback = arguments[1];
+var reader = new FileReader();
+reader.onload = function (ev) { callback(reader.result) };
+reader.onerror = function (ex) { callback(ex.message) };
+reader.readAsDataURL(input.files[0]);
+input.remove();
diff --git a/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-search-file.js b/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-search-file.js
new file mode 100644
index 0000000000..4c7a157eae
--- /dev/null
+++ b/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-search-file.js
@@ -0,0 +1,7 @@
+return document.querySelector('downloads-manager')
+ .shadowRoot
+ .querySelector('#downloadsList')
+ .items
+ .filter(e => e.fileName.match(arguments[0]))
+ .sort((a, b) => b.started - a.started)
+ .map(e => e.filePath)[0]
diff --git a/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-wait.js b/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-wait.js
new file mode 100644
index 0000000000..3a2fecb255
--- /dev/null
+++ b/vividus-plugin-web-app/src/main/resources/org/vividus/steps/ui/web/chrome-download-wait.js
@@ -0,0 +1,6 @@
+return document.querySelector('downloads-manager')
+ .shadowRoot
+ .querySelector('#downloadsList')
+ .items
+ .find(e => e.filePath === arguments[0])
+ .state === "COMPLETE"
diff --git a/vividus-plugin-web-app/src/main/resources/properties/profile/web/desktop/chrome/profile.properties b/vividus-plugin-web-app/src/main/resources/properties/profile/web/desktop/chrome/profile.properties
index 732cf76285..c5ac76b8aa 100644
--- a/vividus-plugin-web-app/src/main/resources/properties/profile/web/desktop/chrome/profile.properties
+++ b/vividus-plugin-web-app/src/main/resources/properties/profile/web/desktop/chrome/profile.properties
@@ -1,3 +1,5 @@
selenium.browser=chrome
selenium.grid.screen-resolution=1920x1080
+
+ui.chrome.file-download-timeout=PT3M
diff --git a/vividus-plugin-web-app/src/main/resources/vividus-plugin/spring.xml b/vividus-plugin-web-app/src/main/resources/vividus-plugin/spring.xml
index e2f6e26e4e..3bf37e12a1 100644
--- a/vividus-plugin-web-app/src/main/resources/vividus-plugin/spring.xml
+++ b/vividus-plugin-web-app/src/main/resources/vividus-plugin/spring.xml
@@ -214,6 +214,10 @@
+
+
+
+
diff --git a/vividus-plugin-web-app/src/test/java/org/vividus/steps/ui/web/ChromeExperimentalStepsTests.java b/vividus-plugin-web-app/src/test/java/org/vividus/steps/ui/web/ChromeExperimentalStepsTests.java
new file mode 100644
index 0000000000..cc44e81741
--- /dev/null
+++ b/vividus-plugin-web-app/src/test/java/org/vividus/steps/ui/web/ChromeExperimentalStepsTests.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2019-2024 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.vividus.steps.ui.web;
+
+import static com.github.valfirst.slf4jtest.LoggingEvent.info;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.time.Duration;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import com.github.valfirst.slf4jtest.LoggingEvent;
+import com.github.valfirst.slf4jtest.TestLogger;
+import com.github.valfirst.slf4jtest.TestLoggerFactory;
+import com.github.valfirst.slf4jtest.TestLoggerFactoryExtension;
+
+import org.hamcrest.MatcherAssert;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.Browser;
+import org.vividus.context.VariableContext;
+import org.vividus.selenium.manager.IWebDriverManager;
+import org.vividus.softassert.ISoftAssert;
+import org.vividus.ui.action.IWaitActions;
+import org.vividus.ui.action.WaitResult;
+import org.vividus.ui.web.action.WebJavascriptActions;
+import org.vividus.variable.VariableScope;
+
+@ExtendWith({ MockitoExtension.class, TestLoggerFactoryExtension.class })
+public class ChromeExperimentalStepsTests
+{
+ private static final String WAITING_FOR_DOWNLOADING_LOG_MSG = "Waiting for the {} file to download";
+ private static final String CHROME_DOWNLOADS_PAGE = "chrome://downloads";
+ private static final String DEFAULT_FILENAME_REGEX = "fileNameRegex";
+ private static final String DEFAULT_FILEPATH = "C:\\user\\fileNameRegex-123.pdf";
+ private static final String DEFAULT_FILE_CONTENT_INNER = "MQ==";
+ private static final String DEFAULT_FILE_CONTENT = "data:@file/octet-stream;base64," + DEFAULT_FILE_CONTENT_INNER;
+ private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(5);
+ private static final Set DEFAULT_VARIABLE_SCOPE_SET = Set.of(VariableScope.SCENARIO);
+ private static final String DEFAULT_VARIABLE_NAME = "variable";
+ private static final String CHROME_DOWNLOAD_SEARCH_FILE_JS = "chrome-download-search-file.js";
+ private static final String CHROME_DOWNLOAD_CREATE_INPUT_JS = "chrome-download-create-input.js";
+ private static final String CHROME_DOWNLOAD_GET_FILE_CONTENT_JS = "chrome-download-get-file-content.js";
+ private static final String CHROME_DOWNLOAD_FILES_LIST_JS = "chrome-download-files-list.js";
+
+ private final TestLogger logger = TestLoggerFactory.getTestLogger(ChromeExperimentalSteps.class);
+
+ @Mock private WebJavascriptActions javascriptActions;
+ @Mock private IWebDriverManager webDriverManager;
+ @Mock private ISoftAssert softAssert;
+ @Mock private VariableContext variableContext;
+ @Mock private IWaitActions waitActions;
+ @Mock private PageSteps pageSteps;
+ @Mock private WebElement createdInput;
+ @Mock private WaitResult