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
@@ -3,7 +3,7 @@
3
3
  require 'capybara/rspec/matchers/have_selector'
4
4
  require 'capybara/rspec/matchers/match_selector'
5
5
  require 'capybara/rspec/matchers/have_current_path'
6
- require 'capybara/rspec/matchers/have_style'
6
+ require 'capybara/rspec/matchers/match_style'
7
7
  require 'capybara/rspec/matchers/have_text'
8
8
  require 'capybara/rspec/matchers/have_title'
9
9
  require 'capybara/rspec/matchers/become_closed'
@@ -124,9 +124,17 @@ module Capybara
124
124
  end
125
125
 
126
126
  # RSpec matcher for element style
127
- # See {Capybara::Node::Matchers#has_style?}
127
+ # See {Capybara::Node::Matchers#matches_style?}
128
+ def match_style(styles, **options)
129
+ Matchers::MatchStyle.new(styles, options)
130
+ end
131
+
132
+ ##
133
+ # @deprecated
134
+ #
128
135
  def have_style(styles, **options)
129
- Matchers::HaveStyle.new(styles, options)
136
+ warn 'DEPRECATED: have_style is deprecated, please use match_style'
137
+ match_style(styles, **options)
130
138
  end
131
139
 
132
140
  %w[selector css xpath text title current_path link button
@@ -35,6 +35,7 @@ module Capybara
35
35
  # * :unchecked (Boolean) — Match unchecked fields?
36
36
  # * :disabled (Boolean) — Match disabled field?
37
37
  # * :multiple (Boolean) — Match fields that accept multiple values
38
+ # * :style (String, Regexp, Hash)
38
39
  #
39
40
  # * **:fieldset** - Select fieldset elements
40
41
  # * Locator: Matches id or contents of wrapped legend
@@ -42,6 +43,7 @@ module Capybara
42
43
  # * :id (String, Regexp, XPath::Expression) — Matches id attribute
43
44
  # * :legend (String) — Matches contents of wrapped legend
44
45
  # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
46
+ # * :style (String, Regexp, Hash)
45
47
  #
46
48
  # * **:link** - Find links ( <a> elements with an href attribute )
47
49
  # * Locator: Matches the id or title attributes, or the string content of the link, or the alt attribute of a contained img element
@@ -51,6 +53,7 @@ module Capybara
51
53
  # * :alt (String) — Matches the alt attribute of a contained img element
52
54
  # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
53
55
  # * :href (String, Regexp, nil) — Matches the normalized href of the link, if nil will find <a> elements with no href attribute
56
+ # * :style (String, Regexp, Hash)
54
57
  #
55
58
  # * **:button** - Find buttons ( input [of type submit, reset, image, button] or button elements )
56
59
  # * Locator: Matches the id, Capybara.test_id attribute, value, or title attributes, string content of a button, or the alt attribute of an image type button or of a descendant image of a button
@@ -60,6 +63,7 @@ module Capybara
60
63
  # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
61
64
  # * :value (String) — Matches the value of an input button
62
65
  # * :type
66
+ # * :style (String, Regexp, Hash)
63
67
  #
64
68
  # * **:link_or_button** - Find links or buttons
65
69
  # * Locator: See :link and :button selectors
@@ -75,6 +79,7 @@ module Capybara
75
79
  # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
76
80
  # * :disabled (Boolean) — Match disabled field?
77
81
  # * :multiple (Boolean) — Match fields that accept multiple values
82
+ # * :style (String, Regexp, Hash)
78
83
  #
79
84
  # * **:radio_button** - Find radio buttons
80
85
  # * Locator: Match id, Capybara.test_id attribute, name, or associated label text
@@ -86,6 +91,7 @@ module Capybara
86
91
  # * :unchecked (Boolean) — Match unchecked fields?
87
92
  # * :disabled (Boolean) — Match disabled field?
88
93
  # * :option (String) — Match the value
94
+ # * :style (String, Regexp, Hash)
89
95
  #
90
96
  # * **:checkbox** - Find checkboxes
91
97
  # * Locator: Match id, Capybara.test_id attribute, name, or associated label text
@@ -97,6 +103,7 @@ module Capybara
97
103
  # * *:unchecked (Boolean) — Match unchecked fields?
98
104
  # * *:disabled (Boolean) — Match disabled field?
99
105
  # * *:option (String) — Match the value
106
+ # * :style (String, Regexp, Hash)
100
107
  #
101
108
  # * **:select** - Find select elements
102
109
  # * Locator: Match id, Capybara.test_id attribute, name, placeholder, or associated label text
@@ -111,6 +118,7 @@ module Capybara
111
118
  # * :with_options (Array<String>) — Partial match options
112
119
  # * :selected (String, Array<String>) — Match the selection(s)
113
120
  # * :with_selected (String, Array<String>) — Partial match the selection(s)
121
+ # * :style (String, Regexp, Hash)
114
122
  #
115
123
  # * **:option** - Find option elements
116
124
  # * Locator: Match text of option
@@ -136,6 +144,7 @@ module Capybara
136
144
  # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
137
145
  # * :disabled (Boolean) — Match disabled field?
138
146
  # * :multiple (Boolean) — Match field that accepts multiple values
147
+ # * :style (String, Regexp, Hash)
139
148
  #
140
149
  # * **:label** - Find label elements
141
150
  # * Locator: Match id or text contents
@@ -148,6 +157,7 @@ module Capybara
148
157
  # * :id (String, Regexp, XPath::Expression) — Match id attribute of table
149
158
  # * :caption (String) — Match text of associated caption
150
159
  # * :class ((String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
160
+ # * :style (String, Regexp, Hash)
151
161
  #
152
162
  # * **:frame** - Find frame/iframe elements
153
163
  # * Locator: Match id or name
@@ -155,6 +165,7 @@ module Capybara
155
165
  # * :id (String, Regexp, XPath::Expression) — Match id attribute
156
166
  # * :name (String) — Match name attribute
157
167
  # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
168
+ # * :style (String, Regexp, Hash)
158
169
  #
159
170
  # * **:element**
160
171
  # * Locator: Type of element ('div', 'a', etc) - if not specified defaults to '*'
@@ -173,8 +184,8 @@ module Capybara
173
184
  all.fetch(name.to_sym) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
174
185
  end
175
186
 
176
- def add(name, &block)
177
- all[name.to_sym] = Capybara::Selector.new(name.to_sym, &block)
187
+ def add(name, **options, &block)
188
+ all[name.to_sym] = Capybara::Selector.new(name.to_sym, **options, &block)
178
189
  end
179
190
 
180
191
  def update(name, &block)
@@ -190,7 +201,7 @@ module Capybara
190
201
  end
191
202
  end
192
203
 
193
- def initialize(name, &block)
204
+ def initialize(name, locator_type: nil, raw_locator: false, &block)
194
205
  @name = name
195
206
  @filter_set = FilterSet.add(name) {}
196
207
  @match = nil
@@ -201,6 +212,8 @@ module Capybara
201
212
  @expression_filters = {}
202
213
  @locator_filter = nil
203
214
  @default_visibility = nil
215
+ @locator_type = locator_type
216
+ @raw_locator = raw_locator
204
217
  @config = {
205
218
  enable_aria_label: false,
206
219
  test_id: nil
@@ -301,6 +314,8 @@ module Capybara
301
314
  else
302
315
  warn 'Selector has no format'
303
316
  end
317
+ ensure
318
+ warn "Locator #{locator.inspect} must #{locator_description}. This will raise an error in a future version of Capybara." unless locator_valid?(locator)
304
319
  end
305
320
 
306
321
  ##
@@ -428,8 +443,37 @@ module Capybara
428
443
  Thread.current["capybara_#{object_id}_errors"] = nil
429
444
  end
430
445
 
446
+ # @api private
447
+ def raw_locator?
448
+ !!@raw_locator
449
+ end
450
+
431
451
  private
432
452
 
453
+ def locator_types
454
+ return nil unless @locator_type
455
+
456
+ Array(@locator_type)
457
+ end
458
+
459
+ def locator_valid?(locator)
460
+ return true unless locator && locator_types
461
+
462
+ locator_types&.any? do |type_or_method|
463
+ type_or_method.is_a?(Symbol) ? locator.respond_to?(type_or_method) : type_or_method === locator # rubocop:disable Style/CaseEquality
464
+ end
465
+ end
466
+
467
+ def locator_description
468
+ locator_types.group_by { |lt| lt.is_a? Symbol }.map do |symbol, types_or_methods|
469
+ if symbol
470
+ "respond to #{types_or_methods.join(' or ')}"
471
+ else
472
+ "be an instance of #{types_or_methods.join(' or ')}"
473
+ end
474
+ end.join(' or ')
475
+ end
476
+
433
477
  def errors
434
478
  Thread.current["capybara_#{object_id}_errors"] || []
435
479
  end
@@ -25,24 +25,24 @@ end
25
25
 
26
26
  # rubocop:disable Metrics/BlockLength
27
27
 
28
- Capybara.add_selector(:xpath) do
28
+ Capybara.add_selector(:xpath, locator_type: [:to_xpath, String], raw_locator: true) do
29
29
  xpath { |xpath| xpath }
30
30
  end
31
31
 
32
- Capybara.add_selector(:css) do
32
+ Capybara.add_selector(:css, locator_type: [String, Symbol], raw_locator: true) do
33
33
  css { |css| css }
34
34
  end
35
35
 
36
- Capybara.add_selector(:id) do
36
+ Capybara.add_selector(:id, locator_type: [String, Symbol, Regexp]) do
37
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
 
41
- Capybara.add_selector(:field) do
42
- visible { |options| :hidden if options[:type] == 'hidden' }
41
+ Capybara.add_selector(:field, locator_type: [String, Symbol]) do
42
+ visible { |options| :hidden if options[:type].to_s == 'hidden' }
43
43
  xpath do |locator, **options|
44
44
  invalid_types = %w[submit image]
45
- invalid_types << 'hidden' unless options[:type] == 'hidden'
45
+ invalid_types << 'hidden' unless options[:type].to_s == 'hidden'
46
46
  xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of(*invalid_types)]
47
47
  locate_field(xpath, locator, options)
48
48
  end
@@ -78,7 +78,7 @@ Capybara.add_selector(:field) do
78
78
  end
79
79
  end
80
80
 
81
- Capybara.add_selector(:fieldset) do
81
+ Capybara.add_selector(:fieldset, locator_type: [String, Symbol]) do
82
82
  xpath do |locator, legend: nil, **|
83
83
  locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]
