capybara 2.8.1 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.yard/templates_custom/default/class/html/selectors.erb +38 -0
  3. data/.yard/templates_custom/default/class/html/setup.rb +17 -0
  4. data/.yard/yard_extensions.rb +78 -0
  5. data/.yardopts +1 -0
  6. data/History.md +13 -1
  7. data/README.md +1 -1
  8. data/lib/capybara/helpers.rb +0 -59
  9. data/lib/capybara/node/actions.rb +17 -11
  10. data/lib/capybara/node/finders.rb +9 -0
  11. data/lib/capybara/node/matchers.rb +54 -15
  12. data/lib/capybara/queries/base_query.rb +59 -3
  13. data/lib/capybara/queries/selector_query.rb +7 -16
  14. data/lib/capybara/queries/text_query.rb +11 -10
  15. data/lib/capybara/rack_test/form.rb +2 -1
  16. data/lib/capybara/result.rb +25 -10
  17. data/lib/capybara/rspec/features.rb +3 -2
  18. data/lib/capybara/rspec/matchers.rb +1 -1
  19. data/lib/capybara/selector.rb +277 -175
  20. data/lib/capybara/selector/filter_set.rb +3 -1
  21. data/lib/capybara/selector/selector.rb +227 -0
  22. data/lib/capybara/session.rb +2 -2
  23. data/lib/capybara/spec/session/element/matches_selector_spec.rb +29 -1
  24. data/lib/capybara/spec/session/selectors_spec.rb +12 -0
  25. data/lib/capybara/spec/views/form.erb +11 -0
  26. data/lib/capybara/version.rb +1 -1
  27. data/spec/fixtures/capybara.csv +1 -0
  28. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  29. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  30. data/spec/rack_test_spec.rb +8 -0
  31. data/spec/result_spec.rb +3 -0
  32. data/spec/selenium_firefox_spec.rb +44 -0
  33. data/spec/selenium_spec_chrome.rb +5 -2
  34. data/spec/{selenium_spec.rb → shared_selenium_session.rb} +9 -43
  35. metadata +10 -3
@@ -12,7 +12,9 @@ module Capybara
12
12
  instance_eval(&block)
13
13
  end
14
14
 
15
- def filter(name, options={}, &block)
15
+ def filter(name, *types_and_options, &block)
16
+ options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
17
+ types_and_options.each { |k| options[k] = true}
16
18
  filters[name] = Filter.new(name, block, options)
17
19
  end
