capybara 3.14.0 → 3.15.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +13 -0
  3. data/README.md +2 -1
  4. data/lib/capybara/node/actions.rb +37 -6
  5. data/lib/capybara/node/matchers.rb +10 -2
  6. data/lib/capybara/selector.rb +117 -1
  7. data/lib/capybara/selector/selector.rb +7 -0
  8. data/lib/capybara/selector/xpath_extensions.rb +9 -0
  9. data/lib/capybara/selenium/driver.rb +13 -2
  10. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +15 -0
  11. data/lib/capybara/selenium/extensions/find.rb +2 -1
  12. data/lib/capybara/selenium/node.rb +1 -1
  13. data/lib/capybara/selenium/nodes/firefox_node.rb +1 -1
  14. data/lib/capybara/selenium/nodes/safari_node.rb +145 -0
  15. data/lib/capybara/spec/session/attach_file_spec.rb +46 -27
  16. data/lib/capybara/spec/session/click_button_spec.rb +65 -60
  17. data/lib/capybara/spec/session/element/matches_selector_spec.rb +40 -39
  18. data/lib/capybara/spec/session/fill_in_spec.rb +3 -3
  19. data/lib/capybara/spec/session/find_spec.rb +5 -0
  20. data/lib/capybara/spec/session/has_table_spec.rb +120 -0
  21. data/lib/capybara/spec/session/node_spec.rb +3 -3
  22. data/lib/capybara/spec/session/reset_session_spec.rb +8 -7
  23. data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
  24. data/lib/capybara/spec/session/window/window_spec.rb +44 -48
  25. data/lib/capybara/spec/views/form.erb +5 -0
  26. data/lib/capybara/spec/views/tables.erb +67 -0
  27. data/lib/capybara/spec/views/with_html.erb +2 -2
  28. data/lib/capybara/spec/views/with_js.erb +1 -0
  29. data/lib/capybara/version.rb +1 -1
  30. data/spec/capybara_spec.rb +4 -4
  31. data/spec/css_builder_spec.rb +2 -0
  32. data/spec/dsl_spec.rb +13 -17
  33. data/spec/rack_test_spec.rb +77 -85
  34. data/spec/rspec/features_spec.rb +2 -0
  35. data/spec/rspec/shared_spec_matchers.rb +34 -35
  36. data/spec/rspec_spec.rb +11 -13
  37. data/spec/selector_spec.rb +31 -0
  38. data/spec/selenium_spec_chrome.rb +25 -25
  39. data/spec/selenium_spec_firefox.rb +62 -35
  40. data/spec/selenium_spec_firefox_remote.rb +2 -0
  41. data/spec/selenium_spec_safari.rb +148 -0
  42. data/spec/server_spec.rb +40 -44
  43. data/spec/shared_selenium_session.rb +27 -21
  44. data/spec/spec_helper.rb +4 -0
  45. data/spec/xpath_builder_spec.rb +2 -0
  46. metadata +7 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f79ad67119c7cc637fcab5cbc4fe95b0d80a76c2c38182f2a6b8beb3564f8448
4
- data.tar.gz: 9d5c1ca24bbc3e7f2a9c18c71c5b61a78b96d24147b21c9b0f753217d4fc2772
3
+ metadata.gz: 023614c44b5551f29984d882a7fb13b5ff240f589eff090cfb24c4f3a2787a47
4
+ data.tar.gz: c5c1cd40b59700f743325cca40efbe9fd68df776c7f16f6d351336400e86cdd4
5
5
  SHA512:
