Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resurrect driver for Chromium-based Opera browser #15166

Draft
wants to merge 2 commits into
base: trunk
Choose a base branch
from

Conversation

sbabcoc
Copy link
Contributor

@sbabcoc sbabcoc commented Jan 27, 2025

User description

Thanks for contributing to Selenium!
A PR well described will help maintainers to quickly review and merge it

Before submitting your PR, please check our contributing guidelines.
Avoid large PRs, help reviewers by making them as simple and short as possible.

Description

Motivation and Context

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • I have read the contributing document.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

PR Type

Enhancement, Tests


Description

  • Reintroduced support for Chromium-based Opera browser.

  • Added new classes for Opera-specific driver functionality.

  • Implemented tests for Opera driver and options.

  • Updated build and configuration files to include Opera support.


Changes walkthrough 📝

Relevant files
Enhancement
7 files
AddHasCasting.java
Added casting capabilities for Opera browser.                       
+57/-0   
AddHasCdp.java
Added CDP (Chrome DevTools Protocol) support for Opera.   
+48/-0   
OperaDriver.java
Implemented OperaDriver for Chromium-based Opera browser.
+97/-0   
OperaDriverInfo.java
Added driver information for Opera browser.                           
+84/-0   
OperaDriverService.java
Added service management for OperaDriver.                               
+300/-0 
OperaOptions.java
Added Opera-specific options for driver configuration.     
+70/-0   
Browser.java
Added Opera browser support in testing drivers.                   
+16/-0   
Tests
3 files
WindowSwitchingTest.java
Updated window switching tests to include Opera.                 
+5/-0     
OperaDriverServiceTest.java
Added unit tests for OperaDriverService.                                 
+52/-0   
OperaOptionsFunctionalTest.java
Added functional tests for OperaOptions.                                 
+58/-0   
Configuration changes
5 files
Rakefile
Updated Rakefile to include Opera-specific tasks.               
+4/-0     
BUILD.bazel
Included Opera module in Selenium build configuration.     
+1/-0     
BUILD.bazel
Added Opera support in Selenium Grid build configuration.
+1/-0     
BUILD.bazel
Created build configuration for Opera module.                       
+25/-0   
BUILD.bazel
Updated testing drivers build configuration to include Opera.
+2/-0     

Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.
  • Copy link
    Contributor

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
    🧪 PR contains tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Code Duplication

    The OperaDriverService class contains significant code duplication with ChromiumDriverService. Consider extracting common functionality to a shared base class or utility methods.

    /**
     * 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<String> args,
                                Map<String, String> 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<String> args,
                                Map<String, String> 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<String> createArgs() {
          if (getLogFile() == null) {
            String logFilePath = System.getProperty(OPERA_DRIVER_LOG_PROPERTY);
            if (logFilePath != null) {
              withLogFile(new File(logFilePath));
            }
          }
    
          List<String> 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<String> args, Map<String, String> environment) {
          try {
            return new OperaDriverService(exe, port, timeout, args, environment);
          } catch (IOException e) {
            throw new WebDriverException(e);
          }
        }
      }
    }
    Error Handling

    The error handling in the generateExecutor method could be improved by adding more specific error messages and exception types rather than relying on generic WebDriverException.

    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);
    }

    @sbabcoc sbabcoc marked this pull request as draft January 27, 2025 00:03
    Copy link
    Contributor

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Score
    Learned
    best practice
    Enhance parameter validation with more descriptive error messages to improve debugging experience

    Add null validation checks for the service, options and clientConfig parameters in
    the generateExecutor method to prevent potential NullPointerException. While
    Require.nonNull() is used, the error messages could be more descriptive.

    java/src/org/openqa/selenium/opera/OperaDriver.java [67-71]

     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);
    +  Require.nonNull("Driver service", service, "OperaDriverService must not be null");
    +  Require.nonNull("Driver options", options, "OperaOptions must not be null");
    +  Require.nonNull("Driver clientConfig", clientConfig, "ClientConfig must not be null");
       DriverFinder finder = new DriverFinder(service, options);
    • Apply this suggestion
    6
    Possible issue
    Improve null parameter validation

    The generateExecutor() method should validate the clientConfig parameter before
    using it to avoid potential NullPointerException. Add null check at the beginning of
    the method.

    java/src/org/openqa/selenium/opera/OperaDriverService.java [67-71]

     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);
    +  clientConfig = Require.nonNull("Client config", clientConfig);
    +  service = Require.nonNull("Driver service", service);
    +  options = Require.nonNull("Driver options", options);
    • Apply this suggestion
    Suggestion importance[1-10]: 3

    Why: The suggestion proposes reordering null checks, but the existing code already has proper null validation using Require.nonNull(). The change would not significantly improve the code's robustness.

    3

    @sbabcoc sbabcoc force-pushed the pr/resurrect-opera-driver branch 2 times, most recently from 6eafc67 to 2c6c996 Compare January 27, 2025 01:31
    @diemol
    Copy link
    Member

    diemol commented Jan 27, 2025

    Why is this needed? Does it support W3C now? operasoftware/operachromiumdriver#88

    @sbabcoc
    Copy link
    Contributor Author

    sbabcoc commented Jan 31, 2025

    Yes, but the capabilities need to explicitly activate the protocol. Bazel integration is not working very well in IntelliJ on my Windows 10 machine, which has prevented me from getting the Selenium project to sync properly. If I can get this driver to work reliably, I'll remove "draft" status from this PR and open it up for review.

    @diemol
    Copy link
    Member

    diemol commented Jan 31, 2025

    Why can't you use ChromeOptions to set the capabilities together with the browser driver and browser binary location?

    @sbabcoc
    Copy link
    Contributor Author

    sbabcoc commented Feb 1, 2025

    I think I could use ChromeOptions, but this adds more complexity to the process of instantiating sessions and supplying Opera sessions from Grid.

    @sbabcoc sbabcoc force-pushed the pr/resurrect-opera-driver branch 3 times, most recently from a81e95e to 2761540 Compare February 1, 2025 20:53
    @sbabcoc sbabcoc force-pushed the pr/resurrect-opera-driver branch from 2761540 to 8ebaae3 Compare February 5, 2025 05:37
    @diemol
    Copy link
    Member

    diemol commented Feb 5, 2025

    You can use ChromeOptions and also configure a stereotype in the Grid for the Opera browser.

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    2 participants