diff --git a/capybara.gemspec b/capybara.gemspec index f10cae0de..978f17526 100644 --- a/capybara.gemspec +++ b/capybara.gemspec @@ -38,6 +38,7 @@ Gem::Specification.new do |s| s.add_development_dependency('launchy', ['>= 2.0.4']) s.add_development_dependency('minitest') s.add_development_dependency('puma') + s.add_development_dependency('byebug') s.add_development_dependency('rake') s.add_development_dependency('rspec', ['>= 3.4.0']) s.add_development_dependency('selenium-webdriver', ['~>3.5']) diff --git a/lib/capybara/plugins/react_select.rb b/lib/capybara/plugins/react_select.rb new file mode 100644 index 000000000..41ba59b0e --- /dev/null +++ b/lib/capybara/plugins/react_select.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Capybara + module Plugins + class ReactSelect + def select(scope, value, **options) + select = find_react_select(scope, value, options).click + option = scope.find(:react_select_option, value) + option.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 select2.has_css?('.select2-selection--multiple') + select.click + option = scope.find(:react_select_option, value) + option.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]] + 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/spec/session/plugin_spec.rb b/lib/capybara/spec/session/plugin_spec.rb index d4eb5774f..07f6807f4 100644 --- a/lib/capybara/spec/session/plugin_spec.rb +++ b/lib/capybara/spec/session/plugin_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'capybara/plugins/select2' +require 'capybara/plugins/react_select' Capybara::SpecHelper.spec 'Plugin', requires: [:js] do before do @@ -83,4 +84,64 @@ @session.select 'Miss', from: 'Title', using: nil expect(@session.find_field('Title').value).to eq('Miss') end + + context "with react select 2", :focus_ do + before do + @session.visit('https://react-select.com/home') + end + + it 'should select an option' do + @session.select 'Florida', from: 'Click this to focus the single select element', using: :react_select + expect(@session).to have_field(type: 'select', with: 'FL', visible: false) + end + + it 'should remain selected if called twice on a single select' do + @session.select 'Florida', from: 'Click this to focus the single select element', using: :react_select + @session.select 'Florida', from: 'Click this to focus the single select element', using: :react_select + expect(@session).to have_field(type: 'select', with: 'FL', visible: false) + end + + it 'should work with multiple select' do + @session.select 'Pennsylvania', from: 'Click this to focus the multiple select element', using: :react_select + @session.select 'California', from: 'Click this to focus the multiple select element', using: :react_select + expect(@session).to have_select(multiple: true, selected: %w[Pennsylvania California], visible: false) + @session.unselect 'Pennsylvania', from: 'Click this to focus the multiple select element', using: :react_select + expect(@session).to have_select(multiple: true, selected: %w[California], visible: false) + @session.unselect 'California', from: 'Click this to focus the multiple select element', using: :react_select + expect(@session).to have_select(multiple: true, selected: %w[], visible: false) + end + + it 'should not reselect if already selected' do + @session.select 'Pennsylvania', from: 'Click this to focus the multiple select element', using: :react_select + @session.select 'Pennsylvania', from: 'Click this to focus the multiple select element', using: :react_select + expect(@session).to have_select(multiple: true, selected: %w[Pennsylvania], visible: false) + @session.unselect 'Pennsylvania', from: 'Click this to focus the multiple select element', using: :react_select + @session.unselect 'Pennsylvania', from: 'Click this to focus the multiple select element', using: :react_select + expect(@session).to have_select(multiple: true, selected: %w[], visible: false) + end + + it 'should work with id' do + @session.select 'Florida', from: 'id_label_single', using: :react_select + expect(@session).to have_field(type: 'select', with: 'FL', visible: false) + end + + it 'should work with name', :focus_ 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 + + it 'works without :from' do + @session.within(:css, 'div.s2-example:nth-of-type(2) p:first-child') do + @session.select 'Florida', using: :react_select + expect(@session).to have_field(type: 'select', with: 'FL', visible: false) + end + end + + it 'works when called on the select box' do + el = @session.find(:css, 'select#id_label_single', visible: false) + el.select 'Florida', using: :react_select + expect(@session).to have_field(type: 'select', with: 'FL', visible: false) + end + + end end