6
- metadata.gz: a799dc6db1032d812bf749ca8ee8ca8b46e59e957130f80ddfb9d5c2b08fce1a543ce3a0e9c28ee2bf38470867bc6d8a64ce6ed3f3d6c32ae3f31f41c071cb8f
7
- data.tar.gz: d41beb222470a77bd2f2784a8def99ef3fc4b2928ee1b50cb66167d1764a94ec7ea1be59424c4d09f2a7981b3c6ba47ee6df02b13d6472480f10d9698c742e82
6
+ metadata.gz: a76732bb72e5e5daefaf55a2bc4c895ef56d109905b9b3b2a5c96e229d97d783f1044dd6971ed21d4e63e55200a5e00a4641f50698f79346767fe498a5b894ad
7
+ data.tar.gz: ac8d9499807a093c26ca861ed204087702b1a3713b757c6e9d84d43f399b38db36b3b1ab28471413dfd385b442b7636abe9f16194ae4ef17395a16d5e92b7a1c
data/History.md CHANGED
@@ -1,3 +1,16 @@
1
+ # Version 3.15
2
+ Release date: 2019-03-19
3
+
4
+ ### Added
5
+
6
+ * `attach_file` now supports a block mode on JS capable drivers to more accurately test user behavior when file inputs are hidden (beta)
7
+ * :table selector now supports `with_rows`, 'rows', `with_cols`, and 'cols' filters
8
+
9
+ ### Fixed
10
+
11
+ * Fix link selector when `Capybara.test_id` is set - Issue #2166 [bingjyang]
12
+
13
+
1
14
  # Version 3.14
2
15
  Release date: 2019-02-25
3
16
 
data/README.md CHANGED
@@ -6,7 +6,8 @@
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.14.x version of Capybara.
9
+ **Note** You are viewing the README for the 3.15.x version of Capybara.
10
+
10
11
 
11
12
  Capybara helps you test web applications by simulating how a real user would
12
13
  interact with your app. It is agnostic about the driver running your tests and
@@ -231,11 +231,14 @@ module Capybara
231
231
 
232
232
  ##
233
233
  #
234
- # Find a descendant file field on the page and attach a file given its path. The file field can
235
- # be found via its name, id or label text. In the case of the file field being hidden for
234
+ # Find a descendant file field on the page and attach a file given its path. There are two ways to use
235
+ # `attach_file`, in the first method the file field can be found via its name, id or label text.
236
+ # In the case of the file field being hidden for
236
237
  # styling reasons the `make_visible` option can be used to temporarily change the CSS of
237
238
  # the file field, attach the file, and then revert the CSS back to original. If no locator is
238
239
  # passed this will match self or a descendant.
240
+ # The second method, which is currently in beta and may be changed/removed, involves passing a block
241
+ # which performs whatever actions would trigger the file chooser to appear.
239
242
  #
240
243
  # # will attach file to a descendant file input element that has a name, id, or label_text matching 'My File'
241
244
  # page.attach_file('My File', '/path/to/file.png')
@@ -243,6 +246,11 @@ module Capybara
243
246
  # # will attach file to el if it's a file input element
244
247
  # el.attach_file('/path/to/file.png')
245
248
  #
249
+ # # will attach file to whatever file input is triggered by the block
250
+ # page.attach_file('/path/to/file.png') do
251
+ # page.find('#upload_button').click
252
+ # end
253
+ #
246
254
  # @overload attach_file([locator], paths, **options)
247
255
  # @macro waiting_behavior
248
256
  #
@@ -256,19 +264,33 @@ module Capybara
256
264
  # @option options [String] name Match fields that match the name attribute
257
265
  # @option options [String, Array<String>, Regexp] class Match fields that match the class(es) provided
258
266
  # @option options [true, Hash] make_visible A Hash of CSS styles to change before attempting to attach the file, if `true` { opacity: 1, display: 'block', visibility: 'visible' } is used (may not be supported by all drivers)
259
- #
260
- # @return [Capybara::Node::Element] The file field element
267
+ # @overload attach_file(paths, &blk)
268
+ # @param [String, Array<String>] paths The path(s) of the file(s) that will be attached
269
+ # @yield Block whose actions will trigger the system file chooser to be shown
270
+ # @return [Capybara::Node::Element] The file field element
261
271
  def attach_file(locator = nil, paths, make_visible: nil, **options) # rubocop:disable Style/OptionalArguments