84
84
  locator_matchers |= XPath.attr(test_id) == locator.to_s if test_id
@@ -90,7 +90,7 @@ Capybara.add_selector(:fieldset) do
90
90
  node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
91
91
  end
92
92
 
93
- Capybara.add_selector(:link) do
93
+ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
94
94
  xpath do |locator, href: true, alt: nil, title: nil, **|
95
95
  xpath = builder(XPath.descendant(:a)).add_attribute_conditions(href: href)
96
96
 
@@ -132,7 +132,7 @@ Capybara.add_selector(:link) do
132
132
  end
133
133
  end
134
134
 
135
- Capybara.add_selector(:button) do
135
+ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
136
136
  xpath(:value, :title, :type) do |locator, **options|
137
137
  input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
138
138
  btn_xpath = XPath.descendant(:button)
@@ -166,7 +166,7 @@ Capybara.add_selector(:button) do
166
166
  end
167
167
  end
168
168
 
169
- Capybara.add_selector(:link_or_button) do
169
+ Capybara.add_selector(:link_or_button, locator_type: [String, Symbol]) do
170
170
  label 'link or button'
171
171
  xpath do |locator, **options|
172
172
  self.class.all.values_at(:link, :button).map do |selector|
@@ -181,9 +181,8 @@ Capybara.add_selector(:link_or_button) do
181
181
  end
