capybara 3.11.0 → 3.12.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +24 -0
  3. data/README.md +4 -4
  4. data/lib/capybara/node/element.rb +7 -1
  5. data/lib/capybara/node/matchers.rb +0 -18
  6. data/lib/capybara/queries/base_query.rb +1 -1
  7. data/lib/capybara/queries/selector_query.rb +24 -39
  8. data/lib/capybara/result.rb +4 -4
  9. data/lib/capybara/selector/builders/css_builder.rb +59 -27
  10. data/lib/capybara/selector/builders/xpath_builder.rb +50 -35
  11. data/lib/capybara/selector/css.rb +7 -7
  12. data/lib/capybara/selector/filter.rb +1 -0
  13. data/lib/capybara/selector/filter_set.rb +17 -15
  14. data/lib/capybara/selector/filters/locator_filter.rb +19 -0
  15. data/lib/capybara/selector/regexp_disassembler.rb +104 -61
  16. data/lib/capybara/selector/selector.rb +14 -5
  17. data/lib/capybara/selector.rb +10 -20
  18. data/lib/capybara/selenium/driver.rb +14 -9
  19. data/lib/capybara/selenium/driver_specializations/{marionette_driver.rb → firefox_driver.rb} +3 -3
  20. data/lib/capybara/selenium/nodes/{marionette_node.rb → firefox_node.rb} +1 -1
  21. data/lib/capybara/spec/session/all_spec.rb +8 -1
  22. data/lib/capybara/spec/session/assert_selector_spec.rb +0 -10
  23. data/lib/capybara/spec/session/click_button_spec.rb +5 -3
  24. data/lib/capybara/spec/session/click_link_spec.rb +5 -5
  25. data/lib/capybara/spec/session/find_spec.rb +1 -1
  26. data/lib/capybara/spec/session/first_spec.rb +1 -1
  27. data/lib/capybara/spec/session/has_css_spec.rb +7 -0
  28. data/lib/capybara/spec/session/has_xpath_spec.rb +17 -0
  29. data/lib/capybara/spec/session/node_spec.rb +10 -3
  30. data/lib/capybara/spec/session/window/window_spec.rb +2 -2
  31. data/lib/capybara/spec/spec_helper.rb +1 -2
  32. data/lib/capybara/spec/views/obscured.erb +3 -0
  33. data/lib/capybara/version.rb +1 -1
  34. data/lib/capybara.rb +7 -0
  35. data/spec/css_builder_spec.rb +99 -0
  36. data/spec/result_spec.rb +6 -0
  37. data/spec/selector_spec.rb +41 -0
  38. data/spec/selenium_spec_chrome.rb +18 -16
  39. data/spec/selenium_spec_chrome_remote.rb +0 -2
  40. data/spec/{selenium_spec_marionette.rb → selenium_spec_firefox.rb} +31 -25
  41. data/spec/selenium_spec_firefox_remote.rb +4 -6
  42. data/spec/shared_selenium_session.rb +2 -2
  43. data/spec/spec_helper.rb +5 -5
  44. data/spec/xpath_builder_spec.rb +91 -0
  45. metadata +10 -7
@@ -65,22 +65,6 @@ module Capybara
65
65
  strs
66
66
  end
67
67
 
68
- def min_repeat(exp)
69
- exp.quantifier&.min || 1
70
- end
71
-
72
- def max_repeat(exp)
73
- exp.quantifier&.max || 1
74
- end
75
-
76
- def fixed_repeat?(exp)
77
- min_repeat(exp) == max_repeat(exp)
78
- end
79
-
80
- def optional?(exp)
81
- min_repeat(exp).zero?
82
- end
83
-
84
68
  def combine(strs)
85
69
  suffixes = [[]]
86
70
  strs.reverse_each do |str|
@@ -91,9 +75,7 @@ module Capybara
91
75
  prefixes.product(suffixes) { |pair| result << pair.flatten(1) }
92
76
  suffixes = result
93
77
  else
