diff --git a/lib/capybara.rb b/lib/capybara.rb index 4ea806df8..5596afee9 100644 --- a/lib/capybara.rb +++ b/lib/capybara.rb @@ -132,6 +132,10 @@ def register_server(name, &block) servers[name.to_sym] = block end + def register_plugin(name, plugin) + plugins[name.to_sym] = plugin + end + ## # # Add a new selector to Capybara. Selectors can be used by various methods in Capybara @@ -190,6 +194,10 @@ def servers @servers ||= {} end + def plugins + @plugins ||= {} + end + # Wraps the given string, which should contain an HTML document or fragment # in a {Capybara::Node::Simple} which exposes all {Capybara::Node::Matchers}, # {Capybara::Node::Finders} and {Capybara::Node::DocumentMatchers}. This allows you to query diff --git a/lib/capybara/node/actions.rb b/lib/capybara/node/actions.rb index 640aad9e9..bfa09157c 100644 --- a/lib/capybara/node/actions.rb +++ b/lib/capybara/node/actions.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'capybara/node/pluginify' + module Capybara module Node module Actions @@ -173,7 +175,9 @@ def uncheck(locator = nil, **options) # @param from: [String] The id, Capybara.test_id atrtribute, name or label of the select box # # @return [Capybara::Node::Element] The option element selected - def select(value = nil, from: nil, **options) + def select(value = nil, from: nil, using: nil, **options) + return Capybara.plugins[using].select(self, value, from: from, **options) if using + el = from ? find_select_or_datalist_input(from, options) : self if el.respond_to?(:tag_name) && (el.tag_name == 'input') @@ -239,6 +243,8 @@ def attach_file(locator = nil, path, make_visible: nil, **options) # rubocop:dis end end + prepend ::Capybara::Node::Pluginify + private def find_select_or_datalist_input(from, options) diff --git a/lib/capybara/node/pluginify.rb b/lib/capybara/node/pluginify.rb new file mode 100644 index 000000000..2eb63eab0 --- /dev/null +++ b/lib/capybara/node/pluginify.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Capybara + module Node + module Pluginify + def self.prepended(mod) + mod.public_instance_methods.each do |method_name| + define_method method_name do |*args, using: nil, **options| + if using + plugin = Capybara.plugins[using] + raise ArgumentError, "Plugin not loaded: #{using}" unless plugin + raise NoMethodError, "Action not implemented in plugin: #{using}:#{method_name}" unless plugin.respond_to?(method_name) + plugin.send(method_name, self, *args, **options) + else + super(*args, **options) + end + end + end + end + end + end +end diff --git a/lib/capybara/plugins/select2.rb b/lib/capybara/plugins/select2.rb new file mode 100644 index 000000000..7e8e1e923 --- /dev/null +++ b/lib/capybara/plugins/select2.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Capybara + module Plugins + class Select2 + def select(scope, value, from: nil, **options) + select2 = if from + scope.find(:select2, 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('select2')][XPath.attr(:class).contains_word('select2-container')]) + end + select2.click + scope.find(:select2_option, value).click + end + end + end +end + +Capybara.add_selector(:select2) do + xpath do |locator, **options| + xpath = XPath.descendant(:select) + xpath = locate_field(xpath, locator, options) + xpath = xpath.next_sibling(:span)[XPath.attr(:class).contains_word('select2')][XPath.attr(:class).contains_word('select2-container')] + xpath + end +end + +Capybara.add_selector(:select2_option) do + xpath do |locator| + xpath = XPath.anywhere(:ul)[XPath.attr(:class).contains_word('select2-results__options')][XPath.attr(:id)] + xpath = xpath.descendant(:li)[XPath.attr(:role) == 'treeitem'] + xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil? + xpath + end +end + +Capybara.register_plugin(:select2, Capybara::Plugins::Select2.new) diff --git a/lib/capybara/spec/session/plugin_spec.rb b/lib/capybara/spec/session/plugin_spec.rb new file mode 100644 index 000000000..b9806f998 --- /dev/null +++ b/lib/capybara/spec/session/plugin_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'capybara/plugins/select2' + +Capybara::SpecHelper.spec 'Plugin', requires: [:js] do + before do + @session.visit('https://select2.org/appearance') + end + + it 'should raise if wrong plugin specified' do + expect do + @session.select 'Florida', from: 'Click this to focus the single select element', using: :select3 + end.to raise_error(ArgumentError, /Plugin not loaded/) + end + + it 'should raise if non-implemented action is called' do + expect do + @session.click_on('blah', using: :select2) + end.to raise_error(NoMethodError, /Action not implemented/) + end + + it 'should select an option' do + @session.select 'Florida', from: 'Click this to focus the single select element', using: :select2 + + 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: :select2 + @session.select 'California', from: 'Click this to focus the multiple select element', using: :select2 + + expect(@session).to have_select(multiple: true, selected: %w[Pennsylvania California], visible: false) + end + + it 'should work with id' do + @session.select 'Florida', from: 'id_label_single', using: :select2 + expect(@session).to have_field(type: 'select', with: 'FL', visible: false) + end + + it 'works without :from' do + @session.within(:css, 'div.s2-example:nth-of-type(2) p:first-child') do + @session.select 'Florida', using: :select2 + expect(@session).to have_field(type: 'select', with: 'FL', visible: false) + end + end +end