18
20
 
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+ require 'capybara/selector/filter_set'
3
+ require 'xpath'
4
+
5
+ #Patch XPath to allow a nil condition in where
6
+ module XPath
7
+ class Renderer
8
+ def where(on, condition)
9
+ condition = condition.to_s
10
+ if !condition.empty?
11
+ "#{on}[#{condition}]"
12
+ else
13
+ "#{on}"
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ module Capybara
20
+ class Selector
21
+
22
+ attr_reader :name, :format, :expression_filters
23
+
24
+ class << self
25
+ def all
26
+ @selectors ||= {}
27
+ end
28
+
29
+ def add(name, &block)
30
+ all[name.to_sym] = Capybara::Selector.new(name.to_sym, &block)
31
+ end
32
+
33
+ def update(name, &block)
34
+ all[name.to_sym].instance_eval(&block)
35
+ end
36
+
37
+ def remove(name)
38
+ all.delete(name.to_sym)
39
+ end
40
+ end
41
+
42
+ def initialize(name, &block)
43
+ @name = name
44
+ @filter_set = FilterSet.add(name){}
45
+ @match = nil
46
+ @label = nil
47
+ @failure_message = nil
48
+ @description = nil
49
+ @format = nil
50
+ @expression = nil
51
+ @expression_filters = []
52
+ instance_eval(&block)
53
+ end
54
+
55
+ def custom_filters
56
+ @filter_set.filters
57
+ end
58
+
59
+ ##
60
+ #
61
+ # Define a selector by an xpath expression
62
+ #
63
+ # @overload xpath(*expression_filters, &block)
64
+ # @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this expression
65
+ # @yield [locator, options] The block to use to generate the XPath expression
66
+ # @yieldparam [String] locator The locator string passed to the query
67
+ # @yieldparam [Hash] options The options hash passed to the query
68
+ # @yieldreturn [#to_xpath, #to_s] An object that can produce an xpath expression
69
+ #
70
+ # @overload xpath()
71
+ # @return [#call] The block that will be called to generate the XPath expression
72
+ #
73
+ def xpath(*expression_filters, &block)
74
+ @format, @expression_filters, @expression = :xpath, expression_filters.flatten, block if block
75
+ format == :xpath ? @expression : nil
76
+ end
77
+
78
+ ##
79
+ #
80
+ # Define a selector by a CSS selector
81
+ #
82
+ # @overload css(*expression_filters, &block)
83
+ # @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this CSS selector
84
+ # @yield [locator, options] The block to use to generate the CSS selector
85
+ # @yieldparam [String] locator The locator string passed to the query
86
+ # @yieldparam [Hash] options The options hash passed to the query
87
+ # @yieldreturn [#to_s] An object that can produce a CSS selector
88
+ #
89
+ # @overload css()
90
+ # @return [#call] The block that will be called to generate the CSS selector
91
+ #
92
+ def css(*expression_filters, &block)
93
+ @format, @expression_filters, @expression = :css, expression_filters.flatten, block if block
94
+ format == :css ? @expression : nil
95
+ end
96
+
97
+ ##
98
+ #
99
+ # Automatic selector detection
100
+ #
101
+ # @yield [locator] This block takes the passed in locator string and returns whether or not it matches the selector
102
+ # @yieldparam [String], locator The locator string used to determin if it matches the selector
103
+ # @yieldreturn [Boolean] Whether this selector matches the locator string
104
+ # @return [#call] The block that will be used to detect selector match
105
+ #
106
+ def match(&block)
107
+ @match = block if block
108
+ @match
109
+ end
110
+
111
+ ##
112
+ #
113
+ # Set/get a descriptive label for the selector
114
+ #
115
+ # @overload label(label)
116
+ # @param [String] label A descriptive label for this selector - used in error messages
117
+ # @overload label()
118
+ # @return [String] The currently set label
119
+ #
120
+ def label(label=nil)
121
+ @label = label if label
122
+ @label
123
+ end
124
+
125
+ ##
126
+ #
127
+ # Description of the selector
128
+ #
129
+ # @param [Hash] options The options of the query used to generate the description
130
+ # @return [String] Description of the selector when used with the options passed
131
+ #
132
+ def description(options={})
133
+ @filter_set.description(options)
134
+ end
135
+
136
+ def call(locator, options={})
137
+ if format
138
+ # @expression.call(locator, options.select {|k,v| @expression_filters.include?(k)})
139
+ @expression.call(locator, options)
140
+ else
141
+ warn "Selector has no format"
142
+ end
143
+ end
144
+
145
+ ##
146
+ #
147
+ # Should this selector be used for the passed in locator
148
+ #
149
+ # This is used by the automatic selector selection mechanism when no selector type is passed to a selector query
150
+ #
151
+ # @param [String] locator The locator passed to the query
152
+ # @return [Boolean] Whether or not to use this selector
153
+ #
154
+ def match?(locator)
155
+ @match and @match.call(locator)
156
+ end
157
+
158
+ ##
159
+ #
160
+ # Define a non-expression filter for use with this selector
161
+ #
162
+ # @overload filter(name, *types, options={}, &block)
163
+ # @param [Symbol] name The filter name
164
+ # @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
165
+ # @param [Hash] options ({}) Options of the filter
166
+ # @option options [Array<>] :valid_values Valid values for this filter
167
+ # @option options :default The default value of the filter (if any)
168
+ # @option options :skip_if Value of the filter that will cause it to be skipped
169
+ #
170
+ def filter(name, *types_and_options, &block)
171
+ options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
172
+ types_and_options.each { |k| options[k] = true}
173
+ custom_filters[name] = Filter.new(name, block, options)
174
+ end
175
+
176
+ def filter_set(name, filters_to_use = nil)
177
+ f_set = FilterSet.all[name]
178
+ f_set.filters.each do | name, filter |
179
+ custom_filters[name] = filter if filters_to_use.nil? || filters_to_use.include?(name)
180
+ end
181
+ f_set.descriptions.each { |desc| @filter_set.describe &desc }
182
+ end
183
+
184
+ def describe &block
185
+ @filter_set.describe &block
186
+ end
187
+
188
+ private
189
+
190
+ def locate_field(xpath, locator, options={})
191
+ locate_xpath = xpath #need to save original xpath for the label wrap
192
+ if locator
193
+ locator = locator.to_s
194
+ attr_matchers = XPath.attr(:id).equals(locator) |
195
+ XPath.attr(:name).equals(locator) |
196
+ XPath.attr(:placeholder).equals(locator) |
197
+ XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for))
198
+ attr_matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
199
+
200
+ locate_xpath = locate_xpath[attr_matchers]
201
+ locate_xpath += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
202
+ end
203
+
204
+ locate_xpath = [:id, :name, :placeholder, :class].inject(locate_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
205
+ locate_xpath
206
+ end
207
+
208
+ def find_by_attr(attribute, value)
209
+ finder_name = "find_by_#{attribute.to_s}_attr"
210
+ if respond_to?(finder_name, true)
211
+ send(finder_name, value)
212
+ else
213
+ value ? XPath.attr(attribute).equals(value) : nil
214
+ end
215
+ end
216
+
217
+ def find_by_class_attr(classes)
218
+ if classes
219
+ Array(classes).map do |klass|
220
+ "contains(concat(' ',normalize-space(@class),' '),' #{klass} ')"
221
+ end.join(" and ").to_sym
222
+ else
223
+ nil
224
+ end
225
+ end
226
+ end
227
+ end
@@ -439,7 +439,7 @@ module Capybara
439
439
  driver.switch_to_window(window.handle)
440
440
  window
441
441
  else
442
- wait_time = Capybara::Queries::SelectorQuery.new(options).wait
442
+ wait_time = Capybara::Queries::BaseQuery.wait(options)
443
443
  document.synchronize(wait_time, errors: [Capybara::WindowError]) do
444
444
  original_window_handle = driver.current_window_handle
445
445
  begin
@@ -536,7 +536,7 @@ module Capybara
536
536
  old_handles = driver.window_handles
537
537
  block.call
538
538
 
539
- wait_time = Capybara::Queries::SelectorQuery.new(options).wait
539
+ wait_time = Capybara::Queries::BaseQuery.wait(options)
540
540
  document.synchronize(wait_time, errors: [Capybara::WindowError]) do
541
541
  opened_handles = (driver.window_handles - old_handles)
542
542
  if opened_handles.size != 1
@@ -1,4 +1,4 @@
1
- Capybara::SpecHelper.spec '#match_xpath?' do
1
+ Capybara::SpecHelper.spec '#match_selector?' do
2
2
  before do
3
3
  @session.visit('/with_html')
4
4
  @element = @session.find('//span', text: '42')
@@ -28,6 +28,20 @@ Capybara::SpecHelper.spec '#match_xpath?' do
28
28
  expect(@element).not_to match_selector("//span", :text => "Doesnotexist")
29
29
  end
30
30
  end
31
+
32
+ it "should have css sugar" do
33
+ expect(@element.matches_css?('span.number')).to be true
34
+ expect(@element.matches_css?('span.not_a_number')).to be false
35
+ expect(@element.matches_css?('span.number', text: "42")).to be true
36
+ expect(@element.matches_css?('span.number', text: "Nope")).to be false
37
+ end
38
+
39
+ it "should have xpath sugar" do
40
+ expect(@element.matches_xpath?("//span")).to be true
41
+ expect(@element.matches_xpath?("//div")).to be false
42
+ expect(@element.matches_xpath?("//span", text: '42')).to be true
43
+ expect(@element.matches_xpath?("//span", text: 'Nope')).to be false
44
+ end
31
45
  end
32
46
 
33
47
  Capybara::SpecHelper.spec '#not_matches_selector?' do
@@ -60,4 +74,18 @@ Capybara::SpecHelper.spec '#not_matches_selector?' do
60
74
  expect(@element).to not_match_selector(:css, "span.number", :text => "Doesnotexist")
61
75
  end
62
76
  end
77
+
78
+ it "should have CSS sugar" do
79
+ expect(@element.not_matches_css?("span.number")).to be false
80
+ expect(@element.not_matches_css?("p a#doesnotexist")).to be true
81
+ expect(@element.not_matches_css?("span.number", :text => "42")).to be false
82
+ expect(@element.not_matches_css?("span.number", :text => "Doesnotexist")).to be true
83
+ end
84
+
85
+ it "should have xpath sugar" do
86
+ expect(@element.not_matches_xpath?("//span")).to be false
87
+ expect(@element.not_matches_xpath?("//div")).to be true
88
+ expect(@element.not_matches_xpath?("//span", :text => "42")).to be false
89
+ expect(@element.not_matches_xpath?("//span", :text => "Doesnotexist")).to be true
90
+ end
63
91
  end if Gem::Version.new(RSpec::Expectations::Version::STRING) >= Gem::Version.new('3.1')
@@ -45,5 +45,17 @@ Capybara::SpecHelper.spec Capybara::Selector do
45
45
  it "can find specifically by placeholder" do
46
46
  expect(@session.find(:field, placeholder: 'FirstName')['id']).to eq "form_first_name"
47
47
  end
48
+
49
+ it "can find by type" do
50
+ expect(@session.find(:field, 'Confusion', type: 'checkbox')['id']).to eq 'confusion_checkbox'
51
+ expect(@session.find(:field, 'Confusion', type: 'text')['id']).to eq 'confusion_text'
52
+ expect(@session.find(:field, 'Confusion', type: 'textarea')['id']).to eq 'confusion_textarea'
53
+ end
54
+
55
+ it "can find by class" do
56
+ expect(@session.find(:field, class: 'confusion-checkbox')['id']).to eq 'confusion_checkbox'
57
+ expect(@session).to have_selector(:field, class: 'confusion', count: 3)
58
+ expect(@session.find(:field, class: ['confusion','confusion-textarea'])['id']).to eq 'confusion_textarea'
59
+ end
48
60
  end
49
61
  end
@@ -526,4 +526,15 @@ New line after and before textarea tag
526
526
  </p>
527
527
  </form>
528
528
 
529
+ <label>Confusion
530
+ <input type="checkbox" id="confusion_checkbox" class="confusion-checkbox confusion"/>
531
+ </label>
532
+
533
+ <label>Confusion
534
+ <input type="text" id="confusion_text" class="confusion-text confusion"/>
535
+ </label>
536
+
537
+ <label>Confusion
538
+ <textarea id="confusion_textarea" class="confusion confusion-textarea"/>
539
+ </label>
529
540
 
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Capybara
3
- VERSION = '2.8.1'
3
+ VERSION = '2.9.0'
4
4
  end
@@ -0,0 +1 @@
1
+ test, mime-type, file
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
 
4
4
  RSpec.describe Capybara::Selenium::Driver do
5
5
  it "should exit with a non-zero exit status" do
6
- browser = Capybara::Selenium::Driver.new(TestApp).browser
6
+ browser = Capybara::Selenium::Driver.new(TestApp, browser: (ENV['SELENIUM_BROWSER'] || :firefox).to_sym).browser
7
7
  expect(true).to eq(false)
8
8
  end
9
9
  end
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
 
4
4
  RSpec.describe Capybara::Selenium::Driver do
5
5
  it "should exit with a zero exit status" do
6
- browser = Capybara::Selenium::Driver.new(TestApp).browser
6
+ browser = Capybara::Selenium::Driver.new(TestApp, browser: (ENV['SELENIUM_BROWSER'] || :firefox).to_sym).browser
7
7
  expect(true).to eq(true)
8
8
  end
9
9
  end
@@ -79,6 +79,14 @@ RSpec.describe Capybara::Session do
79
79
  expect(@session.html).to include('Successfully ignored empty file field.')
80
80
  end
81
81
  end
82
+
83
+ it "should not submit an obsolete mime type" do
84
+ @test_jpg_file_path = File.expand_path('fixtures/capybara.csv', File.dirname(__FILE__))
85
+ @session.visit("/form")
86
+ @session.attach_file "form_document", @test_jpg_file_path
87
+ @session.click_button('Upload Single')
88
+ expect(@session).to have_content("Content-type: text/csv")
89
+ end
82
90
  end
83
91
 
84
92
  describe "#click" do
@@ -81,6 +81,9 @@ RSpec.describe Capybara::Result do
81
81
  expect(result.instance_variable_get('@result_cache').size).to be 1
82
82
 
83
83
  #works for indexed access
84
+ result[0]
85
+ expect(result.instance_variable_get('@result_cache').size).to be 1
86
+
84
87
  result[2]
85
88
  expect(result.instance_variable_get('@result_cache').size).to be 3
86
89
 
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require "selenium-webdriver"
4
+ require 'shared_selenium_session'
5
+
6
+ Capybara.register_driver :selenium_focus do |app|
7
+ # profile = Selenium::WebDriver::Firefox::Profile.new
8
+ # profile["focusmanager.testmode"] = true
9
+ # Capybara::Selenium::Driver.new(app, browser: :firefox, profile: profile)
10
+ Capybara::Selenium::Driver.new(app, browser: :firefox)
11
+ end
12
+
13
+ module TestSessions
14
+ Selenium = Capybara::Session.new(:selenium_focus, TestApp)
15
+ end
16
+
17
+ skipped_tests = [
18
+ :response_headers,
19
+ :status_code,
20
+ :trigger
21
+ ]
22
+ skipped_tests << :windows if ENV['TRAVIS'] && !ENV['WINDOW_TEST']
23
+
24
+ Capybara::SpecHelper.run_specs TestSessions::Selenium, "selenium", :capybara_skip => skipped_tests
25
+
26
+ RSpec.describe "Capybara::Session with firefox" do
27
+ include_examples "Capybara::Session", TestSessions::Selenium, :selenium_focus
28
+ end
29
+
30
+ RSpec.describe Capybara::Selenium::Driver do
31
+ before do
32
+ @driver = Capybara::Selenium::Driver.new(TestApp, browser: :firefox)
33
+ end
34
+
35
+ describe '#quit' do
36
+ it "should reset browser when quit" do
37
+ expect(@driver.browser).to be
38
+ @driver.quit
39
+ #access instance variable directly so we don't create a new browser instance
40
+ expect(@driver.instance_variable_get(:@browser)).to be_nil
41
+ end
42
+ end
43
+ end
44
+
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require 'spec_helper'
3
3
  require 'selenium-webdriver'
4
-
5
- Selenium::WebDriver::Chrome.driver_path = '/home/travis/chromedriver' if ENV['TRAVIS']
4
+ require 'shared_selenium_session'
6
5
 
7
6
  Capybara.register_driver :selenium_chrome do |app|
8
7
  args = ENV['TRAVIS'] ? ['no-sandbox' ] : []
@@ -18,3 +17,7 @@ Capybara::SpecHelper.run_specs TestSessions::Chrome, "selenium_chrome", :capybar
18
17
  :status_code,
19
18
  :trigger
20
19
  ] unless ENV['TRAVIS'] && (RUBY_PLATFORM == 'java')
20
+
21
+ RSpec.describe "Capybara::Session with chrome" do
22
+ include_examples "Capybara::Session", TestSessions::Chrome, :selenium_chrome
23
+ end