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

Initial implementation of multiple expression types for :field selector #2163

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/capybara/selector/definition/datalist_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@

expression_filter(:with_options) do |expr, options|
options.inject(expr) do |xpath, option|
xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option)].attr(:id)]
xpath[
XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option, format: :xpath)
].attr(:id)]
end
end

Expand Down
63 changes: 58 additions & 5 deletions lib/capybara/selector/definition/field.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

Capybara.add_selector(:field, locator_type: [String, Symbol]) do
Capybara.add_selector(:field, locator_type: [String, Symbol], supports_exact: true) do
visible { |options| :hidden if options[:type].to_s == 'hidden' }

xpath do |locator, **options|
Expand All @@ -10,18 +10,71 @@
locate_field(xpath, locator, **options)
end

css do |_locator, **options|
invalid_types = %w[submit image]
invalid_types << 'hidden' unless options[:type].to_s == 'hidden'
invalid_attributes = invalid_types.map { |type| ":not([type=#{type}])" }.join
"input#{invalid_attributes}, textarea, select"
end

locator_filter(skip_if: nil, format: :css) do |node, locator, exact:, **_|
optional_checks = +''
optional_checks << "(field.getAttribute('aria-label') == locator)||" if enable_aria_label
optional_checks << "(field.getAttribute('#{test_id}') == locator)||" if test_id

match_js = <<~JS
(function(field, locator){
return (
(field.id == locator) ||
(field.name == locator) ||
(field.placeholder == locator)||
#{optional_checks}
Array.from(field.labels || []).some(function(label){
return label.innerText#{exact ? '== locator' : '.includes(locator)'};
})
);
})(this, arguments[0])
JS
node.evaluate_script(match_js, locator)
end

expression_filter(:type) do |expr, type|
type = type.to_s
if %w[textarea select].include?(type)
expr.self(type.to_sym)
case default_format
when :css
if %w[textarea select].include?(type)
::Capybara::Selector::CSS.split(expr).select do |css_fragment|
css_fragment.start_with? type
end.join(',')
else
::Capybara::Selector::CSSBuilder.new(expr).add_attribute_conditions(type: type)
end
when :xpath
if %w[textarea select].include?(type)
expr.self(type.to_sym)
else
expr[XPath.attr(:type) == type]
end
else
expr[XPath.attr(:type) == type]
raise ArgumentError, "Unknown format type: #{default_format}"
end
end

filter_set(:_field) # checked/unchecked/disabled/multiple/name/placeholder

node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
expression_filter(:readonly, :boolean, format: :css) do |expr, val|
::Capybara::Selector::CSS.split(expr).map do |css_fragment|
if val
"#{css_fragment}:read-only"
else
"#{css_fragment}:read-write"
end
end.join(',')
end

node_filter(:readonly, :boolean, format: :xpath) do |node, value|
!(value ^ node.readonly?)
end

node_filter(:with) do |node, with|
val = node.value
Expand Down
46 changes: 43 additions & 3 deletions lib/capybara/selector/definition/fillable_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,52 @@
locate_field(xpath, locator, **options)
end

css do |_locator, **_options|
invalid_types = %w[submit image radio checkbox hidden file]
invalid_attributes = invalid_types.map { |type| ":not([type=#{type}])" }.join
"input#{invalid_attributes}, textarea"
end

locator_filter(skip_if: nil, format: :css) do |node, locator, exact:, **_|
optional_checks = +''
optional_checks << "(field.getAttribute('aria-label') == locator)||" if enable_aria_label
optional_checks << "(field.getAttribute('#{test_id}') == locator)||" if test_id

match_js = <<~JS
(function(field, locator){
return (
(field.id == locator) ||
(field.name == locator) ||
(field.placeholder == locator)||
#{optional_checks}
Array.from(field.labels || []).some(function(label){
return label.innerText#{exact ? '== locator' : '.includes(locator)'};
})
);
})(this, arguments[0])
JS
node.evaluate_script(match_js, locator)
end

expression_filter(:type) do |expr, type|
type = type.to_s
if type == 'textarea'
expr.self(type.to_sym)
case default_format
when :css
if type == 'textarea'
::Capybara::Selector::CSS.split(expr).select do |css_fragment|
css_fragment.start_with? type
end.join(',')
else
::Capybara::Selector::CSSBuilder.new(expr).add_attribute_conditions(type: type)
end
when :xpath
if type == 'textarea'
expr.self(type.to_sym)
else
expr[XPath.attr(:type) == type]
end
else
expr[XPath.attr(:type) == type]
raise ArgumentError, "Unknown format type: #{default_format}"
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/capybara/selector/definition/link_or_button.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
label 'link or button'
xpath do |locator, **options|
%i[link button].map do |selector|
expression_for(selector, locator, **options)
expression_for(selector, locator, format: :xpath, **options)
end.reduce(:union)
end

