capybara 3.14.0 → 3.15.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![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.
|
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
|