94
- suffixes.each do |arr|
95
- arr.unshift str
96
- end
78
+ suffixes.each { |arr| arr.unshift str }
97
79
  end
98
80
  end
99
81
  suffixes
@@ -106,59 +88,120 @@ module Capybara
106
88
  end
107
89
 
108
90
  def extract_strings(expression, alternation: false)
109
- strings = []
110
- expression.each do |exp| # rubocop:disable Metrics/BlockLength
111
- if optional?(exp) && !alternation
112
- strings.push(nil)
113
- next
114
- end
91
+ Expression.new(expression).extract_strings(alternation)
92
+ end
93
+
94
+ # @api private
95
+ class Expression
96
+ def initialize(exp)
97
+ @exp = exp
98
+ end
99
+
100
+ def extract_strings(process_alternatives)
101
+ strings = []
102
+ each do |exp|
103
+ next strings.push(nil) if exp.optional? && !process_alternatives
115
104
 
116
- if %i[meta].include?(exp.type) && !exp.terminal? && alternation
117
- strings.push(alternative_strings(exp))
118
- next
105
+ next strings.push(exp.alternative_strings) if exp.alternation? && process_alternatives
106
+
107
+ strings.concat(exp.strings(process_alternatives))
119
108
  end
109
+ strings
110
+ end
111
+
112
+ protected
113
+
114
+ def alternation?
115
+ (type == :meta) && !terminal?
116
+ end
117
+
118
+ def optional?
119
+ min_repeat.zero?
120
+ end
121
+
122
+ def terminal?
123
+ @exp.terminal?
124
+ end
120
125
 
121
- if %i[meta set].include?(exp.type)
122
- strings.push(nil)
123
- next
126
+ def strings(process_alternatives)
127
+ if indeterminate?
128
+ [nil]
129
+ elsif terminal?
130
+ terminal_strings
131
+ elsif optional?
132
+ optional_strings
133
+ else
134
+ repeated_strings(process_alternatives)
124
135
  end
136
+ end
125
137
 
126
- if exp.terminal?
127
- text = case exp.type
128
- when :literal then exp.text
129
- when :escape then exp.char
130
- else
131
- strings.push(nil)
132
- next
133
- end
134
-
135
- if optional?(exp)
136
- strings.push(Set.new([[''], [text]]))
137
- strings.push(nil) unless max_repeat(exp) == 1
138
- next
139
- else
140
- strings.push(text * min_repeat(exp))
141
- end
142
- elsif optional?(exp)
143
- strings.push(Set.new([[''], extract_strings(exp, alternation: true)]))
144
- strings.push(nil) unless max_repeat(exp) == 1
145
- next
138
+ def terminal_strings
139
+ text = case @exp.type
140
+ when :literal then @exp.text
141
+ when :escape then @exp.char
146
142
  else
147
- min_repeat(exp).times { strings.concat extract_strings(exp, alternation: alternation) }
143
+ return [nil]
148
144
  end
149
- strings.push(nil) unless fixed_repeat?(exp)
145
+
146
+ optional? ? options_set(text) : repeat_set(text)
147
+ end
148
+
149
+ def optional_strings
150
+ options_set(extract_strings(true))
151
+ end
152
+
153
+ def repeated_strings(process_alternatives)
154
+ repeat_set extract_strings(process_alternatives)
155
+ end
156
+
157
+ def alternative_strings
158
+ alts = alternatives.map { |sub_exp| sub_exp.extract_strings(alternation: true) }
159
+ alts.all?(&:any?) ? Set.new(alts) : nil
160
+ end
161
+
162
+ private
163
+
164
+ def indeterminate?
165
+ %i[meta set].include?(type)
166
+ end
167
+
168
+ def min_repeat
169
+ @exp.quantifier&.min || 1
170
+ end
171
+
172
+ def max_repeat
173
+ @exp.quantifier&.max || 1
174
+ end
175
+
176
+ def fixed_repeat?
177
+ min_repeat == max_repeat
178
+ end
179
+
180
+ def type
181
+ @exp.type
182
+ end
183
+
184
+ def repeat_set(str)
185
+ strs = Array(str * min_repeat)
186
+ strs.push(nil) unless fixed_repeat?
187
+ strs
188
+ end
189
+
190
+ def options_set(strs)
191
+ strs = [Set.new([[''], Array(strs)])]
192
+ strs.push(nil) unless max_repeat == 1
193
+ strs
194
+ end
195
+
196
+ def alternatives
197
+ @exp.alternatives.map { |exp| Expression.new(exp) }
150
198
  end