Expand Down
2 changes: 1 addition & 1 deletion lib/capybara/selector/definition/select.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

expression_filter(:with_options) do |expr, options|
options.inject(expr) do |xpath, option|
xpath[expression_for(:option, option)]
xpath[expression_for(:option, option, format: :xpath)]
end
end

Expand Down
72 changes: 72 additions & 0 deletions lib/capybara/spec/session/shadow_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

Capybara::SpecHelper.spec 'shadow', requires: %i[js shadow] do
before { @session.visit('/form') }

it 'can check for fields in the shadow dom' do
form = @session.first(:css, 'form')
shadow = @session.evaluate_script <<~JS, form
(function(form){
var shadow_host = document.createElement('div');
var shadow = shadow_host.attachShadow({mode: 'open'});
shadow.appendChild(form);
document.documentElement.appendChild(shadow_host);
return shadow;
}).apply(this, arguments)
JS

expect(shadow).to have_field('Dog', selector_format: :css)
expect(shadow).not_to have_field('Monkey', selector_format: :css)

expect(shadow).to have_field('First Name', with: 'John', selector_format: :css)
expect(shadow).to have_field('First Name', with: /^Joh/, selector_format: :css)
expect(shadow).not_to have_field('Random', with: 'John', selector_format: :css)

expect(shadow).not_to have_field('First Name', with: 'Peter', selector_format: :css)
expect(shadow).not_to have_field('First Name', with: /eter$/, selector_format: :css)

shadow.fill_in('First Name', with: 'Jonas', selector_format: :css, fill_options: { clear: :backspace })
expect(shadow).to have_field('First Name', with: 'Jonas', selector_format: :css)
expect(shadow).to have_field('First Name', with: /ona/, selector_format: :css)

shadow.fill_in('First Name', with: 'Jonas', selector_format: :css, fill_options: { clear: :backspace })
expect(shadow).not_to have_field('First Name', with: 'John', selector_format: :css)
expect(shadow).not_to have_field('First Name', with: /John|Paul|George|Ringo/, selector_format: :css)

# shadow.fill_in('First Name', with: 'Thomas', selector_format: :css, fill_options: { clear: :backspace})
# expect do
# expect(shadow).to have_field('First Name', with: 'Jonas', selector_format: :css)
# end.to raise_exception(RSpec::Expectations::ExpectationNotMetError, /Expected value to be "Jonas" but was "Thomas"/)
#
# expect do
# expect(shadow).to have_field('First Name', readonly: true, selector_format: :css)
# end.to raise_exception(RSpec::Expectations::ExpectationNotMetError, /Expected readonly true but it wasn't/)
#
# # inherited boolean node filter
# expect do
# expect(shadow).to have_field('form_pets_cat', checked: true, selector_format: :css)
# end.to raise_exception(RSpec::Expectations::ExpectationNotMetError, /Expected checked true but it wasn't/)
#
# expect(shadow).to have_field('First Name', type: 'text', selector_format: :css)
#
# expect(shadow).not_to have_field('First Name', type: 'textarea', selector_format: :css)
#
# expect(shadow).to have_field('form[data]', with: 'TWTW', type: 'hidden', selector_format: :css)
#
# expect(shadow).to have_field('Html5 Multiple Email', multiple: true, selector_format: :css)
#
# expect(shadow).not_to have_field('Html5 Multiple Email', multiple: false, selector_format: :css)
#
# shadow.fill_in 'required', with: 'something', selector_format: :css, fill_options: { clear: :backspace}
# shadow.fill_in 'length', with: 'abcd', selector_format: :css, fill_options: { clear: :backspace}
#
# expect(shadow).to have_field('required', valid: true, selector_format: :css)
# expect(shadow).to have_field('length', valid: true, selector_format: :css)
#
# expect(shadow).not_to have_field('required', valid: true, selector_format: :css)
# expect(shadow).to have_field('required', valid: false, selector_format: :css)
#
# shadow.fill_in 'length', with: 'abc', selector_format: :css, fill_options: { clear: :backspace}
# expect(shadow).not_to have_field('length', valid: true, selector_format: :css)
end
end
1 change: 1 addition & 0 deletions spec/rack_test_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module TestSessions
css
scroll
spatial
shadow
]
Capybara::SpecHelper.run_specs TestSessions::RackTest, 'RackTest', capybara_skip: skipped_tests do |example|
case example.metadata[:full_description]
Expand Down
2 changes: 1 addition & 1 deletion spec/selenium_spec_chrome.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ module TestSessions
Chrome = Capybara::Session.new(CHROME_DRIVER, TestApp)
end

skipped_tests = %i[response_headers status_code trigger]
skipped_tests = %i[response_headers status_code trigger shadow]

Capybara::SpecHelper.log_selenium_driver_version(Selenium::WebDriver::Chrome) if ENV['CI']

Expand Down