272
+ raise ArgumentError, '``#attach_file` does not support passing both a locator and a block' if locator && block_given?
273
+
262
274
  Array(paths).each do |path|
263
275
  raise Capybara::FileNotFound, "cannot attach file, #{path} does not exist" unless File.exist?(path.to_s)
264
276
  end
265
277
  options[:allow_self] = true if locator.nil?
278
+
279
+ if block_given?
280
+ begin
281
+ execute_script CAPTURE_FILE_ELEMENT_SCRIPT
282
+ yield
283
+ file_field = evaluate_script 'window._capybara_clicked_file_input'
284
+ rescue ::Capybara::NotSupportedByDriverError
285
+ warn 'Block mode of `#attach_file` is not supported by the current driver - ignoring.'
286
+ end
287
+ end
266
288
  # Allow user to update the CSS style of the file input since they are so often hidden on a page
267
289
  if make_visible
268
- ff = find(:file_field, locator, options.merge(visible: :all))
290
+ ff = file_field || find(:file_field, locator, options.merge(visible: :all))
269
291
  while_visible(ff, make_visible) { |el| el.set(paths) }
270
292
  else
271
- find(:file_field, locator, options).set(paths)
293
+ (file_field || find(:file_field, locator, options)).set(paths)
272
294
  end
273
295
  end
274
296
 
@@ -369,6 +391,15 @@ module Capybara
369
391
  filter(function(el){ return !el.disabled }).
370
392
  map(function(el){ return { "value": el.value, "label": el.label} })
371
393
  JS
394
+
395
+ CAPTURE_FILE_ELEMENT_SCRIPT = <<~'JS'
396
+ document.addEventListener('click', function(e){
397
+ if (e.target.matches("input[type='file']")) {
398
+ window._capybara_clicked_file_input = e.target;
399
+ e.preventDefault();
400
+ }
401
+ })
402
+ JS
372
403
  end
373
404
  end
374
405
  end
@@ -514,8 +514,16 @@ module Capybara
514
514
  #
515
515
  # page.has_table?('People')
516
516
  #
517
- # @param [String] locator The id or caption of a table
518
- # @return [Boolean] Whether it exist
517
+ # @param [String] locator The id or caption of a table
518
+ # @option options [Array<Array<String>>] :rows
519
+ # Text which should be contained in the tables `<td>` elements organized by row (`<td>` visibility is not considered)
520
+ # @option options [Array<Array<String>>, Array<Hash<String,String>>] :with_rows
521
+ # Partial set of text which should be contained in the tables `<td>` elements organized by row (`<td>` visibility is not considered)
522
+ # @option options [Array<Array<String>>] :cols
523
+ # Text which should be contained in the tables `<td>` elements organized by column (`<td>` visibility is not considered)
524
+ # @option options [Array<Array<String>>, Array<Hash<String,String>>] :with_cols
525
+ # Partial set of text which should be contained in the tables `<td>` elements organized by column (`<td>` visibility is not considered)
526
+ # @return [Boolean] Whether it exists
519
527
  #
520
528
  def has_table?(locator = nil, **options, &optional_filter_block)
521
529
  has_selector?(:table, locator, options, &optional_filter_block)
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'capybara/selector/xpath_extensions'
3
4
  require 'capybara/selector/selector'
5
+
4
6
  Capybara::Selector::FilterSet.add(:_field) do
5
7
  node_filter(:checked, :boolean) { |node, value| !(value ^ node.checked?) }
6
8
  node_filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) }
@@ -101,7 +103,7 @@ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
101
103
  XPath.attr(:title).is(locator),
102
104
  XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
103
105
  matchers << XPath.attr(:'aria-label').is(locator) if enable_aria_label
104
- matchers << XPath.attr(test_id) == locator if test_id
106
+ matchers << XPath.attr(test_id).equals(locator) if test_id
105
107
  xpath = xpath[matchers.reduce(:|)]
