From afa611321571d79f858579300218fd8751c18383 Mon Sep 17 00:00:00 2001 From: Aliaksej Mialeshka Date: Wed, 18 Feb 2026 13:22:51 +0100 Subject: [PATCH 1/5] Mobile Emulation settings support for chromium-based browsers +semver:feature --- README.md | 73 +++++++++++++++++ .../driversettings/ChromeSettings.java | 35 +------- .../driversettings/ChromiumSettings.java | 79 +++++++++++++++++++ .../driversettings/DriverSettings.java | 22 ++---- .../driversettings/EdgeSettings.java | 29 +------ 5 files changed, 162 insertions(+), 76 deletions(-) create mode 100644 src/main/java/aquality/selenium/configuration/driversettings/ChromiumSettings.java diff --git a/README.md b/README.md index fc61d18..02e4a5e 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,79 @@ browser.devTools().emulation().setGeolocationOverride(latitude, longitude, accur ``` See more DevTools use cases [here](./src/test/java/tests/usecases/devtools) +It is also possible to set mobile emulation capabilities (for chromium-based browsers) in resources/settings.json file, as well as to configure other arguments and options there: +```json +{ + "browserName": "chrome", + "isRemote": false, + "remoteConnectionUrl": "http://qa-auto-nexus:4444/wd/hub", + "isElementHighlightEnabled": true, + + "driverSettings": { + "chrome": { + "capabilities": { + "selenoid:options": + { + "enableVNC": true + }, + "mobileEmulation": { + "userAgent": "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 5 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19", + "deviceMetrics": { + "width": 660, + "height": 1040, + "pixelRatio": 3.0 + } + }, + "unhandledPromptBehavior": "ignore" + }, + "options": { + "download.prompt_for_download": "false", + "download.default_directory": "./downloads" + }, + "loggingPreferences": { + "Performance": "All" + }, + "excludedArguments": [ "enable-automation" ], + "startArguments": [ "--disable-search-engine-choice-screen" ], + "pageLoadStrategy": "normal" + }, + "safari": { + "options": { + "safari.options.dataDir": "/Users/username/Downloads" + } + } + }, + "timeouts": { + "timeoutImplicit": 0, + "timeoutCondition": 30, + "timeoutScript": 10, + "timeoutPageLoad": 60, + "timeoutPollingInterval": 300, + "timeoutCommand": 60 + }, + "retry": { + "number": 2, + "pollingInterval": 300 + }, + "logger": { + "language": "en", + "logPageSource": true + }, + "elementCache": { + "isEnabled": false + }, + "visualization": { + "imageExtension": "png", + "maxFullFileNameLength": 255, + "defaultThreshold": 0.012, + "comparisonWidth": 16, + "comparisonHeight": 16, + "pathToDumps": "./src/test/resources/visualDumps/" + } +} +``` + + 8. Quit browser at the end ```java browser.quit(); diff --git a/src/main/java/aquality/selenium/configuration/driversettings/ChromeSettings.java b/src/main/java/aquality/selenium/configuration/driversettings/ChromeSettings.java index fafb3c7..765c8a8 100644 --- a/src/main/java/aquality/selenium/configuration/driversettings/ChromeSettings.java +++ b/src/main/java/aquality/selenium/configuration/driversettings/ChromeSettings.java @@ -5,10 +5,7 @@ import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.remote.AbstractDriverOptions; -import java.util.HashMap; -import java.util.Map; - -public class ChromeSettings extends DriverSettings { +public class ChromeSettings extends ChromiumSettings { public ChromeSettings(ISettingsFile settingsFile){ super(settingsFile); @@ -17,39 +14,11 @@ public ChromeSettings(ISettingsFile settingsFile){ @Override public AbstractDriverOptions getDriverOptions() { ChromeOptions chromeOptions = new ChromeOptions(); - setChromePrefs(chromeOptions); - setCapabilities(chromeOptions); - setChromeArgs(chromeOptions); - setExcludedArguments(chromeOptions); - chromeOptions.setPageLoadStrategy(getPageLoadStrategy()); + setupDriverOptions(chromeOptions); setLoggingPreferences(chromeOptions, ChromeOptions.LOGGING_PREFS); return chromeOptions; } - private void setChromePrefs(ChromeOptions options){ - HashMap chromePrefs = new HashMap<>(); - Map configOptions = getBrowserOptions(); - configOptions.forEach((key, value) -> { - if (key.equals(getDownloadDirCapabilityKey())) { - chromePrefs.put(key, getDownloadDir()); - } else { - chromePrefs.put(key, value); - } - }); - options.setExperimentalOption("prefs", chromePrefs); - } - - private void setChromeArgs(ChromeOptions options) { - for (String arg : getBrowserStartArguments()) { - options.addArguments(arg); - } - } - - @Override - public String getDownloadDirCapabilityKey() { - return "download.default_directory"; - } - @Override public BrowserName getBrowserName() { return BrowserName.CHROME; diff --git a/src/main/java/aquality/selenium/configuration/driversettings/ChromiumSettings.java b/src/main/java/aquality/selenium/configuration/driversettings/ChromiumSettings.java new file mode 100644 index 0000000..afb4cd8 --- /dev/null +++ b/src/main/java/aquality/selenium/configuration/driversettings/ChromiumSettings.java @@ -0,0 +1,79 @@ +package aquality.selenium.configuration.driversettings; + +import aquality.selenium.core.utilities.ISettingsFile; +import org.openqa.selenium.MutableCapabilities; +import org.openqa.selenium.chromium.ChromiumOptions; +import org.openqa.selenium.logging.LoggingPreferences; + +import java.util.HashMap; +import java.util.Map; + +public abstract class ChromiumSettings extends DriverSettings { + + private static final String MOBILE_EMULATION_CAPABILITY_KEY = "mobileEmulation"; + private static final String DEVICE_METRICS_CAPABILITY_KEY = "deviceMetrics"; + private static final String CLIENT_HINTS_CAPABILITY_KEY = "clientHints"; + + public ChromiumSettings(ISettingsFile settingsFile){ + super(settingsFile); + } + + protected > void setupDriverOptions(T options) { + setPrefs(options); + setCapabilities(options); + getBrowserStartArguments().forEach(options::addArguments); + setExcludedArguments(options); + options.setPageLoadStrategy(getPageLoadStrategy()); + } + + void setLoggingPreferences(MutableCapabilities options, String capabilityKey) { + if (!getLoggingPreferences().isEmpty()) { + LoggingPreferences logs = new LoggingPreferences(); + getLoggingPreferences().forEach(logs::enable); + options.setCapability(capabilityKey, logs); + } + } + + @Override + void setCapabilities(MutableCapabilities options) { + getBrowserCapabilities().forEach((key, value) -> { + if (key.equals(MOBILE_EMULATION_CAPABILITY_KEY)) { + Map mobileOptions = getMapOrEmpty(getDriverSettingsPath(CapabilityType.CAPABILITIES.getKey(), MOBILE_EMULATION_CAPABILITY_KEY)); + if (mobileOptions.containsKey(DEVICE_METRICS_CAPABILITY_KEY)) { + Map deviceMetrics = getMapOrEmpty(getDriverSettingsPath(CapabilityType.CAPABILITIES.getKey(), MOBILE_EMULATION_CAPABILITY_KEY, DEVICE_METRICS_CAPABILITY_KEY)); + mobileOptions.put(DEVICE_METRICS_CAPABILITY_KEY, deviceMetrics); + } + if (mobileOptions.containsKey(CLIENT_HINTS_CAPABILITY_KEY)) { + Map clientHints = getMapOrEmpty(getDriverSettingsPath(CapabilityType.CAPABILITIES.getKey(), MOBILE_EMULATION_CAPABILITY_KEY, CLIENT_HINTS_CAPABILITY_KEY)); + mobileOptions.put(CLIENT_HINTS_CAPABILITY_KEY, clientHints); + } + ((ChromiumOptions)options).setExperimentalOption(key, mobileOptions); + } else { + options.setCapability(key, value); + } + }); + } + + protected > void setPrefs(T options){ + HashMap prefs = new HashMap<>(); + Map configOptions = getBrowserOptions(); + configOptions.forEach((key, value) -> { + if (key.equals(getDownloadDirCapabilityKey())) { + prefs.put(key, getDownloadDir()); + } else { + prefs.put(key, value); + } + }); + options.setExperimentalOption("prefs", prefs); + } + + + protected > void setExcludedArguments(T chromiumOptions) { + chromiumOptions.setExperimentalOption("excludeSwitches", getExcludedArguments()); + } + + @Override + public String getDownloadDirCapabilityKey() { + return "download.default_directory"; + } +} diff --git a/src/main/java/aquality/selenium/configuration/driversettings/DriverSettings.java b/src/main/java/aquality/selenium/configuration/driversettings/DriverSettings.java index d6403a4..b250d9a 100644 --- a/src/main/java/aquality/selenium/configuration/driversettings/DriverSettings.java +++ b/src/main/java/aquality/selenium/configuration/driversettings/DriverSettings.java @@ -7,8 +7,6 @@ import org.apache.commons.lang3.StringUtils; import org.openqa.selenium.MutableCapabilities; import org.openqa.selenium.PageLoadStrategy; -import org.openqa.selenium.chromium.ChromiumOptions; -import org.openqa.selenium.logging.LoggingPreferences; import java.io.File; import java.io.IOException; @@ -59,9 +57,13 @@ protected Map getLoggingPreferences() { return loggingPreferences; } + protected Map getMapOrEmpty(String path) { + return getSettingsFile().isValuePresent(path) ? getSettingsFile().getMap(path) : Collections.emptyMap(); + } + private Map getMapOrEmpty(CapabilityType capabilityType) { String path = getDriverSettingsPath(capabilityType); - Map map = getSettingsFile().isValuePresent(path) ? getSettingsFile().getMap(path) : Collections.emptyMap(); + Map map = getMapOrEmpty(path); logCollection("loc.browser.".concat(capabilityType.getKey()), map); return map; } @@ -138,18 +140,6 @@ void setCapabilities(MutableCapabilities options) { getBrowserCapabilities().forEach(options::setCapability); } - > void setExcludedArguments(T chromiumOptions) { - chromiumOptions.setExperimentalOption("excludeSwitches", getExcludedArguments()); - } - - void setLoggingPreferences(MutableCapabilities options, String capabilityKey) { - if (!getLoggingPreferences().isEmpty()) { - LoggingPreferences logs = new LoggingPreferences(); - getLoggingPreferences().forEach(logs::enable); - options.setCapability(capabilityKey, logs); - } - } - @Override public String getDownloadDir() { Map browserOptions = getBrowserOptions(); @@ -162,7 +152,7 @@ public String getDownloadDir() { throw new IllegalArgumentException(String.format("failed to find %s profiles option for %s", key, getBrowserName())); } - private enum CapabilityType { + protected enum CapabilityType { CAPABILITIES("capabilities"), OPTIONS("options"), START_ARGS("startArguments"), diff --git a/src/main/java/aquality/selenium/configuration/driversettings/EdgeSettings.java b/src/main/java/aquality/selenium/configuration/driversettings/EdgeSettings.java index 2d299b1..cf3d42f 100644 --- a/src/main/java/aquality/selenium/configuration/driversettings/EdgeSettings.java +++ b/src/main/java/aquality/selenium/configuration/driversettings/EdgeSettings.java @@ -5,10 +5,7 @@ import org.openqa.selenium.edge.EdgeOptions; import org.openqa.selenium.remote.AbstractDriverOptions; -import java.util.HashMap; -import java.util.Map; - -public class EdgeSettings extends DriverSettings { +public class EdgeSettings extends ChromiumSettings { public EdgeSettings(ISettingsFile settingsFile){ super(settingsFile); @@ -17,33 +14,11 @@ public EdgeSettings(ISettingsFile settingsFile){ @Override public AbstractDriverOptions getDriverOptions() { EdgeOptions edgeOptions = new EdgeOptions(); - setCapabilities(edgeOptions); - setPrefs(edgeOptions); - getBrowserStartArguments().forEach(edgeOptions::addArguments); - setExcludedArguments(edgeOptions); - edgeOptions.setPageLoadStrategy(getPageLoadStrategy()); + setupDriverOptions(edgeOptions); setLoggingPreferences(edgeOptions, EdgeOptions.LOGGING_PREFS); return edgeOptions; } - @Override - public String getDownloadDirCapabilityKey() { - return "download.default_directory"; - } - - private void setPrefs(EdgeOptions options){ - HashMap prefs = new HashMap<>(); - Map configOptions = getBrowserOptions(); - configOptions.forEach((key, value) -> { - if (key.equals(getDownloadDirCapabilityKey())) { - prefs.put(key, getDownloadDir()); - } else { - prefs.put(key, value); - } - }); - options.setExperimentalOption("prefs", prefs); - } - @Override public BrowserName getBrowserName() { return BrowserName.EDGE; From 524acbe7aa414eae874ef217e6ef8c5f888061db Mon Sep 17 00:00:00 2001 From: Aliaksej Mialeshka Date: Wed, 18 Feb 2026 13:30:29 +0100 Subject: [PATCH 2/5] Change ChromiumSettings constructor visibility to "protected" --- .../selenium/configuration/driversettings/ChromiumSettings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/aquality/selenium/configuration/driversettings/ChromiumSettings.java b/src/main/java/aquality/selenium/configuration/driversettings/ChromiumSettings.java index afb4cd8..e9c3cb2 100644 --- a/src/main/java/aquality/selenium/configuration/driversettings/ChromiumSettings.java +++ b/src/main/java/aquality/selenium/configuration/driversettings/ChromiumSettings.java @@ -14,7 +14,7 @@ public abstract class ChromiumSettings extends DriverSettings { private static final String DEVICE_METRICS_CAPABILITY_KEY = "deviceMetrics"; private static final String CLIENT_HINTS_CAPABILITY_KEY = "clientHints"; - public ChromiumSettings(ISettingsFile settingsFile){ + protected ChromiumSettings(ISettingsFile settingsFile){ super(settingsFile); } From be0d2d6e529916b682fded140f396d5d6ec97fae Mon Sep 17 00:00:00 2001 From: Aliaksej Mialeshka Date: Wed, 18 Feb 2026 14:49:44 +0100 Subject: [PATCH 3/5] Add clientHints into the README example Add ScreenshotListener for test failure analysis Collect failure artifacts in the pipeline --- README.md | 4 ++ azure-pipelines.yml | 18 +++++++++ .../java/testreport/ScreenshotListener.java | 37 +++++++++++++++++++ src/test/java/tests/BaseTest.java | 3 ++ 4 files changed, 62 insertions(+) create mode 100644 src/test/java/testreport/ScreenshotListener.java diff --git a/README.md b/README.md index 02e4a5e..f778a47 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,10 @@ It is also possible to set mobile emulation capabilities (for chromium-based bro "width": 660, "height": 1040, "pixelRatio": 3.0 + }, + "clientHints": { + "platform": "Android", + "mobile": true } }, "unhandledPromptBehavior": "ignore" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9964784..c447782 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -61,3 +61,21 @@ jobs: mavenAuthenticateFeed: false effectivePomSkip: false sonarQubeRunAnalysis: false + + - task: CopyFiles@2 + displayName: 'Copy failure screenshots and test logs' + inputs: + SourceFolder: '$(Build.SourcesDirectory)/target' + Contents: | + surefire-reports/failure_screenshots/*.png + log/*.log + TargetFolder: '$(Build.ArtifactStagingDirectory)' + condition: succeededOrFailed() + + - task: PublishBuildArtifacts@1 + displayName: 'Publish copied artifacts' + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: 'drop' + publishLocation: 'Container' + condition: succeededOrFailed() diff --git a/src/test/java/testreport/ScreenshotListener.java b/src/test/java/testreport/ScreenshotListener.java new file mode 100644 index 0000000..ea2a35b --- /dev/null +++ b/src/test/java/testreport/ScreenshotListener.java @@ -0,0 +1,37 @@ +package testreport; + +import aquality.selenium.browser.AqualityServices; +import org.apache.logging.log4j.core.util.FileUtils; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.TestListenerAdapter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +public class ScreenshotListener extends TestListenerAdapter { + @Override + public void onTestFailure(ITestResult result) { + Calendar calendar = Calendar.getInstance(); + SimpleDateFormat formatter = new SimpleDateFormat("dd_MM_yyyy_hh_mm_ss"); + String dateString = formatter.format(calendar.getTime()); + String methodName = result.getName(); + if(!result.isSuccess() && AqualityServices.isBrowserStarted()){ + File scrFile = ((TakesScreenshot)AqualityServices.getBrowser().getDriver()).getScreenshotAs(OutputType.FILE); + try { + String reportDirectory = String.format("%s/target/surefire-reports", new File(System.getProperty("user.dir")).getAbsolutePath()); + File destFile = new File(String.format("%s/failure_screenshots/%s_%s.png", reportDirectory, methodName, dateString)); + FileUtils.makeParentDirs(destFile); + Files.copy(scrFile.toPath(), destFile.toPath()); + Reporter.log(String.format(" ", destFile.getAbsolutePath(), destFile.getAbsolutePath())); + } catch (IOException e) { + AqualityServices.getLogger().fatal("An IO exception occurred while tried to save a screenshot", e); + } + } + } +} diff --git a/src/test/java/tests/BaseTest.java b/src/test/java/tests/BaseTest.java index 6463484..21fb7b7 100644 --- a/src/test/java/tests/BaseTest.java +++ b/src/test/java/tests/BaseTest.java @@ -6,12 +6,15 @@ import org.openqa.selenium.Dimension; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import testreport.ScreenshotListener; import theinternet.TheInternetPage; import java.io.IOException; import static utils.FileUtil.getResourceFileByName; +@Listeners(ScreenshotListener.class) public abstract class BaseTest { private static final String DEFAULT_URL = TheInternetPage.LOGIN.getAddress(); From 7af12cc2ca2bbf7f3ca0aba17cb8a29c61a36f51 Mon Sep 17 00:00:00 2001 From: Aliaksej Mialeshka Date: Wed, 18 Feb 2026 15:15:13 +0100 Subject: [PATCH 4/5] Fix coding issues in ScreenshotListener --- src/test/java/testreport/ScreenshotListener.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/test/java/testreport/ScreenshotListener.java b/src/test/java/testreport/ScreenshotListener.java index ea2a35b..0255ed6 100644 --- a/src/test/java/testreport/ScreenshotListener.java +++ b/src/test/java/testreport/ScreenshotListener.java @@ -1,7 +1,6 @@ package testreport; import aquality.selenium.browser.AqualityServices; -import org.apache.logging.log4j.core.util.FileUtils; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.testng.ITestResult; @@ -18,15 +17,15 @@ public class ScreenshotListener extends TestListenerAdapter { @Override public void onTestFailure(ITestResult result) { Calendar calendar = Calendar.getInstance(); - SimpleDateFormat formatter = new SimpleDateFormat("dd_MM_yyyy_hh_mm_ss"); + SimpleDateFormat formatter = new SimpleDateFormat("dd_MM_yyyy_HH_mm_ss"); String dateString = formatter.format(calendar.getTime()); String methodName = result.getName(); - if(!result.isSuccess() && AqualityServices.isBrowserStarted()){ - File scrFile = ((TakesScreenshot)AqualityServices.getBrowser().getDriver()).getScreenshotAs(OutputType.FILE); + if (AqualityServices.isBrowserStarted()) { try { + File scrFile = ((TakesScreenshot) AqualityServices.getBrowser().getDriver()).getScreenshotAs(OutputType.FILE); String reportDirectory = String.format("%s/target/surefire-reports", new File(System.getProperty("user.dir")).getAbsolutePath()); File destFile = new File(String.format("%s/failure_screenshots/%s_%s.png", reportDirectory, methodName, dateString)); - FileUtils.makeParentDirs(destFile); + Files.createDirectories(destFile.getParentFile().toPath()); Files.copy(scrFile.toPath(), destFile.toPath()); Reporter.log(String.format(" ", destFile.getAbsolutePath(), destFile.getAbsolutePath())); } catch (IOException e) { From c5a7edf761d95660a40b79d7de34a2ec98fc913c Mon Sep 17 00:00:00 2001 From: Aliaksej Mialeshka Date: Wed, 18 Feb 2026 15:50:25 +0100 Subject: [PATCH 5/5] Fix coding issues in ScreenshotListener, stabilize geolocation test --- src/test/java/forms/MyLocationForm.java | 3 ++- src/test/java/testreport/ScreenshotListener.java | 8 +++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/test/java/forms/MyLocationForm.java b/src/test/java/forms/MyLocationForm.java index 91e663b..e8ddf2b 100644 --- a/src/test/java/forms/MyLocationForm.java +++ b/src/test/java/forms/MyLocationForm.java @@ -16,9 +16,10 @@ public MyLocationForm() { } public double getLatitude() { - if (!lblLatitude.state().isDisplayed() && btnConsent.state().isDisplayed()) { + if (!lblLatitude.state().isDisplayed() && btnConsent.state().waitForDisplayed()) { clickConsent(); } + lblLatitude.state().waitForDisplayed(); return Double.parseDouble(lblLatitude.getText()); } diff --git a/src/test/java/testreport/ScreenshotListener.java b/src/test/java/testreport/ScreenshotListener.java index 0255ed6..a311fd7 100644 --- a/src/test/java/testreport/ScreenshotListener.java +++ b/src/test/java/testreport/ScreenshotListener.java @@ -10,15 +10,13 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.text.SimpleDateFormat; -import java.util.Calendar; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; public class ScreenshotListener extends TestListenerAdapter { @Override public void onTestFailure(ITestResult result) { - Calendar calendar = Calendar.getInstance(); - SimpleDateFormat formatter = new SimpleDateFormat("dd_MM_yyyy_HH_mm_ss"); - String dateString = formatter.format(calendar.getTime()); + String dateString = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd_MM_yyyy_HH_mm_ss")); String methodName = result.getName(); if (AqualityServices.isBrowserStarted()) { try {