Skip to content

Commit 018533d

Browse files
authored
Drive Opera browser with 'chromedriver' (#293)
1 parent 3ebe430 commit 018533d

File tree

7 files changed

+220
-40
lines changed

7 files changed

+220
-40
lines changed

build.gradle

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,10 @@ test {
151151

152152
scmVersion {
153153
hooks {
154-
pre 'fileUpdate', [file: 'README.md', pattern: {v, p -> /(<version>)\d+\.\d+\.\d+(-s[34]<\/version>)/}, replacement: {v, p -> "\$1$v\$2"}]
155-
pre 'fileUpdate', [file: 'README.md', pattern: {v, p -> /(selenium-foundation:)\d+\.\d+\.\d+(-s[34])/}, replacement: {v, p -> "\$1$v\$2"}]
156-
pre 'commit'
157-
post 'push'
154+
pre 'fileUpdate', [file: 'README.md', pattern: {v, p -> /(<version>)\d+\.\d+\.\d+(-s[34]<\/version>)/}, replacement: {v, p -> "\$1$v\$2"}]
155+
pre 'fileUpdate', [file: 'README.md', pattern: {v, p -> /(selenium-foundation:)\d+\.\d+\.\d+(-s[34])/}, replacement: {v, p -> "\$1$v\$2"}]
156+
pre 'commit'
157+
post 'push'
158158
}
159159
}
160160

src/main/java/com/nordstrom/automation/selenium/core/GridUtility.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Objects;
25+
import java.util.Optional;
2526
import java.util.ServiceConfigurationError;
2627
import java.util.ServiceLoader;
2728
import java.util.concurrent.TimeoutException;
@@ -209,6 +210,17 @@ public static String readAvailable(InputStream inputStream) throws IOException {
209210
return result.toString(StandardCharsets.UTF_8.name());
210211
}
211212

213+
/**
214+
* Get the 'driverPath' value from the specified capabilities map.
215+
*
216+
* @param capabilities map of capabilities
217+
* @return 'driverPath' value; {@code null} if no 'driverPath' value is found
218+
*/
219+
public static String getDriverPath(Capabilities capabilities) {
220+
Map<String, Object> options = getNordOptions(capabilities);
221+
return (String) Optional.ofNullable(options.get("driverPath")).orElse(null);
222+
}
223+
212224
/**
213225
* Get the 'personality' value from the specified capabilities map.
214226
* <p>

src/main/java/com/nordstrom/automation/selenium/plugins/ChromeCaps.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ private ChromeCaps() {
3636
/** extension capability name for <b>ChromeOptions</b> */
3737
public static final String OPTIONS_KEY = "goog:chromeOptions";
3838

39-
private static final String[] PROPERTY_NAMES =
39+
static final String[] PROPERTY_NAMES =
4040
{ DRIVER_PATH, BINARY_PATH, LOGFILE_PATH, VERBOSE_LOG, SILENT_MODE, WHITELISTED };
4141

4242
private static final String CAPABILITIES =
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"109": "123",
3+
"110": "124",
4+
"111": "125",
5+
"112": "126",
6+
"113": "127",
7+
"114": "128",
8+
"115": "130",
9+
"116": "131",
10+
"117": "132",
11+
"118": "133",
12+
"119": "134"
13+
}

src/selenium4/java/com/nordstrom/automation/selenium/SeleniumConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.List;
2020
import java.util.Map;
2121
import java.util.Objects;
22+
import java.util.Optional;
2223
import java.util.stream.Collectors;
2324

2425
import org.apache.commons.configuration2.ex.ConfigurationException;
@@ -496,6 +497,8 @@ public Path createNodeConfig(String capabilities, URL hubUrl) throws IOException
496497
Map<String, Object> thisConfig = new HashMap<>();
497498
thisConfig.put("stereotype", theseCaps);
498499
thisConfig.put("display-name", GridUtility.getPersonality(theseCaps));
500+
Optional.ofNullable(GridUtility.getDriverPath(theseCaps))
501+
.ifPresent(value -> thisConfig.put("webdriver-executable", value));
499502
driverConfiguration.add(thisConfig);
500503
});
501504
}