106
108
  end
107
109
 
@@ -454,11 +456,125 @@ Capybara.add_selector(:table, locator_type: [String, Symbol]) do
454
456
  xpath
455
457
  end
456
458
 
459
+ expression_filter(:with_cols, valid_values: [Array]) do |xpath, cols|
460
+ col_conditions = cols.map do |col|
461
+ if col.is_a? Hash
462
+ col.reduce(nil) do |xp, (header, cell)|
463
+ header_xp = XPath.descendant(:th)[XPath.string.n.is(header)]
464
+ cell_xp = XPath.descendant(:tr)[header_xp].descendant(:td)
465
+ next cell_xp[XPath.string.n.is(cell)] unless xp
466
+
467
+ table_ancestor = XPath.ancestor(:table)[1]
468
+ xp = XPath::Expression.new(:join, table_ancestor, xp)
469
+ cell_xp[XPath.string.n.is(cell) & XPath.position.equals(xp.preceding_sibling(:td).count.plus(1))]
470
+ end
471
+ else
472
+ cells_xp = col.reduce(nil) do |xp, cell|
473
+ cell_conditions = [XPath.string.n.is(cell)]
474
+ if xp
475
+ prev_row_xp = XPath::Expression.new(:join, XPath.ancestor(:tr)[1].preceding_sibling(:tr), xp)
476
+ cell_conditions << XPath.position.equals(prev_row_xp.preceding_sibling(:td).count.plus(1))
477
+ end
478
+ XPath.descendant(:td)[cell_conditions.reduce :&]
479
+ end
480
+ XPath::Expression.new(:join, XPath.descendant(:tr), cells_xp)
481
+ end
482
+ end.reduce(:&)
483
+ xpath[col_conditions]
484
+ end
485
+
486
+ expression_filter(:cols, valid_values: [Array]) do |xpath, cols|
487
+ raise ArgumentError, ":cols must be an Array of Arrays" unless cols.all? { |col| col.is_a? Array }
488
+
489
+ rows = cols.transpose
490
+ xpath = xpath[XPath.descendant(:tbody).descendant(:tr).count.equals(rows.size) | (XPath.descendant(:tr).count.equals(rows.size) & ~XPath.descendant(:tbody))]
491
+
492
+ col_conditions = rows.map do |row|
493
+ row_conditions = row.map do |cell|
494
+ XPath.self(:td)[XPath.string.n.is(cell)]
495
+ end
496
+ row_conditions = row_conditions.reverse.reduce do |cond, cell|
497
+ cell[XPath.following_sibling[cond]]
498
+ end
499
+ row_xpath = XPath.descendant(:tr)[XPath.descendant(:td)[row_conditions]]
500
+ row_xpath[XPath.descendant(:td).count.equals(row.size)]
501
+ end.reduce(:&)
502
+
503
+ xpath[col_conditions]
504
+ end
505
+
506
+ expression_filter(:with_rows, valid_values: [Array]) do |xpath, rows|
507
+ rows_conditions = rows.map do |row|
508
+ if row.is_a? Hash
509
+ row_conditions = row.map do |header, cell|
510
+ header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
511
+ XPath.descendant(:td)[
512
+ XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
513
+ ]
514
+ end.reduce(:&)
515
+ XPath.descendant(:tr)[row_conditions]
516
+ else
517
+ row_conditions = row.map do |cell|
518
+ XPath.self(:td)[XPath.string.n.is(cell)]
519
+ end
520
+ row_conditions = row_conditions.reverse.reduce do |cond, cell|
521
+ cell[XPath.following_sibling[cond]]
522
+ end
523
+ XPath.descendant(:tr)[XPath.descendant(:td)[row_conditions]]
524
+ end
525
+ end.reduce(:&)
526
+ xpath[rows_conditions]
527
+ end
528
+
529
+ expression_filter(:rows, valid_values: [Array]) do |xpath, rows|
530
+ xpath = xpath[XPath.descendant(:tbody).descendant(:tr).count.equals(rows.size) | (XPath.descendant(:tr).count.equals(rows.size) & ~XPath.descendant(:tbody))]
531
+ rows_conditions = rows.map do |row|
532
+ row_xpath = if row.is_a? Hash
533
+ row_conditions = row.map do |header, cell|
534
+ header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
535
+ XPath.descendant(:td)[
536
+ XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
537
+ ]
538
+ end.reduce(:&)
539
+ XPath.descendant(:tr)[row_conditions]
540
+ else
541
+ row_conditions = row.map do |cell|
542
+ XPath.self(:td)[XPath.string.n.is(cell)]
543
+ end
544
+ row_conditions = row_conditions.reverse.reduce do |cond, cell|
545
+ cell[XPath.following_sibling[cond]]
546
+ end
547
+ XPath.descendant(:tr)[XPath.descendant(:td)[row_conditions]]
548
+ end
549
+ row_xpath[XPath.descendant(:td).count.equals(row.size)]
550
+ end.reduce(:&)
551
+ xpath[rows_conditions]
552
+ end
553
+
457
554
  describe_expression_filters do |caption: nil, **|
