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: + *

+ * @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: + * + * @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 waitResult; + @Mock private WindowSteps windowSteps; + + private byte[] decodedContent = Base64.getDecoder().decode(DEFAULT_FILE_CONTENT_INNER); + + @InjectMocks private ChromeExperimentalSteps chromeExperimentalSteps; + + @BeforeEach + void beforeEach() + { + chromeExperimentalSteps.setFileDownloadTimeout(DEFAULT_TIMEOUT); + when(webDriverManager.isBrowserAnyOf(Browser.CHROME)).thenReturn(true); + } + + @Test + void testDownloadFileSuccessfully() + { + when(javascriptActions.executeScriptFromResource(ChromeExperimentalSteps.class, CHROME_DOWNLOAD_SEARCH_FILE_JS, + DEFAULT_FILENAME_REGEX)).thenReturn(DEFAULT_FILEPATH); + + when(waitActions.wait(eq(javascriptActions), eq(DEFAULT_TIMEOUT), Mockito.any(), eq(true))) + .thenReturn(waitResult); + when(waitResult.isWaitPassed()).thenReturn(true); + + when(javascriptActions.executeScriptFromResource(ChromeExperimentalSteps.class, + CHROME_DOWNLOAD_CREATE_INPUT_JS)).thenReturn(createdInput); + + when(javascriptActions.executeAsyncScriptFromResource(ChromeExperimentalSteps.class, + CHROME_DOWNLOAD_GET_FILE_CONTENT_JS, createdInput)).thenReturn(DEFAULT_FILE_CONTENT); + + chromeExperimentalSteps.downloadFile(DEFAULT_FILENAME_REGEX, DEFAULT_VARIABLE_SCOPE_SET, DEFAULT_VARIABLE_NAME); + verify(pageSteps).openPageInNewTab(CHROME_DOWNLOADS_PAGE); + verify(createdInput).sendKeys(DEFAULT_FILEPATH); + verify(variableContext).putVariable(DEFAULT_VARIABLE_SCOPE_SET, DEFAULT_VARIABLE_NAME, decodedContent); + verify(windowSteps).closeCurrentTab(); + + LoggingEvent waitingForDownloadLoggingEvent = info(WAITING_FOR_DOWNLOADING_LOG_MSG, "fileNameRegex-123.pdf"); + LoggingEvent downloadCompleteLoggingEvent = info("Download for the {} file is completed", DEFAULT_FILEPATH); + MatcherAssert.assertThat(logger.getLoggingEvents(), + is(List.of(waitingForDownloadLoggingEvent, downloadCompleteLoggingEvent))); + } + + @Test + void testDownloadFileWrongBrowser() + { + when(webDriverManager.isBrowserAnyOf(Browser.CHROME)).thenReturn(false); + + assertThrows( + IllegalArgumentException.class, () -> chromeExperimentalSteps.downloadFile(DEFAULT_FILENAME_REGEX, + DEFAULT_VARIABLE_SCOPE_SET, DEFAULT_VARIABLE_NAME), + "The step is supported only on Chrome browser."); + verifyNoInteractions(pageSteps); + verifyNoInteractions(variableContext); + verifyNoInteractions(pageSteps); + MatcherAssert.assertThat(logger.getLoggingEvents(), is(Collections.emptyList())); + } + + @Test + void testNoFilesFoundByRegex() + { + when(javascriptActions.executeScriptFromResource(ChromeExperimentalSteps.class, CHROME_DOWNLOAD_SEARCH_FILE_JS, + DEFAULT_FILENAME_REGEX)).thenReturn(null); + when(javascriptActions.executeScriptFromResource(ChromeExperimentalSteps.class, + CHROME_DOWNLOAD_FILES_LIST_JS)).thenReturn(List.of("file1", "file2")); + + chromeExperimentalSteps.downloadFile(DEFAULT_FILENAME_REGEX, DEFAULT_VARIABLE_SCOPE_SET, DEFAULT_VARIABLE_NAME); + verify(softAssert).recordFailedAssertion("Unable to find any file matching regex [" + DEFAULT_FILENAME_REGEX + + "] among files on the browser downloads page: file1, file2"); + verify(pageSteps).openPageInNewTab(CHROME_DOWNLOADS_PAGE); + verifyNoInteractions(variableContext); + verify(windowSteps).closeCurrentTab(); + + MatcherAssert.assertThat(logger.getLoggingEvents(), is(Collections.emptyList())); + } + + @Test + void testNoFilesFoundByRegexEmptyFilesList() + { + when(javascriptActions.executeScriptFromResource(ChromeExperimentalSteps.class, CHROME_DOWNLOAD_SEARCH_FILE_JS, + DEFAULT_FILENAME_REGEX)).thenReturn(null); + when(javascriptActions.executeScriptFromResource(ChromeExperimentalSteps.class, CHROME_DOWNLOAD_FILES_LIST_JS)) + .thenReturn(Collections.emptyList()); + + chromeExperimentalSteps.downloadFile(DEFAULT_FILENAME_REGEX, DEFAULT_VARIABLE_SCOPE_SET, DEFAULT_VARIABLE_NAME); + verify(softAssert).recordFailedAssertion("There are no files on the browser downloads page"); + verify(pageSteps).openPageInNewTab(CHROME_DOWNLOADS_PAGE); + verifyNoInteractions(variableContext); + verify(windowSteps).closeCurrentTab(); + + MatcherAssert.assertThat(logger.getLoggingEvents(), is(Collections.emptyList())); + } + + @Test + void testDownloadTimeoutError() + { + when(javascriptActions.executeScriptFromResource(ChromeExperimentalSteps.class, CHROME_DOWNLOAD_SEARCH_FILE_JS, + DEFAULT_FILENAME_REGEX)).thenReturn("C:\\user\\fileNameRegex-124.pdf"); + + when(waitActions.wait(eq(javascriptActions), eq(DEFAULT_TIMEOUT), Mockito.any(), eq(true))) + .thenReturn(waitResult); + when(waitResult.isWaitPassed()).thenReturn(false); + + chromeExperimentalSteps.downloadFile(DEFAULT_FILENAME_REGEX, DEFAULT_VARIABLE_SCOPE_SET, DEFAULT_VARIABLE_NAME); + verify(pageSteps).openPageInNewTab(CHROME_DOWNLOADS_PAGE); + verifyNoInteractions(variableContext); + verify(windowSteps).closeCurrentTab(); + + LoggingEvent waitingForDownloadLoggingEvent = info(WAITING_FOR_DOWNLOADING_LOG_MSG, "fileNameRegex-124.pdf"); + MatcherAssert.assertThat(logger.getLoggingEvents(), is(List.of(waitingForDownloadLoggingEvent))); + } +} diff --git a/vividus-tests/src/main/resources/story/integration/ChromeExperimentalSteps.story b/vividus-tests/src/main/resources/story/integration/ChromeExperimentalSteps.story new file mode 100644 index 0000000000..84c4a1b868 --- /dev/null +++ b/vividus-tests/src/main/resources/story/integration/ChromeExperimentalSteps.story @@ -0,0 +1,14 @@ +Description: Integration tests for ChromeExperimentalSteps class. + +Meta: + @epic vividus-plugin-web-app + +Scenario: Download and verify excel file +Given I am on page with URL `${vividus-test-site-url}/download.html` +When I click on element located by `linkText(Download sample Excel doc)` +When I download file with name matching `excel_sample` from browser downloads and save its content to SCENARIO variable `excelFile` +Then `${excelFile}` contains excel sheet with name `Sheet1` and records: +|cellsRange|valueRegex | +|A1 |A | +|B2 |12.0 | +|C3 |23.0 |