182
182
  end
183
183
 
184
- Capybara.add_selector(:fillable_field) do
184
+ Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
185
185
  label 'field'
186
-
187
186
  xpath do |locator, allow_self: nil, **options|
188
187
  xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input, :textarea)[
189
188
  !XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')
@@ -215,9 +214,8 @@ Capybara.add_selector(:fillable_field) do
215
214
  end
216
215
  end
217
216
 
218
- Capybara.add_selector(:radio_button) do
217
+ Capybara.add_selector(:radio_button, locator_type: [String, Symbol]) do
219
218
  label 'radio button'
220
-
221
219
  xpath do |locator, allow_self: nil, **options|
222
220
  xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
223
221
  XPath.attr(:type) == 'radio'
@@ -240,7 +238,7 @@ Capybara.add_selector(:radio_button) do
240
238
  end
241
239
  end
242
240
 
243
- Capybara.add_selector(:checkbox) do
241
+ Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
244
242
  xpath do |locator, allow_self: nil, **options|
245
243
  xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
246
244
  XPath.attr(:type) == 'checkbox'
@@ -263,7 +261,7 @@ Capybara.add_selector(:checkbox) do
263
261
  end
264
262
  end
265
263
 
266
- Capybara.add_selector(:select) do
264
+ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
267
265
  label 'select box'
268
266
 
269
267
  xpath do |locator, **options|
@@ -320,7 +318,7 @@ Capybara.add_selector(:select) do
320
318
  end
321
319
  end
322
320
 
323
- Capybara.add_selector(:datalist_input) do
321
+ Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
324
322
  label 'input box with datalist completion'
325
323
 
326
324
  xpath do |locator, **options|
@@ -355,7 +353,7 @@ Capybara.add_selector(:datalist_input) do
355
353
  end
356
354
  end
357
355
 
358
- Capybara.add_selector(:option) do
356
+ Capybara.add_selector(:option, locator_type: [String, Symbol]) do
359
357
  xpath do |locator|
360
358
  xpath = XPath.descendant(:option)
361
359
  xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil?
@@ -373,7 +371,7 @@ Capybara.add_selector(:option) do
373
371
  end
374
372
  end
375
373
 
376
- Capybara.add_selector(:datalist_option) do
374
+ Capybara.add_selector(:datalist_option, locator_type: [String, Symbol]) do
377
375
  label 'datalist option'
378
376
  visible(:all)
379
377
 
@@ -390,7 +388,7 @@ Capybara.add_selector(:datalist_option) do
390
388
  end
391
389
  end
392
390
 
393
- Capybara.add_selector(:file_field) do
391
+ Capybara.add_selector(:file_field, locator_type: [String, Symbol]) do
394
392
  label 'file field'
395
393
  xpath do |locator, allow_self: nil, **options|
396
394
  xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
@@ -404,7 +402,7 @@ Capybara.add_selector(:file_field) do
404
402
  describe_expression_filters
405
403
  end
406
404
 
407
- Capybara.add_selector(:label) do
405
+ Capybara.add_selector(:label, locator_type: [String, Symbol]) do
408
406
  label 'label'
409
407
  xpath(:for) do |locator, options|
410
408
  xpath = XPath.descendant(:label)
@@ -413,12 +411,14 @@ Capybara.add_selector(:label) do
413
411
  locator_matchers |= XPath.attr(test_id) == locator if test_id
414
412
  xpath = xpath[locator_matchers]
415
413
  end
416
- if options.key?(:for) && !options[:for].is_a?(Capybara::Node::Element)
417
- with_attr = XPath.attr(:for) == options[:for].to_s
418
- labelable_elements = %i[button input keygen meter output progress select textarea]
419
- wrapped = !XPath.attr(:for) &
420
- XPath.descendant(*labelable_elements)[XPath.attr(:id) == options[:for].to_s]
421
- xpath = xpath[with_attr | wrapped]
414
+ if options.key?(:for)
415
+ if (for_option = options[:for].is_a?(Capybara::Node::Element) ? options[:for][:id] : options[:for])
416
+ with_attr = XPath.attr(:for) == for_option.to_s
417
+ labelable_elements = %i[button input keygen meter output progress select textarea]
418
+ wrapped = !XPath.attr(:for) &
419
+ XPath.descendant(*labelable_elements)[XPath.attr(:id) == for_option.to_s]
420
+ xpath = xpath[with_attr | wrapped]
421
+ end
422
422
  end
423
423
  xpath
424
424
  end
@@ -442,7 +442,7 @@ Capybara.add_selector(:label) do
442
442
  end
443
443
  end
444
444
 
445
- Capybara.add_selector(:table) do
445
+ Capybara.add_selector(:table, locator_type: [String, Symbol]) do
446
446
  xpath do |locator, caption: nil, **|
447
447
  xpath = XPath.descendant(:table)
448
448
  unless locator.nil?
@@ -459,7 +459,7 @@ Capybara.add_selector(:table) do
459
459
  end
460
460
  end
461
461
 
462
- Capybara.add_selector(:frame) do
462
+ Capybara.add_selector(:frame, locator_type: [String, Symbol]) do
463
463
  xpath do |locator, name: nil, **|
464
464
  xpath = XPath.descendant(:iframe).union(XPath.descendant(:frame))
465
465
  unless locator.nil?
@@ -475,7 +475,7 @@ Capybara.add_selector(:frame) do
475
475
  end
476
476
  end
477
477
 
478
- Capybara.add_selector(:element) do
478
+ Capybara.add_selector(:element, locator_type: [String, Symbol]) do
479
479
  xpath do |locator, **|
480
480
  XPath.descendant.where(locator ? XPath.local_name == locator.to_s : nil)
481
481
  end
@@ -4,6 +4,8 @@ require 'uri'
4
4
  require 'English'
5
5
 
6
6
  class Capybara::Selenium::Driver < Capybara::Driver::Base
7
+ include Capybara::Selenium::Find
8
+
7
9
  DEFAULT_OPTIONS = {
8
10
  browser: :firefox,
9
11
  clear_local_storage: nil,
@@ -73,14 +75,6 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
73
75
  browser.current_url
74
76
  end
75
77
 
76
- def find_xpath(selector)
77
- browser.find_elements(:xpath, selector).map(&method(:build_node))
78
- end
79
-
80
- def find_css(selector)
81
- browser.find_elements(:css, selector).map(&method(:build_node))
82
- end
83
-
84
78
  def wait?; true; end
85
79
  def needs_server?; true; end
86
80
 
@@ -348,8 +342,12 @@ private
348
342
  end
349
343
  end
350
344
 
351
- def build_node(native_node)
352
- ::Capybara::Selenium::Node.new(self, native_node)
345
+ def find_context
346
+ browser
347
+ end
348
+
349
+ def build_node(native_node, initial_cache = {})
350
+ ::Capybara::Selenium::Node.new(self, native_node, initial_cache)
353
351
  end
354
352
 
355
353
  def specialize_driver(sel_driver)
@@ -359,6 +357,8 @@ private
359
357
  when :firefox
360
358
  require 'capybara/selenium/patches/pause_duration_fix' if pause_broken?(sel_driver)
361
359
  extend FirefoxDriver if sel_driver.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
360
+ when :ie, :internet_explorer
361
+ extend InternetExplorerDriver
362
362
  end
363
363
  end
364
364
 
@@ -385,7 +385,7 @@ private
385
385
  until find_xpath('/html/body/*').empty?
386
386
  raise Capybara::ExpectationNotMet, 'Timed out waiting for Selenium session reset' if timer.expired?
387
387
 
388
- sleep 0.05
388
+ sleep 0.01
389
389
  end
390
390
  end
391
391
 
@@ -402,3 +402,4 @@ end
402
402
 
403
403
  require 'capybara/selenium/driver_specializations/chrome_driver'
404
404
  require 'capybara/selenium/driver_specializations/firefox_driver'
405
+ require 'capybara/selenium/driver_specializations/internet_explorer_driver'
@@ -23,7 +23,7 @@ module Capybara::Selenium::Driver::ChromeDriver
23
23
 
24
24
  # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
25
25
  # and raises unnecessary error. Wait a bit and try again.
26
- sleep 0.5
26
+ sleep 0.25
27
27
  super
28
28
  end
29
29
 
@@ -51,8 +51,8 @@ private
51
51
  result['value']
52
52
  end
53
53
 
54
- def build_node(native_node)
55
- ::Capybara::Selenium::ChromeNode.new(self, native_node)
54
+ def build_node(native_node, initial_cache = {})
55
+ ::Capybara::Selenium::ChromeNode.new(self, native_node, initial_cache)
56
56
  end
57
57
 
58
58
  def bridge
@@ -44,7 +44,7 @@ module Capybara::Selenium::Driver::FirefoxDriver
44
44
 
45
45
  private
46
46
 
47
- def build_node(native_node)
48
- ::Capybara::Selenium::FirefoxNode.new(self, native_node)
47
+ def build_node(native_node, initial_cache = {})
48
+ ::Capybara::Selenium::FirefoxNode.new(self, native_node, initial_cache)
49
49
  end
50
50
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Selenium::Driver::InternetExplorerDriver
4
+ def switch_to_frame(frame)
5
+ return super unless frame == :parent
6
+
7
+ # iedriverserver has an issue if the current frame is removed from within it
8
+ # so we have to move to the default_content and iterate back through the frames
9
+ handles = @frame_handles[current_window_handle]
10
+ browser.switch_to.default_content
11
+ handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh) }
12
+ end
13
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Selenium
5
+ module Find
6
+ def find_xpath(selector, uses_visibility: false, styles: nil, **_options)
7
+ find_by(:xpath, selector, uses_visibility: uses_visibility, texts: [], styles: styles)
8
+ end
9
+
10
+ def find_css(selector, uses_visibility: false, texts: [], styles: nil, **_options)
11
+ find_by(:css, selector, uses_visibility: uses_visibility, texts: texts, styles: styles)
12
+ end
13
+
14
+ private
15
+
16
+ def find_by(format, selector, uses_visibility:, texts:, styles:)
17
+ els = find_context.find_elements(format, selector)
18
+ hints = []
19
+
20
+ if (els.size > 2) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
21
+ els = filter_by_text(els, texts) unless texts.empty?
22
+
23
+ hints_js = +''
24
+ functions = []
25
+ if uses_visibility && !is_displayed_atom.empty?
26
+ hints_js << <<~VISIBILITY_JS
27
+ var vis_func = #{is_displayed_atom};
28
+ VISIBILITY_JS
29
+ functions << 'vis_func'
30
+ end
31
+
32
+ if styles.is_a? Hash
33
+ hints_js << <<~STYLE_JS
34
+ var style_func = function(el){
35
+ var el_styles = window.getComputedStyle(el);
36
+ return #{styles.keys.map(&:to_s)}.reduce(function(res, style){
37
+ res[style] = el_styles[style];
38
+ return res;
39
+ }, {});
40
+ };
41
+ STYLE_JS
42
+ functions << 'style_func'
43
+ end
44
+
45
+ unless functions.empty?
46
+ hints_js << <<~EACH_JS
47
+ return arguments[0].map(function(el){
48
+ return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) }); });
49
+ EACH_JS
50
+
51
+ hints = es_context.execute_script hints_js, els
52
+ hints.map! do |results|
53
+ result = {}
54
+ result[:style] = results.pop if styles.is_a? Hash
55
+ result[:visible] = results.pop if uses_visibility
56
+ result
57
+ end
58
+ end
59
+ end
60
+ els.map.with_index { |el, idx| build_node(el, hints[idx] || {}) }
61
+ end
62
+
63
+ def filter_by_text(elements, texts)
64
+ es_context.execute_script <<~JS, elements, texts
65
+ var texts = arguments[1]
66
+ return arguments[0].filter(function(el){
67
+ var content = el.textContent.toLowerCase();
68
+ return texts.every(function(txt){ return content.indexOf(txt.toLowerCase()) != -1 });
69
+ })
70
+ JS
71
+ end
72
+
73
+ def es_context
74
+ respond_to?(:execute_script) ? self : driver
75
+ end
76
+
77
+ def is_displayed_atom # rubocop:disable Naming/PredicateName
78
+ @@is_displayed_atom ||= begin
79
+ browser.send(:bridge).send(:read_atom, 'isDisplayed')
80
+ rescue StandardError
81
+ # If the atom doesn't exist or other error
82
+ ""
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Selenium
5
+ module Scroll
6
+ def scroll_by(x, y)
7
+ driver.execute_script <<~JS, self, x, y
8
+ var el = arguments[0];
9
+ if (el.scrollBy){
10
+ el.scrollBy(arguments[1], arguments[2]);
11
+ } else {
12
+ el.scrollTop = el.scrollTop + arguments[2];
13
+ el.scrollLeft = el.scrollLeft + arguments[1];
14
+ }
15
+ JS
16
+ end
17
+
18
+ def scroll_to(element, location, position = nil)
19
+ # location, element = element, nil if element.is_a? Symbol
20
+ if element.is_a? Capybara::Selenium::Node
21
+ scroll_element_to_location(element, location)
22
+ elsif location.is_a? Symbol
23
+ scroll_to_location(location)
24
+ else
25
+ scroll_to_coords(*position)
26
+ end
27
+ self
28
+ end
29
+
30
+ private
31
+
32
+ def scroll_element_to_location(element, location)
33
+ scroll_opts = case location
34
+ when :top
35
+ 'true'
36
+ when :bottom
37
+ 'false'
38
+ when :center
39
+ "{behavior: 'instant', block: 'center'}"
40
+ else
41
+ raise ArgumentError, "Invalid scroll_to location: #{location}"
42
+ end
43
+ driver.execute_script <<~JS, element
44
+ arguments[0].scrollIntoView(#{scroll_opts})
45
+ JS
46
+ end
47
+
48
+ def scroll_to_location(location)
49
+ scroll_y = case location
50
+ when :top
51
+ '0'
52
+ when :bottom
53
+ 'arguments[0].scrollHeight'
54
+ when :center
55
+ '(arguments[0].scrollHeight - arguments[0].clientHeight)/2'
56
+ end
57
+ driver.execute_script <<~JS, self
58
+ if (arguments[0].scrollTo){
59
+ arguments[0].scrollTo(0, #{scroll_y});
60
+ } else {
61
+ arguments[0].scrollTop = #{scroll_y};
62
+ }
63
+ JS
64
+ end
65
+
66
+ def scroll_to_coords(x, y)
67
+ driver.execute_script <<~JS, self, x, y
68
+ if (arguments[0].scrollTo){
69
+ arguments[0].scrollTo(arguments[1], arguments[2]);
70
+ } else {
71
+ arguments[0].scrollTop = arguments[2];
72
+ arguments[0].scrollLeft = arguments[1];
73
+ }
74
+ JS
75
+ end
76
+ end
77
+ end
78
+ end