458
555
  " with caption \"#{caption}\"" if caption
459
556
  end
460
557
  end
461
558
 
559
+ Capybara.add_selector(:table_row, locator_type: [Array, Hash]) do
560
+ xpath do |locator|
561
+ xpath = XPath.descendant(:tr)
562
+ if locator.is_a? Hash
563
+ locator.reduce(xpath) do |xp, (header, cell)|
564
+ header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
565
+ cell_xp = XPath.descendant(:td)[
566
+ XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
567
+ ]
568
+ xp[cell_xp]
569
+ end
570
+ else
571
+ initial_td = XPath.descendant(:td)[XPath.string.n.is(locator.shift)]
572
+ tds = locator.reverse.map { |cell| XPath.following_sibling(:td)[XPath.string.n.is(cell)] }.reduce { |xp, cell| xp[cell] }
573
+ xpath[initial_td[tds]]
574
+ end
575
+ end
576
+ end
577
+
462
578
  Capybara.add_selector(:frame, locator_type: [String, Symbol]) do
463
579
  xpath do |locator, name: nil, **|
464
580
  xpath = XPath.descendant(:iframe).union(XPath.descendant(:frame))
@@ -158,6 +158,13 @@ module Capybara
158
158
  # * :caption (String) — Match text of associated caption
159
159
  # * :class ((String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
160
160
  # * :style (String, Regexp, Hash)
161
+ # * :with_rows (Array<Array<String>>, Array<Hash<String, String>>) - Partial match <td> data - visibility of <td> elements is not considered
162
+ # * :rows (Array<Array<String>>) — Match all <td>s - visibility of <td> elements is not considered
163
+ # * :with_cols (Array<Array<String>>, Array<Hash<String, String>>) - Partial match <td> data - visibility of <td> elements is not considered
164
+ # * :cols (Array<Array<String>>) — Match all <td>s - visibility of <td> elements is not considered
165
+ #
166
+ # * **:table_row** - Find table row
167
+ # * Locator: Array<String>, Hash<String,String> table row <td> contents - visibility of <td> elements is not considered
161
168
  #
162
169
  # * **:frame** - Find frame/iframe elements
163
170
  # * Locator: Match id or name
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XPath
4
+ class Renderer
5
+ def join(*expressions)
6
+ expressions.join('/')
7
+ end
8
+ end
9
+ end
@@ -267,7 +267,11 @@ private
267
267
  if @browser.respond_to? :session_storage
268
268
  @browser.session_storage.clear
269
269
  else
270
- warn 'sessionStorage clear requested but is not supported by this driver' unless options[:clear_session_storage].nil?
270
+ begin
271
+ @browser&.execute_script('window.sessionStorage.clear()')
272
+ rescue # rubocop:disable Style/RescueStandardError
273
+ warn 'sessionStorage clear requested but is not supported by this driver' unless options[:clear_session_storage].nil?
274
+ end
271
275
  end
