capybara 3.12.0 → 3.13.2

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +27 -0
  3. data/README.md +13 -3
  4. data/lib/capybara/config.rb +3 -1
  5. data/lib/capybara/driver/base.rb +2 -2
  6. data/lib/capybara/driver/node.rb +11 -2
  7. data/lib/capybara/minitest/spec.rb +10 -3
  8. data/lib/capybara/minitest.rb +3 -3
  9. data/lib/capybara/node/actions.rb +4 -0
  10. data/lib/capybara/node/base.rb +13 -5
  11. data/lib/capybara/node/document.rb +12 -0
  12. data/lib/capybara/node/element.rb +37 -0
  13. data/lib/capybara/node/finders.rb +1 -0
  14. data/lib/capybara/node/matchers.rb +19 -4
  15. data/lib/capybara/node/simple.rb +7 -2
  16. data/lib/capybara/queries/selector_query.rb +93 -9
  17. data/lib/capybara/rspec/matchers/become_closed.rb +1 -1
  18. data/lib/capybara/rspec/matchers/match_style.rb +38 -0
  19. data/lib/capybara/rspec/matchers.rb +11 -3
  20. data/lib/capybara/selector/selector.rb +47 -3
  21. data/lib/capybara/selector.rb +30 -30
  22. data/lib/capybara/selenium/driver.rb +12 -11
  23. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +3 -3
  24. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +2 -2
  25. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +13 -0
  26. data/lib/capybara/selenium/extensions/find.rb +87 -0
  27. data/lib/capybara/selenium/extensions/scroll.rb +78 -0
  28. data/lib/capybara/selenium/node.rb +52 -28
  29. data/lib/capybara/session.rb +13 -1
  30. data/lib/capybara/spec/public/test.js +1 -0
  31. data/lib/capybara/spec/session/assert_style_spec.rb +4 -4
  32. data/lib/capybara/spec/session/attach_file_spec.rb +6 -6
  33. data/lib/capybara/spec/session/click_button_spec.rb +1 -1
  34. data/lib/capybara/spec/session/evaluate_script_spec.rb +1 -0
  35. data/lib/capybara/spec/session/execute_script_spec.rb +1 -0
  36. data/lib/capybara/spec/session/fill_in_spec.rb +7 -1
  37. data/lib/capybara/spec/session/find_field_spec.rb +1 -1
  38. data/lib/capybara/spec/session/find_spec.rb +11 -0
  39. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +0 -1
  40. data/lib/capybara/spec/session/has_css_spec.rb +32 -0
  41. data/lib/capybara/spec/session/has_select_spec.rb +2 -2
  42. data/lib/capybara/spec/session/has_selector_spec.rb +7 -0
  43. data/lib/capybara/spec/session/has_text_spec.rb +1 -1
  44. data/lib/capybara/spec/session/matches_style_spec.rb +35 -0
  45. data/lib/capybara/spec/session/scroll_spec.rb +117 -0
  46. data/lib/capybara/spec/session/select_spec.rb +5 -0
  47. data/lib/capybara/spec/spec_helper.rb +1 -0
  48. data/lib/capybara/spec/views/obscured.erb +1 -1
  49. data/lib/capybara/spec/views/scroll.erb +20 -0
  50. data/lib/capybara/spec/views/with_html.erb +1 -1
  51. data/lib/capybara/version.rb +1 -1
  52. data/lib/capybara/window.rb +1 -1
  53. data/lib/capybara.rb +8 -4
  54. data/spec/basic_node_spec.rb +11 -0
  55. data/spec/dsl_spec.rb +3 -1
  56. data/spec/minitest_spec.rb +2 -2
  57. data/spec/minitest_spec_spec.rb +1 -1
  58. data/spec/rack_test_spec.rb +4 -1
  59. data/spec/result_spec.rb +2 -2
  60. data/spec/selector_spec.rb +11 -1
  61. data/spec/selenium_spec_firefox.rb +1 -1
  62. data/spec/selenium_spec_ie.rb +70 -9
  63. data/spec/session_spec.rb +9 -0
  64. data/spec/shared_selenium_session.rb +4 -3
  65. data/spec/spec_helper.rb +2 -0
  66. metadata +38 -6
  67. data/lib/capybara/rspec/matchers/have_style.rb +0 -23
  68. data/lib/capybara/spec/session/has_style_spec.rb +0 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b3b4d3a612269d9ee165e7ed94e1e62f91c6fd85bcc5c5ae614c25d0c22bd32
