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.
- checksums.yaml +4 -4
- data/History.md +13 -0
- data/README.md +2 -1
- data/lib/capybara/node/actions.rb +37 -6
- data/lib/capybara/node/matchers.rb +10 -2
- data/lib/capybara/selector.rb +117 -1
- data/lib/capybara/selector/selector.rb +7 -0
- data/lib/capybara/selector/xpath_extensions.rb +9 -0
- data/lib/capybara/selenium/driver.rb +13 -2
- data/lib/capybara/selenium/driver_specializations/safari_driver.rb +15 -0
- data/lib/capybara/selenium/extensions/find.rb +2 -1
- data/lib/capybara/selenium/node.rb +1 -1
- data/lib/capybara/selenium/nodes/firefox_node.rb +1 -1
- data/lib/capybara/selenium/nodes/safari_node.rb +145 -0
- data/lib/capybara/spec/session/attach_file_spec.rb +46 -27
- data/lib/capybara/spec/session/click_button_spec.rb +65 -60
- data/lib/capybara/spec/session/element/matches_selector_spec.rb +40 -39
- data/lib/capybara/spec/session/fill_in_spec.rb +3 -3
- data/lib/capybara/spec/session/find_spec.rb +5 -0
- data/lib/capybara/spec/session/has_table_spec.rb +120 -0
- data/lib/capybara/spec/session/node_spec.rb +3 -3
- data/lib/capybara/spec/session/reset_session_spec.rb +8 -7
- data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
- data/lib/capybara/spec/session/window/window_spec.rb +44 -48
- data/lib/capybara/spec/views/form.erb +5 -0
- data/lib/capybara/spec/views/tables.erb +67 -0
- data/lib/capybara/spec/views/with_html.erb +2 -2
- data/lib/capybara/spec/views/with_js.erb +1 -0
- data/lib/capybara/version.rb +1 -1
- data/spec/capybara_spec.rb +4 -4
- data/spec/css_builder_spec.rb +2 -0
- data/spec/dsl_spec.rb +13 -17
- data/spec/rack_test_spec.rb +77 -85
- data/spec/rspec/features_spec.rb +2 -0
- data/spec/rspec/shared_spec_matchers.rb +34 -35
- data/spec/rspec_spec.rb +11 -13
- data/spec/selector_spec.rb +31 -0
- data/spec/selenium_spec_chrome.rb +25 -25
- data/spec/selenium_spec_firefox.rb +62 -35
- data/spec/selenium_spec_firefox_remote.rb +2 -0
- data/spec/selenium_spec_safari.rb +148 -0
- data/spec/server_spec.rb +40 -44
- data/spec/shared_selenium_session.rb +27 -21
- data/spec/spec_helper.rb +4 -0
- data/spec/xpath_builder_spec.rb +2 -0
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 023614c44b5551f29984d882a7fb13b5ff240f589eff090cfb24c4f3a2787a47
|
4
|
+
data.tar.gz: c5c1cd40b59700f743325cca40efbe9fd68df776c7f16f6d351336400e86cdd4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
[](https://gitter.im/jnicklas/capybara?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
7
7
|
[](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.
|
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.
|
235
|
-
# be found via its name, id or label text.
|
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
|
-
# @
|
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
|
518
|
-
# @
|
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)
|
data/lib/capybara/selector.rb
CHANGED
@@ -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)
|
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
|
@@ -267,7 +267,11 @@ private
|
|
267
267
|
if @browser.respond_to? :session_storage
|
268
268
|
@browser.session_storage.clear
|
269
269
|
else
|
270
|
-
|
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
|
-
|
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
|
@@ -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
|