272
276
  end
273
277
 
@@ -275,7 +279,11 @@ private
275
279
  if @browser.respond_to? :local_storage
276
280
  @browser.local_storage.clear
277
281
  else
278
- warn 'localStorage clear requested but is not supported by this driver' unless options[:clear_local_storage].nil?
282
+ begin
283
+ @browser&.execute_script('window.localStorage.clear()')
284
+ rescue # rubocop:disable Style/RescueStandardError
285
+ warn 'localStorage clear requested but is not supported by this driver' unless options[:clear_local_storage].nil?
286
+ end
279
287
  end
280
288
  end
281
289
 
@@ -359,6 +367,8 @@ private
359
367
  extend FirefoxDriver if sel_driver.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
360
368
  when :ie, :internet_explorer
361
369
  extend InternetExplorerDriver
370
+ when :safari, :Safari_Technology_Preview
371
+ extend SafariDriver
362
372
  end
363
373
  end
364
374
 
@@ -408,3 +418,4 @@ end
408
418
  require 'capybara/selenium/driver_specializations/chrome_driver'
409
419
  require 'capybara/selenium/driver_specializations/firefox_driver'
410
420
  require 'capybara/selenium/driver_specializations/internet_explorer_driver'
421
+ require 'capybara/selenium/driver_specializations/safari_driver'
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selenium/nodes/safari_node'
4
+
5
+ module Capybara::Selenium::Driver::SafariDriver
6
+ private
7
+
8
+ def build_node(native_node, initial_cache = {})
9
+ ::Capybara::Selenium::SafariNode.new(self, native_node, initial_cache)
10
+ end
11
+
12
+ def bridge
13
+ browser.send(:bridge)
14
+ end
15
+ end
@@ -45,7 +45,8 @@ module Capybara
45
45
  unless functions.empty?
46
46
  hints_js << <<~EACH_JS
47
47
  return arguments[0].map(function(el){
48
- return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) }); });
48
+ return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) });
49
+ });
49
50
  EACH_JS
50
51
 
51
52
  hints = es_context.execute_script hints_js, els
@@ -398,7 +398,7 @@ private
398
398
  end
399
399
 
400
400
  def to_date_str
401
- value.to_date.strftime('%Y-%m-%d')
401
+ value.to_date.iso8601
402
402
  end
403
403
 
404
404
  def timeable?
