SeleniumHQ / selenium

A browser automation framework and ecosystem.
https://selenium.dev
Apache License 2.0
30.15k stars 8.1k forks source link

[🚀 Feature]: Language bindings for Firefox Prefs JS API #14266

Open MatzFan opened 1 month ago

MatzFan commented 1 month ago

Feature and motivation

I would like to be able to get the value of a preference set in about:config dynamically. These are set when instantiating a driver via Options[:prefs] values). I'd also like to be able to dynamically set a preference.

The JS API for Firefox prefs seems straightforward. I do note that Firefox CDP is being deprecated IFO BiDi, could someone confirm whether this JS API is part of that scope or not?

Usage example

From your own Ruby tests I see here that you get a preference value like so, by passing a JS string to the Driver#execute_script method.

dir  = driver.execute_script("return Services.prefs.getStringPref('browser.download.dir')")

Something like this would be much better:

driver.pref['browser.download.dir']
# => ""

And the equivalent to set a pref value:

driver.pref['browser.download.dir'] = 'foo/bar'
driver.pref['browser.download.dir']
# => "/foo/bar"
github-actions[bot] commented 1 month ago

@MatzFan, thank you for creating this issue. We will troubleshoot it as soon as we can.


Info for maintainers

Triage this issue by using labels.

If information is missing, add a helpful comment and then I-issue-template label.

If the issue is a question, add the I-question label.

If the issue is valid but there is no time to troubleshoot it, consider adding the help wanted label.

If the issue requires changes or fixes from an external project (e.g., ChromeDriver, GeckoDriver, MSEdgeDriver, W3C), add the applicable G-* label, and it will provide the correct link and auto-close the issue.

After troubleshooting the issue, please add the R-awaiting answer label.

Thank you!

MatzFan commented 1 month ago

So as a proof of concept I've created a Ruby module to do this. It can get and set prefs exactly as set out above. I've omitted the tests here for brevity, please let me know if you are interested in supporting this - or if I am reinventing a wheel!

module Selenium
  module WebDriver
    module Firefox
      # representation of a Firefox pref API: https://firefox-source-docs.mozilla.org/devtools/preferences.html
      class Preference
        PREF_TYPES = { 'PREF_BOOL' => 128, 'PREF_INT' => 64, 'PREF_INVALID' => 0, 'PREF_STRING' => 32 }.freeze
        GET_METHODS = { 0 => 'getStringPref', 32 => 'getStringPref', 64 => 'getIntPref', 128 => 'getBoolPref' }.freeze
        SET_METHODS = { 32 => 'setStringPref', 64 => 'setIntPref', 128 => 'setBoolPref' }.freeze

        def initialize(driver)
          @driver = driver
        end

        def [](str)
          execute_script_and_preserve_context str
        end

        def []=(str, val)
          execute_script_and_preserve_context str, val
        end

        private

        def execute_script_and_preserve_context(str, val = nil)
          @driver.in_chrome_context { @driver.execute_script(script_string(str, val)) }
        end

        def script_string(str, val)
          val ? setter_script(str, val) : getter_script(str)
        end

        def getter_script(str)
          type = pref_type(str)
          "return Services.prefs.#{GET_METHODS[type]}('#{str}'#{default(type)})"
        end

        def default(type)
          type.zero? ? ", ''" : '' # empty string default arg if pref not found, otherwise no default arg
        end

        def setter_script(str, val)
          "Services.prefs.#{set_method(str, val)}('#{str}', #{quote_str(val)})"
        end

        def set_method(str, val)
          type = pref_type(str)
          return SET_METHODS[type] unless type.zero?
          return 'setBoolPref' if val.instance_of?(TrueClass) || val.instance_of?(FalseClass)
          return 'setIntPref' if val.is_a? Integer

          'setStringPref' # everthing else gets set as a string
        end

        def pref_type(string)
          @driver.execute_script("return Services.prefs.getPrefType('#{string}')")
        end

        def quote_str(value)
          value.is_a?(String) ? "'#{value}'" : value
        end
      end

      # adds Driver#pref method and [] and []= methods on that object
      module FirefoxPrefs
        def pref
          in_chrome_context { Preference.new(self) }
        end

        def in_chrome_context
          old_context = context
          self.context = 'chrome'
          yield
        ensure
          self.context = old_context
        end
      end

      class Driver
        prepend FirefoxPrefs
      end
    end
  end
end
MatzFan commented 1 month ago

FWIW here is an example of a Python project using Selenium which uses the Firefox Prefs JS API to set driver prefs.

diemol commented 1 month ago

We are looking for projects to host on https://github.com/seleniumhq-community/, if you wish to work on this, it would be a great plugin that can be added as a dependency by any user. Would you be interested in doing that?

MatzFan commented 1 month ago

@diemol if you are not interested in incorporating such functionality directly into Selenium (it adds a dependency on the Firefox Prefs JS API) I was planning to simply release my code as a Ruby gem. Are Selenium Community repos simply mirrors of contributors' projects? If so I see no reason why I wouldn't set up a mirror there - and also for my Selenium Tor Browser project too. Can you point me to the docs on Selenium Community?

diemol commented 1 month ago

Selenium is meant to be a base library where users can build things on top. This use case is the perfect example of that.

The community org is meant to host projects driven and owned by the community, with the advantage of using the Selenium name to promote them. We don't have any formal docs because we are getting started. If you are interested, please join our Slack workspace, and we can chat about this.