src/selenium4/java/com/nordstrom/automation/selenium/plugins/ChromePlugin.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ public ChromePlugin() {
1616
super(ChromeCaps.DRIVER_NAME);
1717
}
1818

19+
/**
20+
* Extension constructor for <b>ChromeDriver</b> subclass objects.
21+
*
22+
* @param browserName browser name
23+
*/
24+
protected ChromePlugin(String browserName) {
25+
super(browserName);
26+
}
27+
1928
/**
2029
* <b>org.openqa.selenium.chrome.ChromeDriver</b>
2130
*
Lines changed: 178 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,68 @@
11
package com.nordstrom.automation.selenium.plugins;
22

3+
import java.io.BufferedReader;
4+
import java.io.InputStreamReader;
5+
import java.lang.reflect.Type;
6+
import java.nio.file.Files;
7+
import java.nio.file.Paths;
8+
import java.util.List;
39
import java.util.Map;
10+
import java.util.Objects;
11+
12+
import org.openqa.selenium.json.Json;
13+
import org.openqa.selenium.json.TypeToken;
14+
import org.openqa.selenium.manager.SeleniumManager;
15+
import org.openqa.selenium.manager.SeleniumManagerOutput.Result;
416

517
import com.nordstrom.automation.selenium.SeleniumConfig;
18+
import com.nordstrom.automation.selenium.core.JsUtility;
19+
import com.nordstrom.automation.selenium.exceptions.DriverExecutableNotFoundException;
20+
import com.nordstrom.common.file.OSInfo;
21+
import com.nordstrom.common.file.OSInfo.OSType;
622

723
/**
824
* This class is the plug-in for <b>OperaDriver</b>.
925
*/
10-
public class OperaPlugin extends RemoteWebDriverPlugin {
26+
public class OperaPlugin extends ChromePlugin {
27+
28+
private static final String TEMPLATE =
29+
"{\"browserName\":\"chrome\"," +
30+
"\"goog:chromeOptions\":{\"binary\":\"<binary-path>\"}," +
31+
"\"nord:options\":{\"personality\":\"opera\"," +
32+
"\"driverPath\":\"<driver-path>\"}}";
33+
34+
private static final String BASELINE =
35+
"{\"browserName\":\"chrome\"," +
36+
"\"goog:chromeOptions\":{\"args\":[\"--disable-infobars\",\"--disable-dev-shm-usage\"," +
37+
"\"--remote-debugging-port=9222\",\"--no-sandbox\"," +
38+
"\"--disable-gpu\",\"--disable-logging\"]," +
39+
"\"prefs\":{\"credentials_enable_service\":false}}," +
40+
"\"nord:options\":{\"personality\":\"opera\"," +
41+
"\"pluginClass\":\"com.nordstrom.automation.selenium.plugins.OperaPlugin\"}}";
42+
43+
private static final OSType OS_TYPE;
44+
private static final Map<String, String> OPERA_TO_CHROMIUM;
45+
private static final String BINARY_PATH;
46+
private static final String DRIVER_PATH;
47+
private static final String CAPABILITIES;
48+
private static final Map<String, String> PERSONALITIES;
49+
50+
private static final String VERSION_MAPPINGS = "operaChromiumVersions.json";
51+
private static final Type MAP_TYPE = new TypeToken<Map<String, String>>() {}.getType();
52+
53+
static {
54+
OS_TYPE = OSInfo.getDefault().getType();
55+
String mappings = JsUtility.getScriptResource(VERSION_MAPPINGS);
56+
OPERA_TO_CHROMIUM = new Json().toType(mappings, MAP_TYPE);
57+
58+
BINARY_PATH = Objects.requireNonNull(findOperaBinary(), "Failed finding the Opera browser binary");
59+
DRIVER_PATH = Objects.requireNonNull(findDriverBinary(), "Failed finding the 'chromedriver' binary");
60+
61+
CAPABILITIES = TEMPLATE
62+
.replace("<binary-path>", BINARY_PATH.replaceAll("\\\\", "/"))
63+
.replace("<driver-path>", DRIVER_PATH.replaceAll("\\\\", "/"));
64+
PERSONALITIES = Map.of(OperaCaps.DRIVER_NAME, BASELINE);
65+
}
1166

1267
/**
1368
* Constructor for <b>OperaPlugin</b> objects.
@@ -16,58 +71,146 @@ public OperaPlugin() {
1671
super(OperaCaps.DRIVER_NAME);
1772
}
1873

19-
/**
20-
* <b>org.openqa.selenium.opera.OperaDriver</b>
21-
*
22-
* <pre>&lt;dependency&gt;
23-
* &lt;groupId&gt;org.seleniumhq.selenium&lt;/groupId&gt;
24-
* &lt;artifactId&gt;selenium-opera-driver&lt;/artifactId&gt;
25-
* &lt;version&gt;4.30.0&lt;/version&gt;
26-
*&lt;/dependency&gt;</pre>
27-
*
28-
* <b>net.bytebuddy.matcher.ElementMatcher</b>
29-
*
30-
* <pre>&lt;dependency&gt;
31-
* &lt;groupId&gt;net.bytebuddy&lt;/groupId&gt;
32-
* &lt;artifactId&gt;byte-buddy&lt;/artifactId&gt;
33-
* &lt;version&gt;1.17.5&lt;/version&gt;
34-
*&lt;/dependency&gt;</pre>
35-
*/
36-
private static final String[] DEPENDENCY_CONTEXTS = {
37-
"org.openqa.selenium.opera.OperaDriver",
38-
"net.bytebuddy.matcher.ElementMatcher"
39-
};
40-
4174
/**
4275
* {@inheritDoc}
4376
*/
4477
@Override
45-
public String[] getDependencyContexts() {
46-
return DEPENDENCY_CONTEXTS;
78+
public String getCapabilities(SeleniumConfig config) {
79+
return CAPABILITIES;
4780
}
4881

4982
/**
5083
* {@inheritDoc}
5184
*/
5285
@Override
53-
public String getCapabilities(SeleniumConfig config) {
54-
return OperaCaps.getCapabilities();
86+
public Map<String, String> getPersonalities() {
87+
return PERSONALITIES;
5588
}
5689

5790
/**
58-
* {@inheritDoc}
91+
* Get list of system property names recognized by the driver associated with this plug-in.
92+
*
93+
* @param capabilities JSON {@link org.openqa.selenium.Capabilities Capabilities} object
94+
* @return list of system property names
5995
*/
60-
@Override
61-
public Map<String, String> getPersonalities() {
62-
return OperaCaps.getPersonalities();
96+
public String[] getPropertyNames(String capabilities) {
97+
return ChromeCaps.PROPERTY_NAMES;
6398
}
6499

65100
/**
66-
* {@inheritDoc}
101+
* Get path to installed <b>Opera</b> browser.
102+
*
103+
* @return path to installed <b>Opera</b> browser; {@code null} if browser not found
67104
*/
68-
@Override
69-
public String[] getPropertyNames(String capabilities) {
70-
return OperaCaps.getPropertyNames(capabilities);
105+
public static String findOperaBinary() {
106+
switch (OS_TYPE) {
107+
case MACINTOSH:
108+
return findOnMac();
109+
case UNIX:
110+
return findOnLinux();
111+
case WINDOWS:
112+
return findOnWindows();
113+
default:
114+
return null;
115+
}
71116
}
72117

118+
private static String findOnWindows() {
119+
String[] possiblePaths = { "C:\\Program Files\\Opera\\opera.exe",
120+
System.getenv("LOCALAPPDATA") + "\\Programs\\Opera\\opera.exe" };
121+
122+
return firstExistingPath(possiblePaths);
123+
}
124+
125+
private static String findOnMac() {
126+
String[] possiblePaths = { "/Applications/Opera.app/Contents/MacOS/Opera",
127+
System.getProperty("user.home") + "/Applications/Opera.app/Contents/MacOS/Opera" };
128+
129+
return firstExistingPath(possiblePaths);
130+
}
131+
132+
private static String findOnLinux() {
133+
String[] possiblePaths = { "/usr/bin/opera", "/usr/local/bin/opera", "/opt/opera/opera" };
134+
135+
return firstExistingPath(possiblePaths);
136+
}
137+
138+
private static String firstExistingPath(String[] paths) {
139+
for (String path : paths) {
140+
if (path != null && Files.exists(Paths.get(path))) {
141+
return path;
142+
}
143+
}
144+
return null;
145+
}
146+
147+
/**
148+
* Detect version of installed <b>Opera</b> browser.
149+
*
150+
* @return version of installed <b>Opera</b> browser; {@code null} if detection fails
151+
*/
152+
public static String detectOperaVersion() {
153+
try {
154+
switch (OS_TYPE) {
155+
case MACINTOSH:
156+
return detectMac();
157+
case UNIX:
158+
return detectLinux();
159+
case WINDOWS:
160+
return detectWindows();
161+
default:
162+
return null;
163+
}
164+
} catch (Exception e) {
165+
e.printStackTrace();
166+
return null;
167+
}
168+
}
169+
170+
private static String detectWindows() throws Exception {
171+
// Use PowerShell to get ProductVersion from file metadata
172+
String[] cmd = { "powershell.exe", "-Command", "\"(Get-Item '" + BINARY_PATH + "').VersionInfo.ProductVersion\"" };
173+
return runCommand(cmd).trim();
174+
}
175+
176+
private static String detectMac() throws Exception {
177+
String[] cmd = { "/usr/libexec/PlistBuddy", "-c", "Print :CFBundleShortVersionString", BINARY_PATH + "/Contents/Info.plist" };
178+
return runCommand(cmd).trim();
179+
}
180+
181+
private static String detectLinux() throws Exception {
182+
String[] cmd = { "/bin/sh", "-c", "cat " + BINARY_PATH + "/resources/version" };
183+
return runCommand(cmd).trim();
184+
}
185+
186+
private static String runCommand(String[] command) throws Exception {
187+
Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
188+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
189+
String line = reader.readLine();
190+
return (line != null) ? line : null;
191+
}
192+
}
193+
194+
/**
195+
* Find 'chromedriver' binary that works with the active installation of Opera.
196+
*
197+
* @return path to 'chromedriver' binary; {@code null} if driver not found
198+
*/
199+
public static String findDriverBinary() {
200+
String operaVersion = Objects.requireNonNull(detectOperaVersion(),
201+
"Failed detecting Opera browser version");
202+
String operaMajor = operaVersion.split("\\.")[0];
203+
String chromiumMajor = Objects.requireNonNull(OPERA_TO_CHROMIUM.get(operaMajor),
204+
"No Chromium version mapping found for Opera major version: " + operaMajor);
205+
206+
try {
207+
SeleniumManager manager = SeleniumManager.getInstance();
208+
Result result = manager.getBinaryPaths(List.of(
209+
"--driver", "chromedriver",
210+
"--driver-version", chromiumMajor));
211+
return result.getDriverPath();
212+
} catch (IllegalStateException e) {
213+
throw new DriverExecutableNotFoundException(ChromeCaps.DRIVER_PATH);
214+
}
215+
}
73216
}

0 commit comments

Comments
 (0)