151
- strings
152
- end
153
199
 
154
- def alternative_strings(expression)
155
- alternatives = expression.alternatives.map { |sub_exp| extract_strings(sub_exp, alternation: true) }
156
- if alternatives.all?(&:any?)
157
- Set.new(alternatives)
158
- else
159
- nil
200
+ def each
201
+ @exp.each { |exp| yield Expression.new(exp) }
160
202
  end
161
203
  end
204
+ private_constant :Expression
162
205
  end
163
206
  end
164
207
  end
@@ -169,17 +169,25 @@ module Capybara
169
169
  @selectors ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
170
170
  end
171
171
 
172
+ def [](name)
173
+ all.fetch(name.to_sym) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
174
+ end
175
+
172
176
  def add(name, &block)
173
177
  all[name.to_sym] = Capybara::Selector.new(name.to_sym, &block)
174
178
  end
175
179
 
176
180
  def update(name, &block)
177
- all[name.to_sym].instance_eval(&block)
181
+ self[name].instance_eval(&block)
178
182
  end
179
183
 
180
184
  def remove(name)
181
185
  all.delete(name.to_sym)
182
186
  end
187
+
188
+ def for(locator)
189
+ all.values.find { |sel| sel.match?(locator) }
190
+ end
183
191
  end
184
192
 
185
193
  def initialize(name, &block)
@@ -348,8 +356,9 @@ module Capybara
348
356
 
349
357
  def_delegators :@filter_set, :node_filter, :expression_filter, :filter
350
358
 
351
- def locator_filter(&block)
352
- @locator_filter = block if block
359
+ def locator_filter(*types, **options, &block)
360
+ types.each { |type| options[type] = true }
361
+ @locator_filter = Filters::LocatorFilter.new(block, options) if block
353
362
  @locator_filter
354
363
  end
355
364
 
@@ -400,7 +409,7 @@ module Capybara
400
409
  end
401
410
 
402
411
  # @api private
403
- def builder
412
+ def builder(expr = nil)
404
413
  case format
405
414
  when :css
406
415
  Capybara::Selector::CSSBuilder
@@ -408,7 +417,7 @@ module Capybara
408
417
  Capybara::Selector::XPathBuilder
409
418
  else
410
419
  raise NotImplementedError, "No builder exists for selector of type #{format}"
411
- end
420
+ end.new(expr)
412
421
  end
413
422
 
414
423
  # @api private
@@ -34,7 +34,7 @@ Capybara.add_selector(:css) do
34
34
  end
35
35
 
36
36
  Capybara.add_selector(:id) do
37
- xpath { |id| XPath.descendant[builder.attribute_conditions(id: id)] }
37
+ xpath { |id| builder(XPath.descendant).add_attribute_conditions(id: id) }
38
38
  locator_filter { |node, id| id.is_a?(Regexp) ? node[:id] =~ id : true }
39
39
  end
40
40
 
@@ -92,8 +92,7 @@ end
92
92
 
93
93
  Capybara.add_selector(:link) do
94
94
  xpath do |locator, href: true, alt: nil, title: nil, **|
95
- xpath = XPath.descendant(:a)
96
- xpath = xpath[@href_conditions = builder.attribute_conditions(href: href)]
95
+ xpath = builder(XPath.descendant(:a)).add_attribute_conditions(href: href)
97
96
 
98
97
  unless locator.nil?
99
98
  locator = locator.to_s
