diff --git a/.yard/templates_custom/default/class/html/selectors.erb b/.yard/templates_custom/default/class/html/selectors.erb
new file mode 100644
index 00000000..50037ff5
--- /dev/null
+++ b/.yard/templates_custom/default/class/html/selectors.erb
@@ -0,0 +1,33 @@
+
+
Built-in Selectors
+
+ <% @selectors.each do |name, selector| %>
+ -
+
:<%= name %>
+
+ <% end %>
+
+
diff --git a/.yard/templates_custom/default/class/html/setup.rb b/.yard/templates_custom/default/class/html/setup.rb
new file mode 100644
index 00000000..91fbec56
--- /dev/null
+++ b/.yard/templates_custom/default/class/html/setup.rb
@@ -0,0 +1,17 @@
+def init
+ super
+ sections.place(:builtins).before(:subclasses)
+end
+
+def builtins
+ return if object.path != "Capybara::Selector" # only show built-in selectors for Selector class
+
+ @selectors = Registry.all(:selector)
+ return if @selectors.nil? || @selectors.empty?
+
+ @selectors = @selectors.map do |selector|
+ [selector.name, selector]
+ end
+
+ erb(:selectors)
+end
\ No newline at end of file
diff --git a/.yard/yard_extensions.rb b/.yard/yard_extensions.rb
new file mode 100644
index 00000000..64f76506
--- /dev/null
+++ b/.yard/yard_extensions.rb
@@ -0,0 +1,81 @@
+YARD::Templates::Engine.register_template_path Pathname.new('./.yard/templates_custom')
+
+YARD::Tags::Library.define_tag "Locator", :locator
+YARD::Tags::Library.define_tag "Filter", :filter, :with_types_and_name
+
+class SelectorObject < YARD::CodeObjects::Base
+ def path
+ "__Capybara" + sep + super
+ end
+end
+
+class AddSelectorHandler < YARD::Handlers::Ruby::Base
+ handles method_call(:add_selector)
+ namespace_only
+ process do
+ name = statement.parameters.first.jump(:tstring_content, :ident).source
+ # object = YARD::CodeObjects::MethodObject.new(namespace, name.to_sym)
+ # object = SelectorObject.new(YARD::Registry.resolve(P("Capybara"), "#add_selector", false, true), name.to_sym)
+ object = SelectorObject.new(namespace, name)
+ register(object)
+ parse_block(statement.last.last, :owner => object)
+
+ # modify the object
+ object.dynamic = true
+
+ # add custom metadata to the object
+ object['custom_field'] = 'Generated by add_selector'
+ end
+end
+
+class AddExpressionFilterHandler < YARD::Handlers::Ruby::Base
+ handles method_call(:xpath)
+ handles method_call(:css)
+
+ process do
+ return unless owner.is_a?(SelectorObject)
+ return if statement.parameters.empty?
+ # names = statement.parameters.children.map { |p| p.jump(:tstring_content, :ident).source.sub(/^:/, '') }
+ names = statement.parameters.children.map &:source
+ current_names = owner.tags(:filter).map(&:name)
+ (names-current_names).each do |name|
+ owner.add_tag(YARD::Tags::Tag.new(:filter, nil, nil, name))
+ end
+ end
+end
+
+class AddFilterHandler < YARD::Handlers::Ruby::Base
+ handles method_call(:filter)
+
+ process do
+ return unless owner.is_a?(SelectorObject)
+ return if statement.parameters.empty?
+ name = statement.parameters.first.source
+ type = if statement.parameters[1] && statement.parameters[1].source == ':boolean'
+ 'Boolean'
+ else
+ nil
+ end
+ if owner.tags(:filter).none? {|tag| tag.name == name }
+ filter_tag = YARD::Tags::Tag.new(:filter, nil, type, name)
+ owner.add_tag(filter_tag)
+ end
+ end
+end
+
+class FilterSetHandler < YARD::Handlers::Ruby::Base
+ handles method_call(:filter_set)
+
+ process do
+ return unless owner.is_a?(SelectorObject)
+ return if statement.parameters.empty? || !statement.parameters[1]
+
+ names = statement.parameters[1].flatten.map { |name| ":#{name}" }
+ names.each do |name|
+ if owner.tags(:filter).none? {|tag| tag.name == name }
+ filter_tag = YARD::Tags::Tag.new(:filter, nil, nil, name)
+ owner.add_tag(filter_tag)
+ end
+ end
+ end
+end
diff --git a/.yardopts b/.yardopts
new file mode 100644
index 00000000..6b4ed802
--- /dev/null
+++ b/.yardopts
@@ -0,0 +1 @@
+--load ./.yard/yard_extensions.rb
diff --git a/Rakefile b/Rakefile
index 51736c94..280cbc04 100644
--- a/Rakefile
+++ b/Rakefile
@@ -2,6 +2,7 @@ require 'rubygems'
require 'rspec/core/rake_task'
require 'cucumber/rake/task'
require 'yard'
+require_relative './.yard/yard_extensions'
desc "Run all examples"
RSpec::Core::RakeTask.new(:spec) do |t|
diff --git a/lib/capybara/rspec/features.rb b/lib/capybara/rspec/features.rb
index 1b8ebbc4..8b405763 100644
--- a/lib/capybara/rspec/features.rb
+++ b/lib/capybara/rspec/features.rb
@@ -39,7 +39,6 @@ else
end
end
-
def self.feature(*args, &block)
options = if args.last.is_a?(Hash) then args.pop else {} end
options[:capybara_feature] = true
diff --git a/lib/capybara/selector.rb b/lib/capybara/selector.rb
index 72b7e7e9..c6ed2927 100644
--- a/lib/capybara/selector.rb
+++ b/lib/capybara/selector.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
require 'capybara/selector/selector'
-
Capybara::Selector::FilterSet.add(:_field) do
filter(:checked, :boolean) { |node, value| not(value ^ node.checked?) }
filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) }
@@ -19,18 +18,40 @@ Capybara::Selector::FilterSet.add(:_field) do
end
end
+#
+# @locator An XPath expression
+#
Capybara.add_selector(:xpath) do
xpath { |xpath| xpath }
end
+#
+# @locator A CSS selector
+#
Capybara.add_selector(:css) do
css { |css| css }
end
+#
+# @locator The id of the element to match
+#
Capybara.add_selector(:id) do
xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }
end
+#
+# @locator Matches against the id, name, or placeholder
+# @filter [String] :id Matches the id attribute
+# @filter [String] :name Matches the name attribute
+# @filter [String] :placeholder Matches the placeholder attribute
+# @filter [String] :type Matches the type attribute of the field or element type for 'textarea' and 'select'
+# @filter [Boolean] :readonly
+# @filter [String] :with Matches the current value of the field
+# @filter [String, Array] :class Matches the class(es) provided
+# @filter [Boolean] :checked Match checked fields?
+# @filter [Boolean] :unchecked Match unchecked fields?
+# @filter [Boolean] :disabled Match disabled field?
+# @filter [Boolean] :multiple Match fields that accept multiple values
Capybara.add_selector(:field) do
xpath(:id, :name, :placeholder, :type, :class) do |locator, options|
xpath = XPath.descendant(:input, :textarea, :select)[~XPath.attr(:type).one_of('submit', 'image', 'hidden')]
@@ -61,6 +82,13 @@ Capybara.add_selector(:field) do
end
end
+#
+# @locator Matches id or contents of wrapped legend
+#
+# @filter [String] :id Matches id attribute
+# @filter [String] :legend Matches contents of wrapped legend
+# @filter [String, Array] :class Matches the class(es) provided
+#
Capybara.add_selector(:fieldset) do
xpath(:id, :legend, :class) do |locator, options|
xpath = XPath.descendant(:fieldset)
@@ -71,7 +99,15 @@ Capybara.add_selector(:fieldset) do
xpath
end
end
-
+#
+# @locator Matches the id or title attributes, or the string content of the link, or the alt attribute of a contained img element
+#
+# @filter [String] :id Matches the id attribute
+# @filter [String] :title Matches the title attribute
+# @filter [String] :alt Matches the alt attribute of a contained img element
+# @filter [String] :class Matches the class(es) provided
+# @filter [String, Regexp] :href Matches the normalized href of the link
+#
Capybara.add_selector(:link) do
xpath(:id, :title, :alt, :class) do |locator, options={}|
xpath = XPath.descendant(:a)[XPath.attr(:href)]
diff --git a/lib/capybara/selector/selector.rb b/lib/capybara/selector/selector.rb
index b691ad3d..c3379183 100644
--- a/lib/capybara/selector/selector.rb
+++ b/lib/capybara/selector/selector.rb
@@ -157,14 +157,15 @@ module Capybara
##
#
- # Define a non-expression filter for use with this selector
- # @overload filter(name, *types, options={}, &block)
- # @param [Symbol] name The filter name
- # @param [Array] types The types of the filter - currently valid types are [:boolean]
- # @param [Hash] options ({}) Options of the filter
- # @option options [Array<>] :valid_values Valid values for this filter
- # @option options :default The default value of the filter (if any)
- # @option options :skip_if Value of the filter that will cause it to be skipped
+ # Define a non-expression filter for use with this selector
+ #
+ # @overload filter(name, *types, options={}, &block)
+ # @param [Symbol] name The filter name
+ # @param [Array] types The types of the filter - currently valid types are [:boolean]
+ # @param [Hash] options ({}) Options of the filter
+ # @option options [Array<>] :valid_values Valid values for this filter
+ # @option options :default The default value of the filter (if any)
+ # @option options :skip_if Value of the filter that will cause it to be skipped
#
def filter(name, *types_and_options, &block)
options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}