4
- data.tar.gz: a2bdfa0c2398789bd15d791e3b2d0d03d9c3371033e4f4e4d21e302db845c93f
3
+ metadata.gz: 535d547d5a25d769afa754066f54cc0d873a2997ebfbc1ed12dd418d80cbb3f0
4
+ data.tar.gz: c576db6d285f055bfc3e030ff4973f7c3fecaf54bff9e74c64070408a2a579ac
5
5
  SHA512:
6
- metadata.gz: f4815632f5bc14c932e9b5181d68584ac3088f11241e8ee3c19f3ee0a27190f8ad1544b88a532f901eaf79f9ae3c2f721dfca1d2d6d20d098a40438042b9f64a
7
- data.tar.gz: eb2765462cdbdda76a4ca5d100b93688029d1ca727c00579c5e9e5d77f26fc4a0fe64bbe3616bb3f89b34e730c337e242b319b2ea606b5287a780f8219ee5b08
6
+ metadata.gz: 37be4e1475274ec82574b4839eb7a06b4db262ebacbd6534669561b92b4a87fcbfc73d57d058657a58b800ec95b86804d16ea663beb4407c6cb9247af100e283
7
+ data.tar.gz: b6b5996ff037790502bb89783f48441c6288e5241b967739a463dcf339c17bc1d3a2c5c4226bf7ab3eb502b82e56079347c96c5a55737edcd84477a85a4fa92e
data/History.md CHANGED
@@ -1,3 +1,30 @@
1
+ # Version 3.13.2
2
+ Release date: 2019-01-24
3
+
4
+ ### Fixed
5
+
6
+ * Remove extraneous output
7
+
8
+ # Version 3.13.1
9
+ Release date: 2019-01-24
10
+
11
+ ### Fixed
12
+
13
+ * Only use Selenium visibility optimization when JS atom is available - Issue #2151
14
+
15
+ # Version 3.13.0
16
+ Release date: 2019-01-23
17
+
18
+ ### Added
19
+
20
+ * Session#quit added
21
+ * #scroll_to added to allow scrolling page/elements to specified locations
22
+ * Speed optimizations around multiple element location and path generation when using the Selenium driver
23
+ * Support for locator type checking in custom selectors
24
+ * Allow configuration of gumbo use - defaults to off
25
+ * `assert_style`/`has_style`/`have_style` depreacted in favor of `assert_matches_style`/`matches_styles?`/`match_style`
26
+ * :style filter added to selectors
27
+
1
28
  # Version 3.12.0
2
29
  Release date: 2018-11-28
