From 8d30c00ede5b2df32cb657da26a785bad4c9186e Mon Sep 17 00:00:00 2001 From: Thomas Walpole Date: Tue, 5 Mar 2019 17:01:16 -0800 Subject: [PATCH 1/2] Implement multiple expression types for field selector type --- .../selector/definition/datalist_input.rb | 2 +- lib/capybara/selector/definition/field.rb | 77 +++++++++++++++++-- .../selector/definition/link_or_button.rb | 2 +- lib/capybara/selector/definition/select.rb | 2 +- 4 files changed, 75 insertions(+), 8 deletions(-) diff --git a/lib/capybara/selector/definition/datalist_input.rb b/lib/capybara/selector/definition/datalist_input.rb index f3210fe93..87c1af263 100644 --- a/lib/capybara/selector/definition/datalist_input.rb +++ b/lib/capybara/selector/definition/datalist_input.rb @@ -19,7 +19,7 @@ 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 diff --git a/lib/capybara/selector/definition/field.rb b/lib/capybara/selector/definition/field.rb index ce42b4096..d6120cc98 100644 --- a/lib/capybara/selector/definition/field.rb +++ b/lib/capybara/selector/definition/field.rb @@ -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| @@ -10,18 +10,85 @@ 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(:name) do |expr, val| + if default_format == :css + ::Capybara::Selector::CSSBuilder.new(expr).add_attribute_conditions(name: val) + else + expr[XPath.attr(:name) == val] + end + end + expression_filter(:placeholder) do |expr, val| + if default_format == :css + ::Capybara::Selector::CSSBuilder.new(expr).add_attribute_conditions(placeholder: val) + else + expr[XPath.attr(:placeholder) == val] + end + end + 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 diff --git a/lib/capybara/selector/definition/link_or_button.rb b/lib/capybara/selector/definition/link_or_button.rb index 27b8bd2f0..9cb471984 100644 --- a/lib/capybara/selector/definition/link_or_button.rb +++ b/lib/capybara/selector/definition/link_or_button.rb @@ -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 diff --git a/lib/capybara/selector/definition/select.rb b/lib/capybara/selector/definition/select.rb index 92582f555..1b2f28bfc 100644 --- a/lib/capybara/selector/definition/select.rb +++ b/lib/capybara/selector/definition/select.rb @@ -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 From b69a1f32d19f85ec521bc99c5b28251bb5462d7e Mon Sep 17 00:00:00 2001 From: Thomas Walpole Date: Sun, 23 Feb 2020 10:25:04 -0800 Subject: [PATCH 2/2] Implement multiple expression types for fillable_field selector type and add some tests --- .../selector/definition/datalist_input.rb | 4 +- lib/capybara/selector/definition/field.rb | 14 ---- .../selector/definition/fillable_field.rb | 46 +++++++++++- lib/capybara/spec/session/shadow_spec.rb | 72 +++++++++++++++++++ spec/rack_test_spec.rb | 1 + spec/selenium_spec_chrome.rb | 2 +- 6 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 lib/capybara/spec/session/shadow_spec.rb diff --git a/lib/capybara/selector/definition/datalist_input.rb b/lib/capybara/selector/definition/datalist_input.rb index 87c1af263..f39474385 100644 --- a/lib/capybara/selector/definition/datalist_input.rb +++ b/lib/capybara/selector/definition/datalist_input.rb @@ -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, format: :xpath)].attr(:id)] + xpath[ + XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option, format: :xpath) + ].attr(:id)] end end diff --git a/lib/capybara/selector/definition/field.rb b/lib/capybara/selector/definition/field.rb index d6120cc98..bb6c16ae5 100644 --- a/lib/capybara/selector/definition/field.rb +++ b/lib/capybara/selector/definition/field.rb @@ -62,20 +62,6 @@ filter_set(:_field) # checked/unchecked/disabled/multiple/name/placeholder - expression_filter(:name) do |expr, val| - if default_format == :css - ::Capybara::Selector::CSSBuilder.new(expr).add_attribute_conditions(name: val) - else - expr[XPath.attr(:name) == val] - end - end - expression_filter(:placeholder) do |expr, val| - if default_format == :css - ::Capybara::Selector::CSSBuilder.new(expr).add_attribute_conditions(placeholder: val) - else - expr[XPath.attr(:placeholder) == val] - end - end expression_filter(:readonly, :boolean, format: :css) do |expr, val| ::Capybara::Selector::CSS.split(expr).map do |css_fragment| if val diff --git a/lib/capybara/selector/definition/fillable_field.rb b/lib/capybara/selector/definition/fillable_field.rb index f04defd33..f86971476 100644 --- a/lib/capybara/selector/definition/fillable_field.rb +++ b/lib/capybara/selector/definition/fillable_field.rb @@ -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 diff --git a/lib/capybara/spec/session/shadow_spec.rb b/lib/capybara/spec/session/shadow_spec.rb new file mode 100644 index 000000000..7c25f1e02 --- /dev/null +++ b/lib/capybara/spec/session/shadow_spec.rb @@ -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 diff --git a/spec/rack_test_spec.rb b/spec/rack_test_spec.rb index 0395d2168..cc60915a1 100644 --- a/spec/rack_test_spec.rb +++ b/spec/rack_test_spec.rb @@ -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] diff --git a/spec/selenium_spec_chrome.rb b/spec/selenium_spec_chrome.rb index be630869a..05990f531 100644 --- a/spec/selenium_spec_chrome.rb +++ b/spec/selenium_spec_chrome.rb @@ -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']