@@ -119,25 +118,18 @@ Capybara.add_selector(:link) do
119
118
  end
120
119
 
121
120
  expression_filter(:download, valid_values: [true, false, String]) do |expr, download|
122
- expr[builder.attribute_conditions(download: download)]
121
+ builder(expr).add_attribute_conditions(download: download)
123
122
  end
124
123
 
125
124
  describe_expression_filters do |**options|
126
125
  desc = +''
127
126
  if (href = options[:href])
128
- if !href.is_a?(Regexp)
129
- desc << " with href #{href.inspect}"
130
- elsif @href_conditions
131
- desc << " with href matching #{href.inspect}"
132
- end
127
+ desc << " with href #{'matching ' if href.is_a? Regexp}#{href.inspect}"
128
+ elsif options.key?(:href) # is nil/false specified?
129
+ desc << ' with no href attribute'
133
130
  end
134
- desc << ' with no href attribute' if options.fetch(:href, true).nil?
135
131
  desc
136
132
  end
137
-
138
- describe_node_filters do |href: nil, **|
139
- " with href matching #{href.inspect}" if href.is_a?(Regexp) && @href_conditions.nil?
140
- end
141
133
  end
142
134
 
143
135
  Capybara.add_selector(:button) do
@@ -161,11 +153,9 @@ Capybara.add_selector(:button) do
161
153
  image_btn_xpath = image_btn_xpath[alt_matches]
162
154
  end
163
155
 
164
- res_xpath = input_btn_xpath.union(btn_xpath).union(image_btn_xpath)
165
-
166
- res_xpath = expression_filters.keys.inject(res_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
167
-
168
- res_xpath
156
+ %i[value title type].inject(input_btn_xpath.union(btn_xpath).union(image_btn_xpath)) do |memo, ef|
157
+ memo[find_by_attr(ef, options[ef])]
158
+ end
169
159
  end
170
160
 
171
161
  node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
@@ -491,7 +481,7 @@ Capybara.add_selector(:element) do
491
481
  end
492
482
 
493
483
  expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
494
- xpath[builder.attribute_conditions(name => val)]
484
+ builder(xpath).add_attribute_conditions(name => val)
495
485
  end
496
486
 
497
487
  node_filter(:attributes, matcher: /.+/) do |node, name, val|
@@ -6,10 +6,10 @@ require 'English'
6
6
  class Capybara::Selenium::Driver < Capybara::Driver::Base
7
7
  DEFAULT_OPTIONS = {
8
8
  browser: :firefox,
9
- clear_local_storage: false,
10
- clear_session_storage: false
9
+ clear_local_storage: nil,
10
+ clear_session_storage: nil
11
11
  }.freeze
12
- SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage].freeze
12
+ SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage timeout].freeze
13
13
  attr_reader :app, :options
14
14
 
15
15
  def self.load_selenium
@@ -23,6 +23,9 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
23
23
 
24
24
  def browser
25
25
  @browser ||= begin
26
+ if options[:timeout]
27
+ options[:http_client] ||= Selenium::WebDriver::Remote::Http::Default.new(read_timeout: options[:timeout])
28
+ end
26
29
  processed_options = options.reject { |key, _val| SPECIAL_OPTIONS.include?(key) }
27
30
  Selenium::WebDriver.for(options[:browser], processed_options).tap do |driver|
28
31
  specialize_driver(driver)
@@ -260,15 +263,17 @@ private
260
263
  end
261
264
 
262
265
  def clear_storage
263
- clear_session_storage if options[:clear_session_storage]
264
- clear_local_storage if options[:clear_local_storage]
266
+ clear_session_storage unless options[:clear_session_storage] == false
267
+ clear_local_storage unless options[:clear_local_storage] == false
268
+ rescue Selenium::WebDriver::Error::JavascriptError # rubocop:disable Lint/HandleExceptions
269
+ # session/local storage may not be available if on non-http pages (e.g. about:blank)
265
270
  end
266
271
 
