Implementation:Teamcapybara Capybara Firefox Node
Overview
Capybara::Selenium::FirefoxNode is a Firefox-specific Selenium node class that extends Capybara::Selenium::Node with workarounds and optimizations tailored to the geckodriver/Marionette browser automation interface. It addresses known Firefox quirks such as table row click failures, multi-file upload limitations in older versions, the :space symbol-to-string conversion for key events, native element display detection, and modifier key handling through action chains.
Source File
| Property | Value |
|---|---|
| File | lib/capybara/selenium/nodes/firefox_node.rb
|
| Lines | 136 |
| Language | Ruby |
| Parent Class | Capybara::Selenium::Node
|
| Modules Included | Html5Drag, FileInputClickEmulation
|
Class Definition
class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node include Html5Drag include FileInputClickEmulation end
The class includes two extension modules:
- Html5Drag -- provides HTML5 drag-and-drop support
- FileInputClickEmulation -- emulates click behavior on file input elements
Public Methods
click(keys = [], **options)
Performs a click on the element, delegating to the parent implementation via super. If an ElementNotInteractableError is raised and the element is a <tr> (table row), the method works around a known geckodriver/Marionette issue (geckodriver#1228) by emitting a warning and clicking the first cell (th:first-child or td:first-child) instead.
def click(keys = [], **options)
super
rescue ::Selenium::WebDriver::Error::ElementNotInteractableError
if tag_name == 'tr'
warn 'You are attempting to click a table row which has issues in geckodriver/marionette - ...'
return find_css('th:first-child,td:first-child')[0].click(keys, **options)
end
raise
end
disabled?
Returns whether the element matches the CSS :disabled pseudo-class or is a child of a disabled <select> element. Uses JavaScript evaluation rather than the standard WebDriver property check to handle Firefox-specific behavior.
def disabled?
driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self)
end
set_file(value)
Sets file input values with special handling for multiple file uploads. Clears any existing files on multi-file inputs before setting new values. For Firefox versions 62.0 and above, delegates to the parent implementation. For older versions, works around the lack of native multiple file upload support by uploading files one at a time via native.send_keys.
def set_file(value)
driver.execute_script(<<~JS, self)
if (arguments[0].multiple && arguments[0].files.length){
arguments[0].value = null;
}
JS
return super if browser_version >= 62.0
# Workaround for older versions: upload one file at a time
path_names = value.to_s.empty? ? [] : Array(value)
...
end
focused?
Returns whether the element is the currently active (focused) element in the document by comparing it against document.activeElement via JavaScript evaluation.
def focused?
driver.evaluate_script('arguments[0] == document.activeElement', self)
end
send_keys(*args)
Sends keystrokes to the element with Firefox-specific handling. Maps the :space symbol to a literal space character (' ') to work around geckodriver#846. When arrays are present in the arguments (indicating modifier key combinations), clicks the element to ensure focus and builds an action chain via the private _send_keys method.
def send_keys(*args)
return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none?(Array)
native.click unless focused?
_send_keys(args).perform
end
drop(*args)
Delegates to html5_drop (from the Html5Drag module) to perform HTML5 drag-and-drop file operations.
hover
Moves the mouse over the element. For Firefox versions 65.0 and above, works around Capybara#2156 by performing a two-step move: first to offset (0,0) of the element, then to the element center, ensuring reliable hover behavior after scrolling.
def hover
return super unless browser_version >= 65.0
scroll_if_needed { browser_action.move_to(native, 0, 0).move_to(native).perform }
end
select_option
Selects an option element, optimized to perform a single JavaScript check for whether the option is already selected or disabled (matching :disabled, select:disabled *, or :checked). Only clicks if the option is neither selected nor disabled.
def select_option
selected_or_disabled = driver.evaluate_script(<<~JS, self)
arguments[0].matches(':disabled, select:disabled *, :checked')
JS
click unless selected_or_disabled
end
visible?
Checks element visibility. When native display detection is enabled (and not disabled via environment variable), uses the Selenium :is_element_displayed command directly through the bridge for better performance. Falls back to the parent implementation if the command is unknown, and disables native display detection for future calls in that case.
def visible?
return super unless native_displayed?
begin
bridge.send(:execute, :is_element_displayed, id: native_id)
rescue Selenium::WebDriver::Error::UnknownCommandError
driver.options[:native_displayed] = false
super
end
end
Private Methods
native_displayed?
Returns true if native element display detection is enabled. Checks both the driver option :native_displayed and the DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS environment variable.
perform_with_options(click_options)
Overrides the parent method to scroll the element to the center of the viewport when click coordinates are specified, working around a Firefox/Marionette issue with clicking near viewport edges.
_send_keys(keys, actions, down_keys)
Recursively processes key arguments to build a Selenium action chain. Handles modifier keys (:control, :alt, :shift, :meta, :command and their left/right variants) by pressing them down and tracking them on a ModifierKeysStack. String arguments are uppercased when Shift is held on Firefox versions below 64.0 (workaround for Mozilla bug 1405370). Arrays are recursively processed, with modifier keys released in reverse order upon array completion.
upload(local_file)
Zips and uploads a local file to the remote Selenium server. Used for file input workarounds on older Firefox versions that lack native multi-file upload support.
browser_version
Returns the browser version as a float by reading from the driver's capabilities.
Key Design Decisions
- Table row click workaround -- Rather than failing on
<tr>clicks (a known geckodriver limitation), the class automatically retargets the click to the first cell with a deprecation-style warning. - Version-gated behavior -- Several methods branch based on
browser_version, allowing the class to apply workarounds only for Firefox versions that need them (e.g., multi-file upload below 62.0, hover fix at 65.0+, shift-uppercase below 64.0). - Native display optimization -- The
visible?method attempts a faster native display check before falling back to the standard approach, with graceful degradation if the command is unsupported. - Modifier key stack -- The
_send_keysmethod uses aModifierKeysStackto correctly track nested modifier key states across complex key sequences.
See Also
- Capybara::Selenium::SafariNode -- Safari-specific node implementation
Capybara::Selenium::Node-- Parent class providing base Selenium node functionalityCapybara::Selenium::Extensions::Html5Drag-- HTML5 drag-and-drop extensionCapybara::Selenium::Extensions::FileInputClickEmulation-- File input click emulation extension