3
30
 
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jnicklas/capybara?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
7
7
  [![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=capybara&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=capybara&package-manager=bundler&version-scheme=semver)
8
8
 
9
- **Note** You are viewing the README for the 3.12.x version of Capybara.
9
+ **Note** You are viewing the README for the version 3.13.x of Capybara.
10
10
 
11
11
  Capybara helps you test web applications by simulating how a real user would
12
12
  interact with your app. It is agnostic about the driver running your tests and
@@ -35,6 +35,7 @@ GitHub): http://groups.google.com/group/ruby-capybara
35
35
  - [Selecting the Driver](#selecting-the-driver)
36
36
  - [RackTest](#racktest)
37
37
  - [Selenium](#selenium)
38
+ - [Apparition](#apparition)
38
39
  - [Capybara-webkit](#capybara-webkit)
39
40
  - [Poltergeist](#poltergeist)
40
41
  - [The DSL](#the-dsl)
@@ -217,7 +218,7 @@ feature "Signing in" do
217
218
  end
218
219
  ```
219
220
 
220
- `feature` is in fact just an alias for `describe ..., type:> :feature`,
221
+ `feature` is in fact just an alias for `describe ..., type: :feature`,
221
222
  `background` is an alias for `before`, `scenario` for `it`, and
222
223
  `given`/`given!` aliases for `let`/`let!`, respectively.
223
224
 
@@ -406,6 +407,15 @@ to the browsers. See the section on adding and configuring drivers.
406
407
  same transaction as your tests, causing data not to be shared between your test
407
408
  and test server, see [Transactions and database setup](#transactions-and-database-setup) below.
408
409
 
410
+ ### <a name="apparition"></a>Apparition
411
+
412
+ The [apparition driver](https://github.com/twalpole/apparition) in a new driver thatallows you to run tests using Chrome in a headless
413
+ or headed configuration. It attempts to provide backwards compatibility with the [Poltergeist driver API](https://github.com/teampoltergeist/poltergeist)
414
+ while allowing for the use of modern JS/CSS. It uses CDP to communicate with Chrome, thereby obviating the need for chromedriver.
415
+ A compatibility layer for capybara-webkit is planned, although has not yet been started. This driver is being developed by the
416
+ maintainer of Capybara and will attempt to keep up to date with new Capybara releases. It will probably be moved into the
417
+ teamcapybara repo once completely stable.
418
+
409
419
  ### <a name="capybara-webkit"></a>Capybara-webkit
410
420
 
411
421
  Note: `capybara-webkit` depends on QtWebkit which went EOL quite some time ago. There has been an attempt to revive the project but `capybara-webkit` is not yet (AFAIK) compatible with the revived version of QtWebKit (could be a good OSS project for someone) and as such is still limited to an old version of QtWebKit. This means its support for modern JS and CSS is severely limited.
@@ -428,7 +438,7 @@ Capybara.javascript_driver = :webkit
428
438
 
429
439
  ### <a name="poltergeist"></a>Poltergeist
430
440
 
431
- Note: `poltergeist` depends on PhantomJS for which active development ended quite some time ago (2.1.1). As such it is roughly equivalent to a 6-7 year old version of Safari, meaning lack of support for modern JS And CSS. If any effort to update PhantomJS succeeds in the future this situation could change.
441
+ Note: `poltergeist` depends on PhantomJS for which active development ended quite some time ago (2.1.1). As such it is roughly equivalent to a 6-7 year old version of Safari, meaning lack of support for modern JS and CSS. If any effort to update PhantomJS succeeds in the future this situation could change.
432
442
 
433
443
  [Poltergeist](https://github.com/teampoltergeist/poltergeist) is another
434
444
  headless driver which integrates Capybara with
@@ -7,12 +7,14 @@ module Capybara
7
7
  class Config
8
8
  extend Forwardable
9
9
 
10
- OPTIONS = %i[app reuse_server threadsafe default_wait_time server default_driver javascript_driver].freeze
10
+ OPTIONS = %i[app reuse_server threadsafe default_wait_time server
11
+ default_driver javascript_driver allow_gumbo].freeze
11
12
 
12
13
  attr_accessor :app
13
14
  attr_reader :reuse_server, :threadsafe
14
15
  attr_reader :session_options
15
16
  attr_writer :default_driver, :javascript_driver
17
+ attr_accessor :allow_gumbo
16
18
 
17
19
  SessionConfig::OPTIONS.each do |method|
18
20
  def_delegators :session_options, method, "#{method}="
@@ -15,11 +15,11 @@ class Capybara::Driver::Base
15
15
  raise NotImplementedError
16
16
  end
17
17
 
18
- def find_xpath(query)
18
+ def find_xpath(query, **options)
19
19
  raise NotImplementedError
20
20
  end
21
21
 
22
- def find_css(query)
22
+ def find_css(query, **options)
23
23
  raise NotImplementedError
24
24
  end
25
25
 
@@ -3,11 +3,12 @@
3
3
  module Capybara
4
4
  module Driver
5
5
  class Node
6
- attr_reader :driver, :native
6
+ attr_reader :driver, :native, :initial_cache
7
7
 
8
- def initialize(driver, native)
8
+ def initialize(driver, native, initial_cache = {})
9
9
  @driver = driver
10
10
  @native = native
11
+ @initial_cache = initial_cache
11
12
  end
12
13
 
13
14
  def all_text
@@ -68,6 +69,14 @@ module Capybara
68
69
  raise NotImplementedError
69
70
  end
70
71
 
72
+ def scroll_by(x, y)
73
+ raise NotImplementedError
74
+ end
75
+
76
+ def scroll_to(element, alignment, position = nil)
77
+ raise NotImplementedError
78
+ end
79
+
71
80
  def tag_name
72
81
  raise NotImplementedError
73
82
  end
@@ -17,7 +17,7 @@ module Capybara
17
17
  end + [%w[assert_all_of_selectors must_have_all_of_selectors],
18
18
  %w[assert_none_of_selectors must_have_none_of_selectors],
19
19
  %w[assert_any_of_selectors must_have_any_of_selectors],
20
- %w[assert_style must_have_style]] +
20
+ %w[assert_matches_style must_match_style]] +
21
21
  %w[selector xpath css].flat_map do |assertion|
22
22
  [%W[assert_matches_#{assertion} must_match_#{assertion}],
23
23
  %W[refute_matches_#{assertion} wont_match_#{assertion}]]
@@ -36,6 +36,13 @@ module Capybara
36
36
  end
37
37
  # rubocop:enable Style/MultilineBlockChain
38
38
 
39
+ ##
40
+ # @deprecated
41
+ def must_have_style(*args, &block)
42
+ warn 'must_have_style is deprecated, please use must_match_style'
43
+ must_match_style(*args, &block)
44
+ end
45
+
39
46
  ##
40
47
  # Expectation that there is xpath
41
48
  #
@@ -169,8 +176,8 @@ module Capybara
169
176
  ##
170
177
  # Expectation that element has style
171
178
  #
172
- # @!method must_have_style
173
- # see {Capybara::Node::Matchers#assert_style}
179
+ # @!method must_match_style
180
+ # see {Capybara::Node::Matchers#assert_matches_style}
174
181
  end
175
182
  end
176
183
  end
@@ -83,10 +83,10 @@ module Capybara
83
83
 
84
84
  ## Assert element has the provided CSS styles
85
85
  #
86
- # @!method assert_style
87
- # see {Capybara::Node::Matchers#assert_style}
86
+ # @!method assert_matches_style
87
+ # see {Capybara::Node::Matchers#assert_matches_style}
88
88
 
89
- %w[selector no_selector style
89
+ %w[selector no_selector matches_style
90
90
  all_of_selectors none_of_selectors any_of_selectors
91
91
  matches_selector not_matches_selector].each do |assertion_name|
92
92
  class_eval <<-ASSERTION, __FILE__, __LINE__ + 1
@@ -195,6 +195,8 @@ module Capybara
195
195
  #
196
196
  # @return [Capybara::Node::Element] The option element selected
197
197
  def select(value = nil, from: nil, **options)
198
+ raise ArgumentError, 'The :from option does not take an element' if from.is_a? Capybara::Node::Element
199
+
198
200
  el = from ? find_select_or_datalist_input(from, options) : self
199
201
 
200
202
  if el.respond_to?(:tag_name) && (el.tag_name == 'input')
@@ -221,6 +223,8 @@ module Capybara
221
223
  #
222
224
  # @return [Capybara::Node::Element] The option element unselected
223
225
  def unselect(value = nil, from: nil, **options)
226
+ raise ArgumentError, 'The :from option does not take an element' if from.is_a? Capybara::Node::Element
227
+
224
228
  scope = from ? find(:select, from, options) : self
225
229
  scope.find(:option, value, options).unselect_option
226
230
  end
@@ -86,7 +86,7 @@ module Capybara
86
86
  raise err unless driver.wait? && catch_error?(err, errors)
87
87
  raise err if timer.expired?
88
88
 
89
- sleep(0.05)
89
+ sleep(0.01)
90
90
  reload if session_options.automatic_reload
91
91
  retry
92
92
  ensure
@@ -95,13 +95,21 @@ module Capybara
95
95
  end
96
96
 
97
97
  # @api private
98
- def find_css(css)
99
- base.find_css(css)
98
+ def find_css(css, **options)
99
+ if base.method(:find_css).arity != 1
100
+ base.find_css(css, **options)
101
+ else
102
+ base.find_css(css)
103
+ end
100
104
  end
101
105
 
102
106
  # @api private
103
- def find_xpath(xpath)
104
- base.find_xpath(xpath)
107
+ def find_xpath(xpath, **options)
108
+ if base.method(:find_xpath).arity != 1
109
+ base.find_xpath(xpath, **options)
110
+ else
111
+ base.find_xpath(xpath)
112
+ end
105
113
  end
106
114
 
107
115
  # @api private
@@ -31,6 +31,18 @@ module Capybara
31
31
  def title
32
32
  session.driver.title
33
33
  end
34
+
35
+ def execute_script(*args)
36
+ find(:xpath, '/html').execute_script(*args)
37
+ end
38
+
39
+ def evaluate_script(*args)
40
+ find(:xpath, '/html').evaluate_script(*args)
41
+ end
42
+
43
+ def scroll_to(*args)
44
+ find(:xpath, '//body').scroll_to(*args)
45
+ end
34
46
  end
35
47
  end
36
48
  end
@@ -381,6 +381,38 @@ module Capybara
381
381
  self
382
382
  end
383
383
 
384
+ ##
385
+ #
386
+ # Scroll the page or element
387
+ #
388
+ # Scroll the page or element to its top, bottom or middle
389
+ # @overload scroll_to(position, offset: [0,0])
390
+ # @param [:top, :bottom, :center, :current] position
391
+ # @param :offset
392
+ #
393
+ # Scroll the page or current element until the given element is aligned at the top, bottom, or center of it
394
+ # @overload scroll_to(element, align: :top)
395
+ # @param [Capybara::Node::Element] element The element to be scrolled into view
396
+ # @param [:top, :bottom, :center] :align Where to align the element being scrolled into view with relation to the current page/element if possible
397
+ #
398
+ # @overload scroll_to(x,y)
399
+ # @param [Integer] x Horizontal scroll offset
400
+ # @param [Integer] y Vertical scroll offset
401
+ #
402
+ # @return [Capybara::Node::Element] The element
403
+ def scroll_to(pos_or_el_or_x, y = nil, align: :top, offset: nil)
404
+ case pos_or_el_or_x
405
+ when Symbol
406
+ synchronize { base.scroll_to(nil, pos_or_el_or_x) } unless pos_or_el_or_x == :current
407
+ when Capybara::Node::Element
408
+ synchronize { base.scroll_to(pos_or_el_or_x.base, align) }
409
+ else
410
+ synchronize { base.scroll_to(nil, nil, [pos_or_el_or_x, y]) }
411
+ end
412
+ synchronize { base.scroll_by(*offset) } unless offset.nil?
413
+ self
414
+ end
415
+
384
416
  ##
385
417
  #
386
418
  # Execute the given JS in the context of the element not returning a result. This is useful for scripts that return
@@ -454,6 +486,11 @@ module Capybara
454
486
  %(Obsolete #<Capybara::Node::Element>)
455
487
  end
456
488
 
489
+ # @api private
490
+ def initial_cache
491
+ base.respond_to?(:initial_cache) ? base.initial_cache : {}
492
+ end
493
+
457
494
  STYLE_SCRIPT = <<~JS
458
495
  (function(){
459
496
  var s = window.getComputedStyle(this);
@@ -36,6 +36,7 @@ module Capybara
36
36
  # + Absence of a class can be checked by prefixing the class name with !
37
37
  # + If you need to check for existence of a class name that starts with ! then prefix with !!
38
38
  # + ```class:['a', '!b', '!!!c'] # limit to elements with class 'a' and '!c' but not class 'b'```
39
+ # @option options [String, Regexp, Hash] style Only find elements with matching style. String and Regexp will be checked against text of the elements `style` attribute, while a Hash will be compared against the elements full style
39
40
  # @option options [Boolean] exact Control whether `is` expressions in the given XPath match exactly or partially
40
41
  # @option options [Symbol] match The matching strategy to use.
41
42
  #
@@ -56,13 +56,21 @@ module Capybara
56
56
  #
57
57
  # Checks if a an element has the specified CSS styles
58
58
  #
59
- # element.has_style?( 'color' => 'rgb(0,0,255)', 'font-size' => /px/ )
59
+ # element.matches_style?( 'color' => 'rgb(0,0,255)', 'font-size' => /px/ )
60
60
  #
61
61
  # @param styles [Hash]
62
62
  # @return [Boolean] If the styles match
63
63
  #
64
+ def matches_style?(styles, **options)
65
+ make_predicate(options) { assert_matches_style(styles, options) }
66
+ end
67
+
68
+ ##
69
+ # @deprecated
70
+ #
64
71
  def has_style?(styles, **options)
65
- make_predicate(options) { assert_style(styles, options) }
72
+ warn 'DEPRECATED: has_style? is deprecated, please use matches_style?'
73
+ matches_style?(styles, **options)
66
74
  end
67
75
 
68
76
  ##
@@ -108,12 +116,12 @@ module Capybara
108
116
  #
109
117
  # Asserts that an element has the specified CSS styles
110
118
  #
111
- # element.assert_style( 'color' => 'rgb(0,0,255)', 'font-size' => /px/ )
119
+ # element.assert_matches_style( 'color' => 'rgb(0,0,255)', 'font-size' => /px/ )
112
120
  #
113
121
  # @param styles [Hash]
114
122
  # @raise [Capybara::ExpectationNotMet] If the element doesn't have the specified styles
115
123
  #
116
- def assert_style(styles, **options)
124
+ def assert_matches_style(styles, **options)
117
125
  query_args = _set_query_session_options(styles, options)
118
126
  query = Capybara::Queries::StyleQuery.new(*query_args)
119
127
  synchronize(query.wait) do
@@ -122,6 +130,13 @@ module Capybara
122
130
  true
123
131
  end
124
132
 
133
+ ##
134
+ # @deprecated
135
+ def assert_style(styles, **options)
136
+ warn 'assert_style is deprecated, please use assert_matches_style instead'
137
+ assert_matches_style(styles, **options)
138
+ end
139
+
125
140
  # Asserts that all of the provided selectors are present on the given page
126
141
  # or descendants of the current node. If options are provided, the assertion
127
142
  # will check that each locator is present with those options as well (other than :wait).
@@ -164,12 +164,12 @@ module Capybara
164
164
  end
165
165
 
166
166
  # @api private
167
- def find_css(css)
167
+ def find_css(css, **_options)
168
168
  native.css(css)
169
169
  end
170
170
 
171
171
  # @api private
172
- def find_xpath(xpath)
172
+ def find_xpath(xpath, **_options)
173
173
  native.xpath(xpath)
174
174
  end
175
175
 
@@ -178,6 +178,11 @@ module Capybara
178
178
  Capybara.session_options
179
179
  end
180
180
 
181
+ # @api private
182
+ def initial_cache
183
+ {}
184
+ end
185
+
181
186
  private
182
187
 
183
188
  def option_value(option)
@@ -4,7 +4,7 @@ module Capybara
4
4
  module Queries
5
5
  class SelectorQuery < Queries::BaseQuery
6
6
  attr_reader :expression, :selector, :locator, :options
7
- VALID_KEYS = COUNT_KEYS + %i[text id class visible exact exact_text normalize_ws match wait filter_set]
7
+ VALID_KEYS = COUNT_KEYS + %i[text id class style visible exact exact_text normalize_ws match wait filter_set]
8
8
  VALID_MATCH = %i[first smart prefer_exact one].freeze
9
9
 
10
10
  def initialize(*args,
@@ -14,6 +14,7 @@ module Capybara
14
14
  **options,
15
15
  &filter_block)
16
16
  @resolved_node = nil
17
+ @resolved_count = 0
17
18
  @options = options.dup
18
19
  super(@options)
19
20
  self.session_options = session_options
@@ -50,9 +51,24 @@ module Capybara
50
51
  end
51
52
  desc << " with id #{options[:id]}" if options[:id]
52
53
  desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
54
+ desc << case options[:style]
55
+ when String
56
+ " with style attribute #{options[:style].inspect}"
57
+ when Regexp
58
+ " with style attribute matching #{options[:style].inspect}"
59
+ when Hash
60
+ " with styles #{options[:style].inspect}"
61
+ else ''
62
+ end
53
63
  desc << selector.description(node_filters: show_for[:node], **options)
54
64
  desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]
55
65
  desc << " within #{@resolved_node.inspect}" if describe_within?
66
+ if locator.is_a?(String) && locator.start_with?('#', './/', '//')
67
+ unless selector.raw_locator?
68
+ desc << "\nNote: It appears you may be passing a CSS selector or XPath expression rather than a locator. " \
69
+ "Please see the documentation for acceptable locator values.\n\n"
70
+ end
71
+ end
56
72
  desc
57
73
  end
58
74
 
@@ -91,7 +107,9 @@ module Capybara
91
107
  exact = exact? if exact.nil?
92
108
  expr = apply_expression_filters(@expression)
93
109
  expr = exact ? expr.to_xpath(:exact) : expr.to_s if expr.respond_to?(:to_xpath)
94
- filtered_expression(expr)
110
+ expr = filtered_expression(expr)
111
+ expr = "(#{expr})[#{xpath_text_conditions}]" if try_text_match_in_expression?
112
+ expr
95
113
  end
96
114
 
97
115
  def css
@@ -102,6 +120,7 @@ module Capybara
102
120
  def resolve_for(node, exact = nil)
103
121
  applied_filters.clear
104
122
  @resolved_node = node
123
+ @resolved_count += 1
105
124
  node.synchronize do
106
125
  children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
107
126
  Capybara::Result.new(children, self)
@@ -123,6 +142,26 @@ module Capybara
123
142
 
124
143
  private
125
144
 
145
+ def text_fragments
146
+ text = (options[:text] || options[:exact_text])
147
+ text.is_a?(String) ? text.split : []
148
+ end
149
+
150
+ def xpath_text_conditions
151
+ (options[:text] || options[:exact_text]).split.map { |txt| XPath.contains(txt) }.reduce(&:&)
152
+ end
153
+
154
+ def try_text_match_in_expression?
155
+ first_try? &&
156
+ (options[:text] || options[:exact_text]).is_a?(String) &&
157
+ @resolved_node&.respond_to?(:session) &&
158
+ @resolved_node.session.driver.wait?
159
+ end
160
+
161
+ def first_try?
162
+ @resolved_count == 1
163
+ end
164
+
126
165
  def show_for_stage(only_applied)
127
166
  lambda do |stage = :any|
128
167
  !only_applied || (stage == :any ? applied_filters.any? : applied_filters.include?(stage))
@@ -141,10 +180,25 @@ module Capybara
141
180
  end
142
181
 
143
182
  def find_nodes_by_selector_format(node, exact)
183
+ hints = {}
184
+ hints[:uses_visibility] = true unless visible == :all
185
+ hints[:texts] = text_fragments unless selector.format == :xpath
186
+ hints[:styles] = options[:style] if use_default_style_filter?
187
+
144
188
  if selector.format == :css
145
- node.find_css(css)
189
+ if node.method(:find_css).arity != 1
190
+ node.find_css(css, **hints)
191
+ else
192
+ node.find_css(css)
193
+ end
194
+ elsif selector.format == :xpath
195
+ if node.method(:find_xpath).arity != 1
196
+ node.find_xpath(xpath(exact), **hints)
197
+ else
198
+ node.find_xpath(xpath(exact))
199
+ end
146
200
  else
147
- node.find_xpath(xpath(exact))
201
+ raise ArgumentError, "Unknown format: #{selector.format}"
148
202
  end
149
203
  end
150
204
 
@@ -237,6 +291,7 @@ module Capybara
237
291
  conditions = {}
238
292
  conditions[:id] = options[:id] if use_default_id_filter?
239
293
  conditions[:class] = options[:class] if use_default_class_filter?
294
+ conditions[:style] = options[:style] if use_default_style_filter? && !options[:style].is_a?(Hash)
240
295
  builder(expr).add_attribute_conditions(conditions)
241
296
  end
242
297
 
@@ -248,6 +303,10 @@ module Capybara
248
303
  options.key?(:class) && !custom_keys.include?(:class)
249
304
  end
250
305
 
306
+ def use_default_style_filter?
307
+ options.key?(:style) && !custom_keys.include?(:style)
308
+ end
309
+
251
310
  def apply_expression_filters(expression)
252
311
  unapplied_options = options.keys - valid_keys
253
312
  expression_filters.inject(expression) do |expr, (name, ef)|
@@ -298,11 +357,12 @@ module Capybara
298
357
  def matches_system_filters?(node)
299
358
  applied_filters << :system
300
359
 
301
- matches_id_filter?(node) &&
360
+ matches_visible_filter?(node) &&
361
+ matches_id_filter?(node) &&
302
362
  matches_class_filter?(node) &&
363
+ matches_style_filter?(node) &&
303
364
  matches_text_filter?(node) &&
304
- matches_exact_text_filter?(node) &&
305
- matches_visible_filter?(node)
365
+ matches_exact_text_filter?(node)
306
366
  end
307
367
 
308
368
  def matches_id_filter?(node)
@@ -317,6 +377,28 @@ module Capybara
317
377
  node[:class] =~ options[:class]
318
378
  end
319
379
 
380
+ def matches_style_filter?(node)
381
+ case options[:style]
382
+ when String, nil
383
+ true
384
+ when Regexp
385
+ node[:style] =~ options[:style]
386
+ when Hash
387
+ matches_style?(node, options[:style])
388
+ end
389
+ end
390
+
391
+ def matches_style?(node, styles)
392
+ @actual_styles = node.initial_cache[:style] || node.style(*styles.keys)
393
+ styles.all? do |style, value|
394
+ if value.is_a? Regexp
395
+ @actual_styles[style.to_s] =~ value
396
+ else
397
+ @actual_styles[style.to_s] == value
398
+ end
399
+ end
400
+ end
401
+
320
402
  def matches_text_filter?(node)
321
403
  value = options[:text]
322
404
  return true unless value
@@ -334,8 +416,10 @@ module Capybara
334
416
 
335
417
  def matches_visible_filter?(node)
336
418
  case visible
337
- when :visible then node.visible?
338
- when :hidden then !node.visible?
419
+ when :visible then
420
+ node.initial_cache[:visible] || (node.initial_cache[:visible].nil? && node.visible?)
421
+ when :hidden then
422
+ (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
339
423
  else true
340
424
  end
341
425
  end
@@ -15,7 +15,7 @@ module Capybara
15
15
  while window.exists?
16
16
  return false if timer.expired?
17
17
 
18
- sleep 0.05
18
+ sleep 0.01
19
19
  end
20
20
  true
21
21
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/rspec/matchers/base'
4
+
5
+ module Capybara
6
+ module RSpecMatchers
7
+ module Matchers
8
+ class MatchStyle < WrappedElementMatcher
9
+ def element_matches?(el)
10
+ el.assert_matches_style(*@args)
11
+ end
12
+
13
+ def does_not_match?(_actual)
14
+ raise ArgumentError, 'The match_style matcher does not support use with not_to/should_not'
15
+ end
16
+
17
+ def description
18
+ 'match style'
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ module Capybara
26
+ module RSpecMatchers
27
+ module Matchers
28
+ ##
29
+ # @deprecated
30
+ class HaveStyle < MatchStyle
31
+ def initialize(*args, &filter_block)
32
+ warn 'HaveStyle matcher is deprecated, please use the MatchStyle matcher instead'
33
+ super
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end