Add initial Element#shadow_root support

This commit is contained in:
Thomas Walpole 2022-05-05 11:32:50 -07:00
parent 780d578592
commit 836a4167a2
10 changed files with 95 additions and 1 deletions

View File

@ -9,6 +9,7 @@ Release date: unreleased
* [Beta] CSP nonces inserted into animation disabler additions - Issue #2542
* Support `<base>` element in rack-test driver - ISsue #2544
* [Beta] `Element#shadow_root` support. Requires selenium-webdriver 4.1+. Only currently supported with Chrome when using the selenium driver. Note: only CSS can be used to find elements inside the shadow dom so you won't be able to use most Capybara helper methods (`fill_in`, `click_link`, `find_field`, etc) since those locators are built using XPath. Stick to `find(:css, ...)` and direct interaction methods.
### Fixed

View File

@ -125,6 +125,10 @@ module Capybara
raise NotSupportedByDriverError, 'Capybara::Driver::Node#trigger'
end
def shadow_root
raise NotSupportedByDriverError, 'Capybara::Driver::Node#shadow_root'
end
def inspect
%(#<#{self.class} tag="#{tag_name}" path="#{path}">)
rescue NotSupportedByDriverError

View File

@ -472,6 +472,17 @@ module Capybara
self
end
##
#
# Return the shadow_root for the current element
#
# @return [Capybara::Node::Element] The shadow root
def shadow_root
root = synchronize { base.shadow_root }
root && Capybara::Node::Element.new(self.session, root, nil, nil)
end
##
#
# Execute the given JS in the context of the element not returning a result. This is useful for scripts that return

View File

@ -473,12 +473,16 @@ private
end
def unwrap_script_result(arg)
# TODO - move into the case when we drop support for Selenium < 4.1
element_types = [Selenium::WebDriver::Element]
element_types.push(Selenium::WebDriver::ShadowRoot) if defined?(Selenium::WebDriver::ShadowRoot)
case arg
when Array
arg.map { |arr| unwrap_script_result(arr) }
when Hash
arg.transform_values! { |value| unwrap_script_result(value) }
when Selenium::WebDriver::Element
when *element_types
build_node(arg)
else
arg

View File

@ -219,6 +219,13 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
native.rect
end
def shadow_root
raise_error "You must be using Selenium 4.1+ for shadow_root support" unless native.respond_to? :shadow_root
root = native.shadow_root
root && build_node(native.shadow_root)
end
protected
def scroll_if_needed

View File

@ -1176,6 +1176,29 @@ Capybara::SpecHelper.spec 'node' do
end
end
describe '#shadow_root', requires: %i[js] do
it 'should get the shadow root' do
@session.visit('/with_shadow')
expect do
shadow_root = @session.find(:css, '#shadow_host').shadow_root
expect(shadow_root).not_to be_nil
end.not_to raise_error
end
it 'should find elements inside the shadow dom using CSS' do
@session.visit('/with_shadow')
shadow_root = @session.find(:css, '#shadow_host').shadow_root
expect(shadow_root).to have_css('#shadow_content', text: 'some text')
end
it 'should find nested shadow roots' do
@session.visit('/with_shadow')
shadow_root = @session.find(:css, '#shadow_host').shadow_root
nested_shadow_root = shadow_root.find(:css, '#nested_shadow_host').shadow_root
expect(nested_shadow_root).to have_css('#nested_shadow_content', text: 'nested text')
end
end
describe '#reload', requires: [:js] do
it 'should reload elements found via ancestor with CSS' do
@session.visit('/with_js')

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<%# Borrowed from Titus Fortner %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Shadow DOM</title>
</head>
<body>
<div id="no_host"></div>
<div id="shadow_host"></div>
<a href="scroll.html">scroll.html</a>
<script>
let shadowRoot = document.getElementById('shadow_host').attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<span class="wrapper" id="shadow_content"><span class="info">some text</span></span>
<div id="nested_shadow_host"></div>
<a href="scroll.html">scroll.html</a>
<input type="text" />
<input type="checkbox" />
<input type="file" />
`;
let nestedShadowRoot = shadowRoot.getElementById('nested_shadow_host').attachShadow({mode: 'open'});
nestedShadowRoot.innerHTML = `
<div id="nested_shadow_content"><div>nested text</div></div>
`;
</script>
</body>
</html>

View File

@ -72,6 +72,10 @@ Capybara::SpecHelper.run_specs TestSessions::SeleniumFirefox, 'selenium', capyba
when 'Capybara::Session selenium #accept_alert should handle the alert if the page changes',
'Capybara::Session selenium #accept_alert with an asynchronous alert should accept the alert'
skip 'No clue what Firefox is doing here - works fine on MacOS locally'
when 'Capybara::Session selenium node #shadow_root should get the shadow root',
'Capybara::Session selenium node #shadow_root should find elements inside the shadow dom using CSS',
'Capybara::Session selenium node #shadow_root should find nested shadow roots'
pending "Firefox doesn't yet have W3C shadow root support"
end
end

View File

@ -84,6 +84,10 @@ Capybara::SpecHelper.run_specs TestSessions::Safari, SAFARI_DRIVER.to_s, capybar
when 'Capybara::Session selenium_safari #go_back should fetch a response from the driver from the previous page',
'Capybara::Session selenium_safari #go_forward should fetch a response from the driver from the previous page'
skip 'safaridriver loses the ability to find elements in the document after `go_back`'
when 'Capybara::Session selenium node #shadow_root should get the shadow root',
'Capybara::Session selenium node #shadow_root should find elements inside the shadow dom using CSS',
'Capybara::Session selenium node #shadow_root should find nested shadow roots'
pending "Safari doesn't yet have W3C shadow root support"
end
end

View File

@ -280,6 +280,13 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
expect(element).to eq session.find(:id, 'form_title')
end
it 'returns a shadow root' do
session.visit('/with_shadow')
shadow = session.find(:css, '#shadow_host')
element = session.evaluate_script("arguments[0].shadowRoot", shadow)
expect(element).to be_instance_of(Capybara::Node::Element)
end
it 'can return arrays of nested elements' do
session.visit('/form')
elements = session.evaluate_script('document.querySelectorAll("#form_city option")')