diff --git a/lib/capybara/node/pluginify.rb b/lib/capybara/node/pluginify.rb index 13f82820c0..0e1b9a23cc 100644 --- a/lib/capybara/node/pluginify.rb +++ b/lib/capybara/node/pluginify.rb @@ -6,15 +6,14 @@ module Pluginify def self.prepended(mod) mod.public_instance_methods.each do |method_name| define_method method_name do |*args, **options| - plugin_name = options.delete(:using) { |_using| session_options.default_plugin[method_name] } - if plugin_name + plugin_names = Array(options.delete(:using) { |_using| session_options.default_plugin[method_name] }) + plugin_names.reduce(false) do |memo, plugin_name| plugin = Capybara.plugins[plugin_name] raise ArgumentError, "Plugin not loaded: #{plugin_name}" unless plugin raise NoMethodError, "Action not implemented in plugin: #{plugin_name}:#{method_name}" unless plugin.respond_to?(method_name) - plugin.send(method_name, self, *args, **options) - else - super(*args, **options) - end + + memo || plugin.send(method_name, self, *args, **options) + end || super(*args, **options) end end end diff --git a/lib/capybara/plugins/react_select.rb b/lib/capybara/plugins/react_select.rb new file mode 100644 index 0000000000..1a5104c04a --- /dev/null +++ b/lib/capybara/plugins/react_select.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Capybara + module Plugins + class ReactSelect + def select(scope, value, **options) + sel = find_react_select(scope, value, options) + sel.click.assert_matches_selector(:css, '.select__control--is-focused') + scope.find(:react_select_option, value).click + end + + def unselect(scope, value, **options) + select = find_react_select(scope, value, options) + raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select.has_css?('.select__value-container--is-multi') + + scope.find(:css, '.select__multi-value', text: value).find(:css, '.select__multi-value__remove').click + end + + private + + def find_react_select(scope, value, from: nil, **options) + if from + scope.find(:react_select, from, options.merge(visible: false)) + else + select = scope.find(:option, value, options).ancestor(:css, 'select', visible: false) + select.find(:xpath, XPath.next_sibling(:span)[XPath.attr(:class).contains_word('react-select')][XPath.attr(:class).contains_word('react-select-container')]) + end + end + end + end +end + +Capybara.add_selector(:react_select) do + xpath do |locator, **_options| + XPath.css('.select__control')[ + XPath.following_sibling(:input)[XPath.attr(:name) == locator].or( + XPath.following_sibling(:div)[XPath.child(:input)[XPath.attr(:name) == locator]] + ) + ] + end +end + +Capybara.add_selector(:react_select_option) do + xpath do |locator| + xpath = XPath.anywhere(:div)[XPath.attr(:class).contains_word('select__menu')] + xpath = xpath.descendant(:div)[XPath.attr(:class).contains_word('select__option')] + xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil? + xpath + end +end + +Capybara.register_plugin(:react_select, Capybara::Plugins::ReactSelect.new) diff --git a/lib/capybara/plugins/select2.rb b/lib/capybara/plugins/select2.rb index d8e9fce403..c49786dd4c 100644 --- a/lib/capybara/plugins/select2.rb +++ b/lib/capybara/plugins/select2.rb @@ -12,6 +12,7 @@ def select(scope, value, **options) def unselect(scope, value, **options) select2 = find_select2(scope, value, options) raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select2.has_css?('.select2-selection--multiple') + select2.click option = scope.find(:select2_option, value) option[:"aria-selected"] == 'true' ? option.click : select2.click diff --git a/lib/capybara/spec/session/plugin_spec.rb b/lib/capybara/spec/session/plugin_spec.rb index d4eb5774fd..27f231e3d0 100644 --- a/lib/capybara/spec/session/plugin_spec.rb +++ b/lib/capybara/spec/session/plugin_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true require 'capybara/plugins/select2' +require 'capybara/plugins/react_select' -Capybara::SpecHelper.spec 'Plugin', requires: [:js] do +Capybara::SpecHelper.spec 'Plugin', requires: [:js], focus_: true do before do @session.visit('https://select2.org/appearance') end @@ -83,4 +84,44 @@ @session.select 'Miss', from: 'Title', using: nil expect(@session.find_field('Title').value).to eq('Miss') end + + context 'with react select 2' do + before do + @session.visit('https://react-select.com/home') + end + + it 'should select an option' do + @session.select 'Red', from: 'color', using: :react_select + expect(@session).to have_field('color', type: 'hidden', with: 'red') + end + + it 'should remain selected if called twice on a single select' do + @session.select 'Blue', from: 'color', using: :react_select + @session.select 'Blue', from: 'color', using: :react_select + expect(@session).to have_field('color', type: 'hidden', with: 'blue') + end + + it 'should work with multiple select' do + @session.within @session.first(:css, 'div.basic-multi-select') do + @session.select 'Green', from: 'colors', using: :react_select + @session.select 'Silver', from: 'colors', using: :react_select + expect(@session).to have_field('colors', with: 'green', type: 'hidden') + expect(@session).to have_field('colors', with: 'silver', type: 'hidden') + end + end + + it 'should unselect an option' do + @session.within @session.first(:css, 'div.basic-multi-select') do + @session.select 'Green', from: 'colors', using: :react_select + expect(@session).to have_field('colors', with: 'green', type: 'hidden') + @session.unselect 'Green', from: 'colors', using: :react_select + expect(@session).to have_no_field('colors', with: 'green', type: 'hidden') + end + end + + it 'should work with name' do + @session.select 'Purple', from: 'color', using: :react_select + expect(@session).to have_css('input[type=hidden][name=color]', visible: false) { |el| el.value == 'purple' } + end + end end