267
272
  def clear_session_storage
268
273
  if @browser.respond_to? :session_storage
269
274
  @browser.session_storage.clear
270
275
  else
271
- warn 'sessionStorage clear requested but is not available for this driver'
276
+ warn 'sessionStorage clear requested but is not supported by this driver' unless options[:clear_session_storage].nil?
272
277
  end
273
278
  end
274
279
 
@@ -276,7 +281,7 @@ private
276
281
  if @browser.respond_to? :local_storage
277
282
  @browser.local_storage.clear
278
283
  else
279
- warn 'localStorage clear requested but is not available for this driver'
284
+ warn 'localStorage clear requested but is not supported by this driver' unless options[:clear_local_storage].nil?
280
285
  end
281
286
  end
282
287
 
@@ -353,7 +358,7 @@ private
353
358
  extend ChromeDriver
354
359
  when :firefox
355
360
  require 'capybara/selenium/patches/pause_duration_fix' if pause_broken?(sel_driver)
356
- extend MarionetteDriver if sel_driver.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
361
+ extend FirefoxDriver if sel_driver.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
357
362
  end
358
363
  end
359
364
 
@@ -396,4 +401,4 @@ private
396
401
  end
397
402
 
398
403
  require 'capybara/selenium/driver_specializations/chrome_driver'
399
- require 'capybara/selenium/driver_specializations/marionette_driver'
404
+ require 'capybara/selenium/driver_specializations/firefox_driver'
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'capybara/selenium/nodes/marionette_node'
3
+ require 'capybara/selenium/nodes/firefox_node'
4
4
 
5
- module Capybara::Selenium::Driver::MarionetteDriver
5
+ module Capybara::Selenium::Driver::FirefoxDriver
6
6
  def resize_window_to(handle, width, height)
7
7
  within_given_window(handle) do
8
8
  # Don't set the size if already set - See https://github.com/mozilla/geckodriver/issues/643
@@ -45,6 +45,6 @@ module Capybara::Selenium::Driver::MarionetteDriver
45
45
  private
46
46
 
47
47
  def build_node(native_node)
48
- ::Capybara::Selenium::MarionetteNode.new(self, native_node)
48
+ ::Capybara::Selenium::FirefoxNode.new(self, native_node)
49
49
  end
50
50
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'capybara/selenium/extensions/html5_drag'
4
4
 
5
- class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node
5
+ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
6
6
  include Html5Drag
7
7
 
8
8
  def click(keys = [], **options)
@@ -30,7 +30,7 @@ Capybara::SpecHelper.spec '#all' do
30
30
 
31
31
  it 'should accept an XPath instance', :exact_false do
32
32
  @session.visit('/form')
33
- @xpath = Capybara::Selector.all[:fillable_field].call('Name')
33
+ @xpath = Capybara::Selector[:fillable_field].call('Name')
34
34
  expect(@xpath).to be_a(::XPath::Union)
35
35
  @result = @session.all(@xpath).map(&:value)
36
36
  expect(@result).to include('Smith', 'John', 'John Smith')
@@ -135,6 +135,13 @@ Capybara::SpecHelper.spec '#all' do
135
135
  it 'should raise ExpectationNotMet when the number of elements founds does not match the expectation' do
136
136
  expect { @session.all(:css, 'h1, p', between: 0..3) }.to raise_error(Capybara::ExpectationNotMet)
137
137
  end
138
+
139
+ eval <<~TEST, binding, __FILE__, __LINE__ + 1 if RUBY_VERSION.to_f > 2.5
140
+ it'treats an endless range as minimum' do
141
+ expect { @session.all(:css, 'h1, p', between: 2..) }.not_to raise_error
142
+ expect { @session.all(:css, 'h1, p', between: 5..) }.to raise_error(Capybara::ExpectationNotMet)
143
+ end
144
+ TEST
138
145
  end
139
146
 
140
147
  context 'with multiple count filters' do
@@ -72,16 +72,6 @@ Capybara::SpecHelper.spec '#assert_selector' do
72
72
  end
