diff --git a/Rakefile b/Rakefile index 447f256344583..6c60dfbf8d0b0 100644 --- a/Rakefile +++ b/Rakefile @@ -135,6 +135,7 @@ task tests: [ '//java/test/org/openqa/selenium/ie:ie', '//java/test/org/openqa/selenium/chrome:chrome', '//java/test/org/openqa/selenium/edge:edge', + '//java/test/org/openqa/selenium/opera:opera', '//java/test/org/openqa/selenium/support:small-tests', '//java/test/org/openqa/selenium/support:large-tests', '//java/test/org/openqa/selenium/remote:small-tests', @@ -184,6 +185,7 @@ task test_ie: [ ] task test_jobbie: [:test_ie] task test_firefox: ['//java/test/org/openqa/selenium/firefox:marionette:run'] +task test_opera: ['//java/test/org/openqa/selenium/opera:opera:run'] task test_remote_server: [ '//java/test/org/openqa/selenium/remote/server:small-tests:run', '//java/test/org/openqa/selenium/remote/server/log:test:run' @@ -208,6 +210,8 @@ task :test_java_webdriver do Rake::Task['test_chrome'].invoke elsif SeleniumRake::Checks.edge? Rake::Task['test_edge'].invoke + elsif SeleniumRake::Checks.opera? + Rake::Task['test_opera'].invoke else Rake::Task['test_htmlunit'].invoke Rake::Task['test_firefox'].invoke diff --git a/java/src/org/openqa/selenium/BUILD.bazel b/java/src/org/openqa/selenium/BUILD.bazel index a10ac67d52a0f..8fb14bfc0a041 100644 --- a/java/src/org/openqa/selenium/BUILD.bazel +++ b/java/src/org/openqa/selenium/BUILD.bazel @@ -56,6 +56,7 @@ java_export( "//java/src/org/openqa/selenium/edge", "//java/src/org/openqa/selenium/firefox", "//java/src/org/openqa/selenium/ie", + "//java/src/org/openqa/selenium/opera", "//java/src/org/openqa/selenium/remote", "//java/src/org/openqa/selenium/safari", "//java/src/org/openqa/selenium/support", diff --git a/java/src/org/openqa/selenium/grid/BUILD.bazel b/java/src/org/openqa/selenium/grid/BUILD.bazel index 496ea262d0973..a771e1ec4f707 100644 --- a/java/src/org/openqa/selenium/grid/BUILD.bazel +++ b/java/src/org/openqa/selenium/grid/BUILD.bazel @@ -160,6 +160,7 @@ java_export( "//java/src/org/openqa/selenium/grid/sessionmap/httpd", "//java/src/org/openqa/selenium/grid/sessionqueue/httpd", "//java/src/org/openqa/selenium/ie", + "//java/src/org/openqa/selenium/opera", "//java/src/org/openqa/selenium/remote", "//java/src/org/openqa/selenium/safari", "//javascript/grid-ui:react_jar", diff --git a/java/src/org/openqa/selenium/opera/AddHasCasting.java b/java/src/org/openqa/selenium/opera/AddHasCasting.java new file mode 100644 index 0000000000000..fe9391a51a173 --- /dev/null +++ b/java/src/org/openqa/selenium/opera/AddHasCasting.java @@ -0,0 +1,57 @@ +// 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.opera; + +import com.google.auto.service.AutoService; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.remote.AdditionalHttpCommands; +import org.openqa.selenium.remote.AugmenterProvider; +import org.openqa.selenium.remote.CommandInfo; +import org.openqa.selenium.remote.http.HttpMethod; + +import java.util.Map; +import java.util.function.Predicate; + +import static org.openqa.selenium.remote.Browser.OPERA; + +@SuppressWarnings({"rawtypes", "RedundantSuppression"}) +@AutoService({AdditionalHttpCommands.class, AugmenterProvider.class}) +public class AddHasCasting extends org.openqa.selenium.chromium.AddHasCasting { + + private static final Map COMMANDS = + Map.of( + GET_CAST_SINKS, new CommandInfo("session/:sessionId/ms/cast/get_sinks", HttpMethod.GET), + SET_CAST_SINK_TO_USE, + new CommandInfo("session/:sessionId/ms/cast/set_sink_to_use", HttpMethod.POST), + START_CAST_TAB_MIRRORING, + new CommandInfo("session/:sessionId/ms/cast/start_tab_mirroring", HttpMethod.POST), + GET_CAST_ISSUE_MESSAGE, + new CommandInfo("session/:sessionId/ms/cast/get_issue_message", HttpMethod.GET), + STOP_CASTING, + new CommandInfo("session/:sessionId/ms/cast/stop_casting", HttpMethod.POST)); + + @Override + public Map getAdditionalCommands() { + return COMMANDS; + } + + @Override + public Predicate isApplicable() { + return OPERA::is; + } +} diff --git a/java/src/org/openqa/selenium/opera/AddHasCdp.java b/java/src/org/openqa/selenium/opera/AddHasCdp.java new file mode 100644 index 0000000000000..d1e787741d927 --- /dev/null +++ b/java/src/org/openqa/selenium/opera/AddHasCdp.java @@ -0,0 +1,48 @@ +// 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.opera; + +import com.google.auto.service.AutoService; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.remote.AdditionalHttpCommands; +import org.openqa.selenium.remote.AugmenterProvider; +import org.openqa.selenium.remote.CommandInfo; +import org.openqa.selenium.remote.http.HttpMethod; + +import java.util.Map; +import java.util.function.Predicate; + +import static org.openqa.selenium.remote.Browser.OPERA; + +@SuppressWarnings({"rawtypes", "RedundantSuppression"}) +@AutoService({AdditionalHttpCommands.class, AugmenterProvider.class}) +public class AddHasCdp extends org.openqa.selenium.chromium.AddHasCdp { + + private static final Map COMMANDS = + Map.of(EXECUTE_CDP, new CommandInfo("session/:sessionId/ms/cdp/execute", HttpMethod.POST)); + + @Override + public Map getAdditionalCommands() { + return COMMANDS; + } + + @Override + public Predicate isApplicable() { + return OPERA::is; + } +} diff --git a/java/src/org/openqa/selenium/opera/BUILD.bazel b/java/src/org/openqa/selenium/opera/BUILD.bazel new file mode 100644 index 0000000000000..b8030e54a3848 --- /dev/null +++ b/java/src/org/openqa/selenium/opera/BUILD.bazel @@ -0,0 +1,25 @@ +load("//java:defs.bzl", "java_export") +load("//java:version.bzl", "SE_VERSION") + +java_export( + name = "opera", + srcs = glob(["*.java"]), + maven_coordinates = "org.seleniumhq.selenium:selenium-opera-driver:%s" % SE_VERSION, + pom_template = "//java/src/org/openqa/selenium:template-pom", + tags = [ + "release-artifact", + ], + visibility = [ + "//visibility:public", + ], + exports = [ + "//java/src/org/openqa/selenium/chromium", + ], + deps = [ + "//java:auto-service", + "//java/src/org/openqa/selenium:core", + "//java/src/org/openqa/selenium/chromium", + "//java/src/org/openqa/selenium/manager", + "//java/src/org/openqa/selenium/remote", + ], +) diff --git a/java/src/org/openqa/selenium/opera/OperaDriver.java b/java/src/org/openqa/selenium/opera/OperaDriver.java new file mode 100644 index 0000000000000..139030cdcba7f --- /dev/null +++ b/java/src/org/openqa/selenium/opera/OperaDriver.java @@ -0,0 +1,97 @@ +// 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.opera; + +import org.openqa.selenium.Beta; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chromium.ChromiumDriver; +import org.openqa.selenium.chromium.ChromiumDriverCommandExecutor; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.CommandInfo; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.remote.RemoteWebDriverBuilder; +import org.openqa.selenium.remote.http.ClientConfig; +import org.openqa.selenium.remote.service.DriverFinder; +import org.openqa.selenium.remote.service.DriverService; + +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A {@link WebDriver} implementation that controls a Chromium-based Opera browser running on the local + * machine. It requires an operadriver executable to be available in PATH. + * + * @see operadriver + */ +public class OperaDriver extends ChromiumDriver { + + public OperaDriver() { + this(new OperaOptions()); + } + + public OperaDriver(OperaOptions options) { + this(new OperaDriverService.Builder().build(), options); + } + + public OperaDriver(OperaDriverService service) { + this(service, new OperaOptions()); + } + + public OperaDriver(OperaDriverService service, OperaOptions options) { + this(service, options, ClientConfig.defaultConfig()); + } + + public OperaDriver(OperaDriverService service, OperaOptions options, ClientConfig clientConfig) { + super(generateExecutor(service, options, clientConfig), options, OperaOptions.CAPABILITY); + casting = new AddHasCasting().getImplementation(getCapabilities(), getExecuteMethod()); + cdp = new AddHasCdp().getImplementation(getCapabilities(), getExecuteMethod()); + } + + private static OperaDriver.OperaDriverCommandExecutor generateExecutor( + OperaDriverService service, OperaOptions options, ClientConfig clientConfig) { + Require.nonNull("Driver service", service); + Require.nonNull("Driver options", options); + Require.nonNull("Driver clientConfig", clientConfig); + DriverFinder finder = new DriverFinder(service, options); + service.setExecutable(finder.getDriverPath()); + if (finder.hasBrowserPath()) { + options.setBinary(finder.getBrowserPath()); + options.setCapability("browserVersion", (Object) null); + } + return new OperaDriver.OperaDriverCommandExecutor(service, clientConfig); + } + + @Beta + public static RemoteWebDriverBuilder builder() { + return RemoteWebDriver.builder().oneOf(new OperaOptions()); + } + + private static class OperaDriverCommandExecutor extends ChromiumDriverCommandExecutor { + public OperaDriverCommandExecutor(DriverService service, ClientConfig clientConfig) { + super(service, getExtraCommands(), clientConfig); + } + + private static Map getExtraCommands() { + return Stream.of( + new AddHasCasting().getAdditionalCommands(), new AddHasCdp().getAdditionalCommands()) + .flatMap((m) -> m.entrySet().stream()) + .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); + } + } +} diff --git a/java/src/org/openqa/selenium/opera/OperaDriverInfo.java b/java/src/org/openqa/selenium/opera/OperaDriverInfo.java new file mode 100644 index 0000000000000..12f29569a7eb8 --- /dev/null +++ b/java/src/org/openqa/selenium/opera/OperaDriverInfo.java @@ -0,0 +1,84 @@ +// 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.opera; + +import com.google.auto.service.AutoService; + +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.ImmutableCapabilities; +import org.openqa.selenium.SessionNotCreatedException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverInfo; +import org.openqa.selenium.chromium.ChromiumDriverInfo; +import org.openqa.selenium.remote.service.DriverFinder; + +import java.util.Optional; + +import static org.openqa.selenium.remote.Browser.OPERA; +import static org.openqa.selenium.remote.CapabilityType.BROWSER_NAME; + +@AutoService(WebDriverInfo.class) +public class OperaDriverInfo extends ChromiumDriverInfo { + + @Override + public String getDisplayName() { + return "Opera"; + } + + @Override + public Capabilities getCanonicalCapabilities() { + return new ImmutableCapabilities(BROWSER_NAME, OPERA.browserName()); + } + + @Override + public boolean isSupporting(Capabilities capabilities) { + return OPERA.is(capabilities.getBrowserName()); + } + + @Override + public boolean isSupportingCdp() { + return true; + } + + @Override + public boolean isSupportingBiDi() { + return false; + } + + @Override + public boolean isAvailable() { + return new DriverFinder(OperaDriverService.createDefaultService(), getCanonicalCapabilities()) + .isAvailable(); + } + + @Override + public boolean isPresent() { + return new DriverFinder(OperaDriverService.createDefaultService(), getCanonicalCapabilities()) + .isPresent(); + } + + @Override + public Optional createDriver(Capabilities capabilities) + throws SessionNotCreatedException { + if (!isAvailable() || !isSupporting(capabilities)) { + return Optional.empty(); + } + + return Optional.of(new OperaDriver(new OperaOptions().merge(capabilities))); + } +} diff --git a/java/src/org/openqa/selenium/opera/OperaDriverService.java b/java/src/org/openqa/selenium/opera/OperaDriverService.java new file mode 100644 index 0000000000000..96001fc97092b --- /dev/null +++ b/java/src/org/openqa/selenium/opera/OperaDriverService.java @@ -0,0 +1,300 @@ +// 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.opera; + +import com.google.auto.service.AutoService; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.chromium.ChromiumDriverLogLevel; +import org.openqa.selenium.remote.service.DriverFinder; +import org.openqa.selenium.remote.service.DriverService; + +import java.io.File; +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.unmodifiableList; +import static org.openqa.selenium.remote.Browser.OPERA; + +/** + * Manages the life and death of a operadriver server. + */ +public class OperaDriverService extends DriverService { + + public static final String OPERA_DRIVER_NAME = "operadriver"; + + /** + * System property that defines the location of the operadriver executable that will be used by + * the {@link #createDefaultService() default service}. + */ + public static final String OPERA_DRIVER_EXE_PROPERTY = "webdriver.opera.driver"; + + /** System property that toggles the formatting of the timestamps of the logs */ + public static final String OPERA_DRIVER_READABLE_TIMESTAMP = "webdriver.opera.readableTimestamp"; + + /** + * System property that defines the location of the log that will be written by + * the {@link #createDefaultService() default service}. + */ + public static final String OPERA_DRIVER_LOG_PROPERTY = "webdriver.opera.logfile"; + + /** System property that defines the {@link ChromiumDriverLogLevel} for OperaDriver logs. */ + public static final String OPERA_DRIVER_LOG_LEVEL_PROPERTY = "webdriver.opera.loglevel"; + + /** + * Boolean system property that defines whether OperaDriver should append to existing log file. + */ + public static final String OPERA_DRIVER_APPEND_LOG_PROPERTY = "webdriver.opera.appendLog"; + + /** + * Boolean system property that defines whether the OperaDriver executable should be started + * with verbose logging. + */ + public static final String OPERA_DRIVER_VERBOSE_LOG_PROPERTY = "webdriver.opera.verboseLogging"; + + /** + * Boolean system property that defines whether the OperaDriver executable should be started + * in silent mode. + */ + public static final String OPERA_DRIVER_SILENT_OUTPUT_PROPERTY = "webdriver.opera.silentOutput"; + + /** + * System property that defines comma-separated list of remote IPv4 addresses which are allowed to + * connect to OperaDriver. + */ + public static final String OPERA_DRIVER_ALLOWED_IPS_PROPERTY = "webdriver.opera.withAllowedIps"; + + /** + * System property that defines whether the OperaDriver executable should check for build version + * compatibility between OperaDriver and the browser. + */ + public static final String OPERA_DRIVER_DISABLE_BUILD_CHECK = "webdriver.opera.disableBuildCheck"; + + /** + * + * @param executable The operadriver executable. + * @param port Which port to start the operadriver on. + * @param args The arguments to the launched server. + * @param environment The environment for the launched server. + * @throws IOException If an I/O error occurs. + */ + public OperaDriverService(File executable, int port, List args, + Map environment) throws IOException { + super(executable, port, DEFAULT_TIMEOUT, args, environment); + } + + /** + * + * @param executable The operadriver executable. + * @param port Which port to start the operadriver on. + * @param timeout Timeout waiting for driver server to start. + * @param args The arguments to the launched server. + * @param environment The environment for the launched server. + * @throws IOException If an I/O error occurs. + */ + public OperaDriverService(File executable, int port, Duration timeout, List args, + Map environment) throws IOException { + super(executable, port, timeout, args, environment); + } + + /** + * Configures and returns a new {@link OperaDriverService} using the default configuration. In + * this configuration, the service will use the operadriver executable identified by the + * {@link #OPERA_DRIVER_EXE_PROPERTY} system property. Each service created by this method will + * be configured to use a free port on the current system. + * + * @return A new OperaDriverService using the default configuration. + */ + + public String getDriverName() { + return OPERA_DRIVER_NAME; + } + + public String getDriverProperty() { + return OPERA_DRIVER_EXE_PROPERTY; + } + + @Override + public Capabilities getDefaultDriverOptions() { + return new OperaOptions(); + } + + /** + * Configures and returns a new {@link OperaDriverService} using the default configuration. In this + * configuration, the service will use the {@code operadriver} executable identified by the {@link + * DriverFinder#getDriverPath()} (DriverService, Capabilities)}. Each service created by this + * method will be configured to use a free port on the current system. + * + * @return A new OperaDriverService using the default configuration. + */ + public static OperaDriverService createDefaultService() { + return new Builder().build(); + } + + /** + * Builder used to configure new {@link OperaDriverService} instances. + */ + @AutoService(DriverService.Builder.class) + public static class Builder extends DriverService.Builder< + OperaDriverService, OperaDriverService.Builder> { + + private Boolean disableBuildCheck; + private Boolean readableTimestamp; + private Boolean appendLog; + private Boolean verbose; + private Boolean silent; + private String allowedListIps; + private ChromiumDriverLogLevel logLevel; + + @Override + public int score(Capabilities capabilities) { + int score = 0; + + if (OPERA.is(capabilities)) { + score++; + } + + if (capabilities.getCapability(OperaOptions.CAPABILITY) != null) { + score++; + } + + return score; + } + + /** + * Configures the driver server appending to log file. + * + * @param appendLog True for appending to log file, false otherwise. + * @return A self reference. + */ + public OperaDriverService.Builder withAppendLog(boolean appendLog) { + this.appendLog = appendLog; + return this; + } + + /** + * Allows the driver to be used with potentially incompatible versions of the browser. + * + * @param noBuildCheck True for not enforcing matching versions. + * @return A self reference. + */ + public OperaDriverService.Builder withBuildCheckDisabled(boolean noBuildCheck) { + this.disableBuildCheck = noBuildCheck; + return this; + } + + /** + * Configures the driver server log level. + * + * @param logLevel {@link ChromiumDriverLogLevel} for desired log level output. + * @return A self reference. + */ + public OperaDriverService.Builder withLoglevel(ChromiumDriverLogLevel logLevel) { + this.logLevel = logLevel; + this.silent = false; + this.verbose = false; + return this; + } + + /** + * Configures the driver server verbosity. + * + * @param verbose true for verbose output, false otherwise. + * @return A self reference. + */ + public Builder withVerbose(boolean verbose) { + this.verbose = verbose; + return this; + } + + /** + * Configures the driver server for silent output. + * + * @param silent true for silent output, false otherwise. + * @return A self reference. + */ + public Builder withSilent(boolean silent) { + this.silent = silent; + return this; + } + + @Override + protected void loadSystemProperties() { + parseLogOutput(OPERA_DRIVER_LOG_PROPERTY); + if (disableBuildCheck == null) { + this.disableBuildCheck = Boolean.getBoolean(OPERA_DRIVER_DISABLE_BUILD_CHECK); + } + if (readableTimestamp == null) { + this.readableTimestamp = Boolean.getBoolean(OPERA_DRIVER_READABLE_TIMESTAMP); + } + if (appendLog == null) { + this.appendLog = Boolean.getBoolean(OPERA_DRIVER_APPEND_LOG_PROPERTY); + } + if (verbose == null && Boolean.getBoolean(OPERA_DRIVER_VERBOSE_LOG_PROPERTY)) { + withVerbose(Boolean.getBoolean(OPERA_DRIVER_VERBOSE_LOG_PROPERTY)); + } + if (silent == null && Boolean.getBoolean(OPERA_DRIVER_SILENT_OUTPUT_PROPERTY)) { + withSilent(Boolean.getBoolean(OPERA_DRIVER_SILENT_OUTPUT_PROPERTY)); + } + if (allowedListIps == null) { + this.allowedListIps = System.getProperty(OPERA_DRIVER_ALLOWED_IPS_PROPERTY); + } + if (logLevel == null && System.getProperty(OPERA_DRIVER_LOG_LEVEL_PROPERTY) != null) { + String level = System.getProperty(OPERA_DRIVER_LOG_LEVEL_PROPERTY); + withLoglevel(ChromiumDriverLogLevel.fromString(level)); + } + } + + @Override + protected List createArgs() { + if (getLogFile() == null) { + String logFilePath = System.getProperty(OPERA_DRIVER_LOG_PROPERTY); + if (logFilePath != null) { + withLogFile(new File(logFilePath)); + } + } + + List args = new ArrayList<>(); + args.add(String.format("--port=%d", getPort())); + if (getLogFile() != null) { + args.add(String.format("--log-path=%s", getLogFile().getAbsolutePath())); + } + if (verbose) { + args.add("--verbose"); + } + if (silent) { + args.add("--silent"); + } + + return unmodifiableList(args); + } + + @Override + protected OperaDriverService createDriverService( + File exe, int port, Duration timeout, List args, Map environment) { + try { + return new OperaDriverService(exe, port, timeout, args, environment); + } catch (IOException e) { + throw new WebDriverException(e); + } + } + } +} diff --git a/java/src/org/openqa/selenium/opera/OperaOptions.java b/java/src/org/openqa/selenium/opera/OperaOptions.java new file mode 100644 index 0000000000000..74ac38e85c44f --- /dev/null +++ b/java/src/org/openqa/selenium/opera/OperaOptions.java @@ -0,0 +1,70 @@ +// 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.opera; + +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.chromium.ChromiumOptions; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.CapabilityType; + +import static org.openqa.selenium.remote.Browser.OPERA; + +/** + * Class to manage options specific to {@link OperaDriver}. + * + *

Example usage: + * + *


+ * OperaOptions options = new OperaOptions()
+ * options.addExtensions(new File("/path/to/extension.crx"))
+ * options.setBinary(new File("/path/to/opera"));
+ *
+ * // For use with OperaDriver:
+ * OperaDriver driver = new OperaDriver(options);
+ *
+ * // For use with RemoteWebDriver:
+ * RemoteWebDriver driver = new RemoteWebDriver(
+ *     new URL("http://localhost:4444/"),
+ *     new OperaOptions());
+ * 
+ */ +public class OperaOptions extends ChromiumOptions { + + /** + * Key used to store a set of OperaOptions in a {@link org.openqa.selenium.Capabilities} + * object. + */ + public static final String CAPABILITY = "opera:operaOptions"; + + public OperaOptions() { + super(CapabilityType.BROWSER_NAME, OPERA.browserName(), CAPABILITY); + setExperimentalOption("w3c", true); + } + + @Override + public OperaOptions merge(Capabilities extraCapabilities) { + Require.nonNull("Capabilities to merge", extraCapabilities); + + OperaOptions newInstance = new OperaOptions(); + newInstance.mergeInPlace(this); + newInstance.mergeInPlace(extraCapabilities); + newInstance.mergeInOptionsFromCaps(CAPABILITY, extraCapabilities); + + return newInstance; + } +} diff --git a/java/test/org/openqa/selenium/WindowSwitchingTest.java b/java/test/org/openqa/selenium/WindowSwitchingTest.java index 757bd2c369c61..008d773b55986 100644 --- a/java/test/org/openqa/selenium/WindowSwitchingTest.java +++ b/java/test/org/openqa/selenium/WindowSwitchingTest.java @@ -27,6 +27,8 @@ import static org.openqa.selenium.testing.drivers.Browser.CHROME; import static org.openqa.selenium.testing.drivers.Browser.FIREFOX; import static org.openqa.selenium.testing.drivers.Browser.IE; +import static org.openqa.selenium.testing.drivers.Browser.LEGACY_OPERA; +import static org.openqa.selenium.testing.drivers.Browser.OPERA; import static org.openqa.selenium.testing.drivers.Browser.SAFARI; import java.util.Set; @@ -204,6 +206,9 @@ void testClickingOnAButtonThatClosesAnOpenWindowDoesNotCauseTheBrowserToHang() { @Test @Ignore(SAFARI) public void testCanCallGetWindowHandlesAfterClosingAWindow() { + assumeFalse(Browser.detect() == Browser.LEGACY_OPERA && + getEffectivePlatform(driver).is(Platform.WINDOWS)); + driver.get(pages.xhtmlTestPage); Set currentWindowHandles = driver.getWindowHandles(); diff --git a/java/test/org/openqa/selenium/opera/OperaDriverServiceTest.java b/java/test/org/openqa/selenium/opera/OperaDriverServiceTest.java new file mode 100644 index 0000000000000..ba24fa7c4c5a9 --- /dev/null +++ b/java/test/org/openqa/selenium/opera/OperaDriverServiceTest.java @@ -0,0 +1,91 @@ +// 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.opera; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Tag; +import org.openqa.selenium.chromium.ChromiumDriverLogLevel; + +import java.io.File; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; + +@Tag("UnitTests") +public class OperaDriverServiceTest { + + @Test + public void builderPassesTimeoutToDriverService() { + File exe = new File("someFile"); + Duration defaultTimeout = Duration.ofSeconds(20); + Duration customTimeout = Duration.ofSeconds(60); + + OperaDriverService.Builder builderMock = spy(OperaDriverService.Builder.class); + builderMock.build(); + + verify(builderMock).createDriverService(any(), anyInt(), eq(defaultTimeout), any(), any()); + + builderMock.withTimeout(customTimeout); + builderMock.build(); + verify(builderMock).createDriverService(any(), anyInt(), eq(customTimeout), any(), any()); + } + + @Test + void testScoring() { + OperaDriverService.Builder builder = new OperaDriverService.Builder(); + assertThat(builder.score(new OperaOptions())).isPositive(); + } + + @Test + void logLevelLastWins() { + OperaDriverService.Builder builderMock = spy(OperaDriverService.Builder.class); + + List silentLast = Arrays.asList("--port=1", "--log-level=OFF"); + builderMock.withLoglevel(ChromiumDriverLogLevel.ALL).usingPort(1).withSilent(true).build(); + verify(builderMock).createDriverService(any(), anyInt(), any(), eq(silentLast), any()); + + List silentFirst = Arrays.asList("--port=1", "--log-level=DEBUG"); + builderMock.withSilent(true).withLoglevel(ChromiumDriverLogLevel.DEBUG).usingPort(1).build(); + verify(builderMock).createDriverService(any(), anyInt(), any(), eq(silentFirst), any()); + + List verboseLast = Arrays.asList("--port=1", "--log-level=ALL"); + builderMock.withLoglevel(ChromiumDriverLogLevel.OFF).usingPort(1).withVerbose(true).build(); + verify(builderMock).createDriverService(any(), anyInt(), any(), eq(verboseLast), any()); + + List verboseFirst = Arrays.asList("--port=1", "--log-level=INFO"); + builderMock.withVerbose(true).withLoglevel(ChromiumDriverLogLevel.INFO).usingPort(1).build(); + verify(builderMock).createDriverService(any(), anyInt(), any(), eq(verboseFirst), any()); + } + + // Setting these to false makes no sense; we're just going to ignore it. + @Test + void ignoreFalseLogging() { + OperaDriverService.Builder builderMock = spy(OperaDriverService.Builder.class); + + List falseSilent = Arrays.asList("--port=1", "--log-level=DEBUG"); + builderMock.withLoglevel(ChromiumDriverLogLevel.DEBUG).usingPort(1).withSilent(false).build(); + verify(builderMock).createDriverService(any(), anyInt(), any(), eq(falseSilent), any()); + } +} diff --git a/java/test/org/openqa/selenium/opera/OperaOptionsFunctionalTest.java b/java/test/org/openqa/selenium/opera/OperaOptionsFunctionalTest.java new file mode 100644 index 0000000000000..424df9f814afc --- /dev/null +++ b/java/test/org/openqa/selenium/opera/OperaOptionsFunctionalTest.java @@ -0,0 +1,73 @@ +// 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.opera; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openqa.selenium.remote.CapabilityType.ACCEPT_INSECURE_CERTS; + +import org.junit.jupiter.api.Test; +import org.openqa.selenium.HasCapabilities; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.testing.JupiterTestBase; +import org.openqa.selenium.testing.NoDriverBeforeTest; +import org.openqa.selenium.testing.drivers.WebDriverBuilder; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.Base64; + +/** + * Functional tests for {@link OperaOptions}. + */ +public class OperaOptionsFunctionalTest extends JupiterTestBase { + + @Test + @NoDriverBeforeTest + public void canStartOperaWithCustomOptions() { + OperaOptions options = new OperaOptions(); + options.addArguments("user-agent=foo;bar"); + localDriver = new WebDriverBuilder().get(options); + + localDriver.get(pages.clickJacker); + Object userAgent = + ((JavascriptExecutor) localDriver).executeScript("return window.navigator.userAgent"); + assertThat(userAgent).isEqualTo("foo;bar"); + } + + @Test + void optionsStayEqualAfterSerialization() { + OperaOptions options1 = new OperaOptions(); + OperaOptions options2 = new OperaOptions(); + assertThat(options2).isEqualTo(options1); + options1.asMap(); + assertThat(options2).isEqualTo(options1); + } + + @Test + @NoDriverBeforeTest + public void canSetAcceptInsecureCerts() { + OperaOptions options = new OperaOptions(); + options.setAcceptInsecureCerts(true); + localDriver = new WebDriverBuilder().get(options); + System.out.println(((HasCapabilities) localDriver).getCapabilities()); + + assertThat( + ((HasCapabilities) localDriver).getCapabilities().getCapability(ACCEPT_INSECURE_CERTS)) + .isEqualTo(true); + } +} diff --git a/java/test/org/openqa/selenium/opera/OperaOptionsTest.java b/java/test/org/openqa/selenium/opera/OperaOptionsTest.java new file mode 100644 index 0000000000000..e8ae8c5252fd6 --- /dev/null +++ b/java/test/org/openqa/selenium/opera/OperaOptionsTest.java @@ -0,0 +1,265 @@ +// 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.opera; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.LIST; +import static org.assertj.core.api.InstanceOfAssertFactories.MAP; +import static org.assertj.core.api.InstanceOfAssertFactories.STRING; +import static org.openqa.selenium.remote.Browser.OPERA; +import static org.openqa.selenium.remote.CapabilityType.ACCEPT_INSECURE_CERTS; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.ImmutableCapabilities; +import org.openqa.selenium.MutableCapabilities; +import org.openqa.selenium.PageLoadStrategy; +import org.openqa.selenium.remote.CapabilityType; +import org.openqa.selenium.testing.TestUtilities; + +@Tag("UnitTests") +class OperaOptionsTest { + + @Test + void testDefaultOptions() { + OperaOptions options = new OperaOptions(); + checkCommonStructure(options); + assertThat(options.asMap()) + .extracting(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .containsEntry("args", Collections.emptyList()) + .containsEntry("extensions", Collections.emptyList()); + } + + @Test + void canAddArguments() { + OperaOptions options = new OperaOptions(); + options.addArguments("--arg1", "--arg2"); + checkCommonStructure(options); + assertThat(options.asMap()) + .extracting(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .containsEntry("args", Arrays.asList("--arg1", "--arg2")) + .containsEntry("extensions", Collections.emptyList()); + } + + @Test + void canAddExtensions() throws IOException { + OperaOptions options = new OperaOptions(); + Path tmpDir = Files.createTempDirectory("webdriver"); + File ext1 = createTempFile(tmpDir, "ext1 content"); + File ext2 = createTempFile(tmpDir, "ext2 content"); + options.addExtensions(ext1, ext2); + checkCommonStructure(options); + assertThat(options.asMap()) + .extracting(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .containsEntry("args", Collections.emptyList()) + .containsEntry( + "extensions", + Stream.of("ext1 content", "ext2 content") + .map(s -> Base64.getEncoder().encodeToString(s.getBytes())) + .collect(Collectors.toList())); + } + + @Test + void canMergeWithoutChangingOriginalObject() { + OperaOptions options = new OperaOptions(); + OperaOptions merged = + options.merge( + new ImmutableCapabilities(CapabilityType.PAGE_LOAD_STRATEGY, PageLoadStrategy.NONE)); + assertThat(merged.getCapability(CapabilityType.PAGE_LOAD_STRATEGY)) + .isEqualTo(PageLoadStrategy.NONE); + } + + @Test + void mergingOptionsWithMutableCapabilities() { + File ext1 = TestUtilities.createTmpFile("ext1"); + String ext1Encoded = Base64.getEncoder().encodeToString("ext1".getBytes()); + String ext2 = Base64.getEncoder().encodeToString("ext2".getBytes()); + + MutableCapabilities one = new MutableCapabilities(); + + OperaOptions options = new OperaOptions(); + options.addArguments("verbose"); + options.addArguments("silent"); + options.setExperimentalOption("opt1", "val1"); + options.setExperimentalOption("opt2", "val4"); + options.addExtensions(ext1); + options.addEncodedExtensions(ext2); + options.setAcceptInsecureCerts(true); + File binary = TestUtilities.createTmpFile("binary"); + options.setBinary(binary); + + one.setCapability(OperaOptions.CAPABILITY, options); + + OperaOptions two = new OperaOptions(); + two.addArguments("verbose"); + two.setExperimentalOption("opt2", "val2"); + two.setExperimentalOption("opt3", "val3"); + + two = two.merge(one); + + Map map = two.asMap(); + + assertThat(map) + .asInstanceOf(MAP) + .extractingByKey(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .extractingByKey("args") + .asInstanceOf(LIST) + .containsExactly("verbose", "silent"); + + assertThat(map) + .asInstanceOf(MAP) + .extractingByKey(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .containsEntry("opt1", "val1") + .containsEntry("opt2", "val4") + .containsEntry("opt3", "val3"); + + assertThat(map) + .asInstanceOf(MAP) + .extractingByKey(ACCEPT_INSECURE_CERTS) + .isExactlyInstanceOf(Boolean.class); + + assertThat(map) + .asInstanceOf(MAP) + .extractingByKey(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .extractingByKey("extensions") + .asInstanceOf(LIST) + .containsExactly(ext1Encoded, ext2); + + assertThat(map) + .asInstanceOf(MAP) + .extractingByKey(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .extractingByKey("binary") + .asInstanceOf(STRING) + .isEqualTo(binary.getPath()); + } + + @Test + void mergingOptionsWithOptionsAsMutableCapabilities() { + File ext1 = TestUtilities.createTmpFile("ext1"); + String ext1Encoded = Base64.getEncoder().encodeToString("ext1".getBytes()); + String ext2 = Base64.getEncoder().encodeToString("ext2".getBytes()); + + MutableCapabilities browserCaps = new MutableCapabilities(); + + File binary = TestUtilities.createTmpFile("binary"); + + browserCaps.setCapability("binary", binary.getPath()); + browserCaps.setCapability("opt1", "val1"); + browserCaps.setCapability("opt2", "val4"); + browserCaps.setCapability("args", Arrays.asList("silent", "verbose")); + browserCaps.setCapability("extensions", Arrays.asList(ext1, ext2)); + + MutableCapabilities one = new MutableCapabilities(); + one.setCapability(OperaOptions.CAPABILITY, browserCaps); + + OperaOptions two = new OperaOptions(); + two.addArguments("verbose"); + two.setExperimentalOption("opt2", "val2"); + two.setExperimentalOption("opt3", "val3"); + two = two.merge(one); + + Map map = two.asMap(); + + assertThat(map) + .asInstanceOf(MAP) + .extractingByKey(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .extractingByKey("args") + .asInstanceOf(LIST) + .containsExactly("verbose", "silent"); + + assertThat(map).asInstanceOf(MAP).containsEntry("opt1", "val1"); + + assertThat(map).asInstanceOf(MAP).containsEntry("opt2", "val4"); + + assertThat(map) + .asInstanceOf(MAP) + .extractingByKey(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .containsEntry("opt2", "val2") + .containsEntry("opt3", "val3"); + + assertThat(map) + .asInstanceOf(MAP) + .extractingByKey(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .extractingByKey("extensions") + .asInstanceOf(LIST) + .containsExactly(ext1Encoded, ext2); + + assertThat(map) + .asInstanceOf(MAP) + .extractingByKey(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .extractingByKey("binary") + .asInstanceOf(STRING) + .isEqualTo(binary.getPath()); + } + + private void checkCommonStructure(OperaOptions options) { + assertThat(options.asMap()) + .containsEntry(CapabilityType.BROWSER_NAME, OPERA.browserName()) + .extracting(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .containsOnlyKeys("args", "extensions"); + } + + private File createTempFile(Path tmpDir, String content) { + try { + Path file = Files.createTempFile(tmpDir, "tmp", "ext"); + Files.writeString(file, content, Charset.defaultCharset()); + return file.toFile(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Test + void mergingOptionsMergesArguments() { + OperaOptions one = new OperaOptions().addArguments("verbose"); + OperaOptions two = new OperaOptions().addArguments("silent"); + OperaOptions merged = one.merge(two); + + assertThat(merged.asMap()) + .asInstanceOf(MAP) + .extractingByKey(OperaOptions.CAPABILITY) + .asInstanceOf(MAP) + .extractingByKey("args") + .asInstanceOf(LIST) + .containsExactly("verbose", "silent"); + } +} diff --git a/java/test/org/openqa/selenium/testing/drivers/BUILD.bazel b/java/test/org/openqa/selenium/testing/drivers/BUILD.bazel index 109fb9e20fac4..b7732adad6f3a 100644 --- a/java/test/org/openqa/selenium/testing/drivers/BUILD.bazel +++ b/java/test/org/openqa/selenium/testing/drivers/BUILD.bazel @@ -17,6 +17,7 @@ java_library( "//java/src/org/openqa/selenium/edge", "//java/src/org/openqa/selenium/firefox", "//java/src/org/openqa/selenium/ie", + "//java/src/org/openqa/selenium/opera", "//java/src/org/openqa/selenium/remote", "//java/src/org/openqa/selenium/safari", ], @@ -48,6 +49,7 @@ java_library( "//java/src/org/openqa/selenium/firefox", "//java/src/org/openqa/selenium/ie", "//java/src/org/openqa/selenium/json", + "//java/src/org/openqa/selenium/opera", "//java/src/org/openqa/selenium/remote", "//java/src/org/openqa/selenium/remote/http", "//java/src/org/openqa/selenium/safari", diff --git a/java/test/org/openqa/selenium/testing/drivers/Browser.java b/java/test/org/openqa/selenium/testing/drivers/Browser.java index 3037087237032..a708152566c5d 100644 --- a/java/test/org/openqa/selenium/testing/drivers/Browser.java +++ b/java/test/org/openqa/selenium/testing/drivers/Browser.java @@ -34,6 +34,8 @@ import org.openqa.selenium.firefox.GeckoDriverInfo; import org.openqa.selenium.ie.InternetExplorerDriverInfo; import org.openqa.selenium.ie.InternetExplorerOptions; +import org.openqa.selenium.opera.OperaDriverInfo; +import org.openqa.selenium.opera.OperaOptions; import org.openqa.selenium.safari.SafariDriverInfo; import org.openqa.selenium.safari.SafariOptions; @@ -144,6 +146,20 @@ public Capabilities getCapabilities() { return options; } }, + LEGACY_OPERA(new OperaOptions(), new OperaDriverInfo().getDisplayName(), false), + OPERA(new OperaOptions(), new OperaDriverInfo().getDisplayName(), false) { + @Override + public Capabilities getCapabilities() { + OperaOptions options = new OperaOptions(); + options.addArguments("disable-extensions"); + String operaPath = System.getProperty("webdriver.opera.binary"); + if (operaPath != null) { + options.setBinary(new File(operaPath)); + } + + return options; + } + }, SAFARI(new SafariOptions(), new SafariDriverInfo().getDisplayName(), false); private static final Logger LOG = Logger.getLogger(Browser.class.getName());