@@ -119,7 +119,7 @@ private
119
119
  x.parent(:fieldset)[
120
120
  x.attr(:disabled)
121
121
  ] + x.ancestor[
122
- ~x.self(:legned) |
122
+ ~x.self(:legend) |
123
123
  x.preceding_sibling(:legend)
124
124
  ][
125
125
  x.parent(:fieldset)[
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require 'capybara/selenium/extensions/html5_drag'
4
+
5
+ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
6
+ # include Html5Drag
7
+
8
+ def click(keys = [], **options)
9
+ # driver.execute_script('arguments[0].scrollIntoViewIfNeeded({block: "center"})', self)
10
+ super
11
+ rescue ::Selenium::WebDriver::Error::ElementNotInteractableError
12
+ if tag_name == 'tr'
13
+ warn 'You are attempting to click a table row which has issues in safaridriver - '\
14
+ 'Your test should probably be clicking on a table cell like a user would. '\
15
+ 'Clicking the first cell in the row instead.'
16
+ return find_css('th:first-child,td:first-child')[0].click(keys, options)
17
+ end
18
+ raise
19
+ end
20
+
21
+ def select_option
22
+ driver.execute_script("arguments[0].closest('select').scrollIntoView()", self)
23
+ super
24
+ end
25
+
26
+ def unselect_option
27
+ driver.execute_script("arguments[0].closest('select').scrollIntoView()", self)
28
+ super
29
+ end
30
+
31
+ def visible_text
32
+ return '' unless visible?
33
+
34
+ vis_text = driver.execute_script('return arguments[0].innerText', self)
35
+ vis_text.gsub(/\ +/, ' ')
36
+ .gsub(/[\ \n]*\n[\ \n]*/, "\n")
37
+ .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
38
+ .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
39
+ .tr("\u00a0", ' ')
40
+ end
41
+
42
+ def disabled?
43
+ return true if super || (self[:disabled] == 'true')
44
+
45
+ # workaround for safaridriver reporting elements as enabled when they are nested in disabling elements
46
+ if %w[option optgroup].include? tag_name
47
+ find_xpath('parent::*[self::optgroup or self::select]')[0].disabled?
48
+ else
49
+ !find_xpath(DISABLED_BY_FIELDSET_XPATH).empty?
50
+ end
51
+ end
52
+
53
+ def set_file(value) # rubocop:disable Naming/AccessorMethodName
54
+ # By default files are appended so we have to clear here if its multiple and already set
55
+ native.clear if multiple? && driver.evaluate_script('arguments[0].files', self).any?
56
+ super
57
+ end
58
+
59
+ def send_keys(*args)
60
+ return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none? { |arg| arg.is_a? Array }
61
+
62
+ native.click
63
+ _send_keys(args).perform
64
+ end
65
+
66
+ def set_text(value, clear: nil, **_unused)
67
+ value = value.to_s
68
+ if clear == :backspace
69
+ # Clear field by sending the correct number of backspace keys.
70
+ backspaces = [:backspace] * self.value.to_s.length
71
+ send_keys(*([[:control, 'e']] + backspaces + [value]))
72
+ else
73
+ super.tap do
74
+ # React doesn't see the safaridriver element clear
75
+ send_keys(:space, :backspace) if value.to_s.empty? && clear.nil?
76
+ end
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def bridge
83
+ driver.browser.send(:bridge)
84
+ end
85
+
86
+ DISABLED_BY_FIELDSET_XPATH = XPath.generate do |x|
87
+ x.parent(:fieldset)[
88
+ x.attr(:disabled)
89
+ ] + x.ancestor[
90
+ ~x.self(:legend) |
91
+ x.preceding_sibling(:legend)
92
+ ][
93
+ x.parent(:fieldset)[
94
+ x.attr(:disabled)
95
+ ]
96
+ ]
97
+ end.to_s.freeze
98
+
99
+ def _send_keys(keys, actions = browser_action, down_keys = ModifierKeysStack.new)
100
+ case keys
101
+ when :control, :left_control, :right_control,
102
+ :alt, :left_alt, :right_alt,
103
+ :shift, :left_shift, :right_shift,
104
+ :meta, :left_meta, :right_meta,
105
+ :command
106
+ down_keys.press(keys)
107
+ actions.key_down(keys)
108
+ when String
109
+ keys = keys.upcase if down_keys&.include?(:shift)
110
+ actions.send_keys(keys)
111
+ when Symbol
112
+ actions.send_keys(keys)
113
+ when Array
114
+ down_keys.push
115
+ keys.each { |sub_keys| _send_keys(sub_keys, actions, down_keys) }
116
+ down_keys.pop.reverse_each { |key| actions.key_up(key) }
117
+ else
118
+ raise ArgumentError, 'Unknown keys type'
119
+ end
120
+ actions
121
+ end
122
+
123
+ class ModifierKeysStack
124
+ def initialize
125
+ @stack = []
126
+ end
127
+
128
+ def include?(key)
129
+ @stack.flatten.include?(key)
130
+ end
131
+
132
+ def press(key)
133
+ @stack.last.push(key)
134
+ end
135
+
136
+ def push
137
+ @stack.push []
138
+ end
139
+
140
+ def pop
141
+ @stack.pop
142
+ end
143
+ end
144
+ private_constant :ModifierKeysStack
145
+ end