73
73
  end
74
74
 
75
- Capybara::SpecHelper.spec '#refute_selector', requires: [:driver] do
76
- it 'should warn not to use' do
77
- @session.visit('/with_html')
78
- doc = @session.document
79
- allow(doc).to receive(:warn)
80
- doc.refute_selector(:xpath, '//abbr')
81
- expect(doc).to have_received(:warn)
82
- end
83
- end
84
-
85
75
  Capybara::SpecHelper.spec '#assert_no_selector' do
86
76
  before do
87
77
  @session.visit('/with_html')
@@ -5,10 +5,12 @@ Capybara::SpecHelper.spec '#click_button' do
5
5
  @session.visit('/form')
6
6
  end
7
7
 
8
- it 'should wait for asynchronous load', requires: [:js] do
8
+ it 'should wait for asynchronous load', :focus_, requires: [:js] do
9
9
  @session.visit('/with_js')
10
- @session.click_link('Click me')
11
- @session.click_button('New Here')
10
+ @session.using_wait_time(1.5) do
11
+ @session.click_link('Click me')
12
+ @session.click_button('New Here')
13
+ end
12
14
  end
13
15
 
14
16
  it 'casts to string' do
@@ -83,7 +83,7 @@ Capybara::SpecHelper.spec '#click_link' do
83
83
  end
84
84
 
85
85
  it "should raise error if link wasn't found" do
86
- expect { @session.click_link('labore', href: 'invalid_href') }.to raise_error(Capybara::ElementNotFound)
86
+ expect { @session.click_link('labore', href: 'invalid_href') }.to raise_error(Capybara::ElementNotFound, /with href "invalid_href/)
87
87
  end
88
88
  end
89
89
 
@@ -104,8 +104,8 @@ Capybara::SpecHelper.spec '#click_link' do
104
104
  end
105
105
 
106
106
  it "should raise an error if no link's href matched the pattern" do
107
- expect { @session.click_link('labore', href: /invalid_pattern/) }.to raise_error(Capybara::ElementNotFound)
108
- expect { @session.click_link('labore', href: /.+d+/) }.to raise_error(Capybara::ElementNotFound)
107
+ expect { @session.click_link('labore', href: /invalid_pattern/) }.to raise_error(Capybara::ElementNotFound, %r{with href matching /invalid_pattern/})
108
+ expect { @session.click_link('labore', href: /.+d+/) }.to raise_error(Capybara::ElementNotFound, /#{Regexp.quote "with href matching /.+d+/"}/)
109
109
  end
110
110
 
111
111
  context 'href: nil' do
@@ -114,8 +114,8 @@ Capybara::SpecHelper.spec '#click_link' do
114
114
  end
115
115
 
116
116
  it 'should raise an error if href attribute exists' do
117
- expect { @session.click_link('Blank Href', href: nil) }.to raise_error(Capybara::ElementNotFound)
118
- expect { @session.click_link('Normal Anchor', href: nil) }.to raise_error(Capybara::ElementNotFound)
117
+ expect { @session.click_link('Blank Href', href: nil) }.to raise_error(Capybara::ElementNotFound, /with no href attribute/)
118
+ expect { @session.click_link('Normal Anchor', href: nil) }.to raise_error(Capybara::ElementNotFound, /with no href attribute/)
119
119
  end
120
120
  end
121
121
  end
@@ -224,7 +224,7 @@ Capybara::SpecHelper.spec '#find' do
224
224
 
225
225
  it 'should accept an XPath instance' do
226
226
  @session.visit('/form')
227
- @xpath = Capybara::Selector.all[:fillable_field].call('First Name')
227
+ @xpath = Capybara::Selector[:fillable_field].call('First Name')
228
228
  expect(@xpath).to be_a(::XPath::Union)
229
229
  expect(@session.find(@xpath).value).to eq('John')
230
230
  end
@@ -24,7 +24,7 @@ Capybara::SpecHelper.spec '#first' do
24
24
 
25
25
  it 'should accept an XPath instance' do
26
26
  @session.visit('/form')
27
- @xpath = Capybara::Selector.all[:fillable_field].call('First Name')
27
+ @xpath = Capybara::Selector[:fillable_field].call('First Name')
28
28
  expect(@xpath).to be_a(::XPath::Union)
29
29
  expect(@session.first(@xpath).value).to eq('John')
30
30
  end
@@ -27,11 +27,15 @@ Capybara::SpecHelper.spec '#has_css?' do
27
27
  expect(@session).to have_css('h2', id: 'h2one')
28
28
  expect(@session).to have_css('h2')
29
29
  expect(@session).to have_css('h2', id: /h2o/)
30
+ expect(@session).to have_css('li', id: /john|paul/)
30
31
  end
31
32
 
32
33
  it 'should support :class option' do
33
34
  expect(@session).to have_css('li', class: 'guitarist')
34
35
  expect(@session).to have_css('li', class: /guitar/)
36
+ expect(@session).to have_css('li', class: /guitar|drummer/)
37
+ expect(@session).to have_css('li', class: %w[beatle guitarist])
38
+ expect(@session).to have_css('li', class: /.*/)
35
39
  end
36
40
 
37
41
  it 'should support case insensitive :class and :id options' do
@@ -111,6 +115,9 @@ Capybara::SpecHelper.spec '#has_css?' do
111
115
  expect(@session).to have_css('p', count: 3)
112
116
  expect(@session).to have_css('p a#foo', count: 1)
113
117
  expect(@session).to have_css('p a.doesnotexist', count: 0)
118
+ expect(@session).to have_css('li', class: /guitar|drummer/, count: 4)
119
+ expect(@session).to have_css('li', id: /john|paul/, class: /guitar|drummer/, count: 2)
120
+ expect(@session).to have_css('li', class: %w[beatle guitarist], count: 2)
114
121
  end
115
122
 
116
123
  it 'should be false if the content occurs a different number of times than the given' do
@@ -11,6 +11,20 @@ Capybara::SpecHelper.spec '#has_xpath?' do
11
11
  expect(@session).to have_xpath("//p[contains(.,'est')]")
12
12
  end
13
13
 
14
+ it 'should support :id option' do
15
+ expect(@session).to have_xpath('//h2', id: 'h2one')
16
+ expect(@session).to have_xpath('//h2')
17
+ expect(@session).to have_xpath('//h2', id: /h2o/)
18
+ end
19
+
20
+ it 'should support :class option' do
21
+ expect(@session).to have_xpath('//li', class: 'guitarist')
22
+ expect(@session).to have_xpath('//li', class: /guitar/)
23
+ expect(@session).to have_xpath('//li', class: /guitar|drummer/)
24
+ expect(@session).to have_xpath('//li', class: %w[beatle guitarist])
25
+ expect(@session).to have_xpath('//li', class: /.*/)
26
+ end
27
+
14
28
  it 'should be false if the given selector is not on the page' do
15
29
  expect(@session).not_to have_xpath('//abbr')
16
30
  expect(@session).not_to have_xpath("//p//a[@id='doesnotexist']")
@@ -43,6 +57,9 @@ Capybara::SpecHelper.spec '#has_xpath?' do
43
57
  expect(@session).to have_xpath("//p//a[@id='foo']", count: 1)
44
58
  expect(@session).to have_xpath("//p[contains(.,'est')]", count: 1)
45
59
  expect(@session).to have_xpath("//p//a[@id='doesnotexist']", count: 0)
60
+ expect(@session).to have_xpath('//li', class: /guitar|drummer/, count: 4)
61
+ expect(@session).to have_xpath('//li', id: /john|paul/, class: /guitar|drummer/, count: 2)
62
+ expect(@session).to have_xpath('//li', class: %w[beatle guitarist], count: 2)
46
63
  end
47
64
 
48
65
  it 'should be false if the content occurs a different number of times than the given' do
@@ -406,6 +406,13 @@ Capybara::SpecHelper.spec 'node' do
406
406
  expect(locations[:y].to_f).to be_within(1).of(5)
407
407
  end
408
408
 
409
+ it 'should raise error if both x and y values are not passed' do
410
+ @session.visit('with_js')
411
+ el = @session.find(:css, '#click-test')
412
+ expect { el.click(x: 5) }.to raise_error ArgumentError
413
+ expect { el.click(x: nil, y: 3) }.to raise_error ArgumentError
414
+ end
415
+
409
416
  it 'should be able to click a table row', requires: [:js] do
410
417
  @session.visit('/tables')
411
418
  tr = @session.find(:css, '#agent_table tr:first-child').click
@@ -416,7 +423,7 @@ Capybara::SpecHelper.spec 'node' do
416
423
  @session.visit('/obscured')
417
424
  obscured = @session.find(:css, '#obscured')
418
425
  @session.execute_script <<~JS
419
- setTimeout(function(){ $('#cover').hide(); }, 1000)
426
+ setTimeout(function(){ $('#cover').hide(); }, 700)
420
427
  JS
421
428
  expect { obscured.click }.not_to raise_error
422
429
  end
@@ -468,7 +475,7 @@ Capybara::SpecHelper.spec 'node' do
468
475
  @session.visit('/obscured')
469
476
  obscured = @session.find(:css, '#obscured')
470
477
  @session.execute_script <<~JS
471
- setTimeout(function(){ $('#cover').hide(); }, 1000)
478
+ setTimeout(function(){ $('#cover').hide(); }, 700)
472
479
  JS
473
480
  expect { obscured.double_click }.not_to raise_error
474
481
  end
@@ -502,7 +509,7 @@ Capybara::SpecHelper.spec 'node' do
502
509
  @session.visit('/obscured')
503
510
  obscured = @session.find(:css, '#obscured')
504
511
  @session.execute_script <<~JS
505
- setTimeout(function(){ $('#cover').hide(); }, 1000)
512
+ setTimeout(function(){ $('#cover').hide(); }, 700)
506
513
  JS
507
514
  expect { obscured.right_click }.not_to raise_error
508
515
  end
@@ -120,7 +120,7 @@ Capybara::SpecHelper.spec Capybara::Window, requires: [:windows] do
120
120
 
121
121
  after do
122
122
  @session.current_window.resize_to(*@initial_size)
123
- sleep 0.5
123
+ sleep 1
124
124
  end
125
125
 
126
126
  it 'should be able to resize window', requires: %i[windows js] do
@@ -194,7 +194,7 @@ Capybara::SpecHelper.spec Capybara::Window, requires: [:windows] do
194
194
 
195
195
  after do
196
196
  @session.current_window.resize_to(*@initial_size)
197
- sleep 0.5
197
+ sleep 1
198
198
  end
199
199
 
200
200
  it 'should be able to fullscreen the window' do
@@ -16,8 +16,7 @@ module Capybara
16
16
  config.filter_run_excluding requires: method(:filter).to_proc
17
17
  config.before { Capybara::SpecHelper.reset! }
18
18
  config.after { Capybara::SpecHelper.reset! }
19
- # Test in 3.5+ where metadata doesn't autotrigger shared context inclusion - will be only behavior in RSpec 4
20
- config.shared_context_metadata_behavior = :apply_to_host_groups if config.respond_to?(:shared_context_metadata_behavior=)
19
+ config.shared_context_metadata_behavior = :apply_to_host_groups
21
20
  end
22
21
 
23
22
  def reset!
@@ -3,6 +3,9 @@
3
3
  <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
4
4
  <title>with_animation</title>
5
5
  <script src="/jquery.js" type="text/javascript" charset="utf-8"></script>
6
+ <script>
7
+ $(document).on('contextmenu', function(e){ e.preventDefault(); })
8
+ </script>
6
9
  <style>
7
10
  div {
8
11
  width: 400px;