capybara-playwright-driver 0.5.10 → 0.5.12
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/AGENTS.md +5 -0
- data/README.md +37 -4
- data/lib/capybara/playwright/browser.rb +17 -7
- data/lib/capybara/playwright/node.rb +159 -53
- data/lib/capybara/playwright/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a90dd1edbae8b79e2f4d7eac72dcc8014e8d9e491730cbffa20be906c74ee5c
|
|
4
|
+
data.tar.gz: 729834c2bd8e56d523230e043f3d148777a89e2870da1624294d109eadd9434a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b674a26ae213904eacec537a259f9111c5c4686d2317691bfe6bd965ad9fe4d57bad3dfb701cb2f456676f7a9d50ed94b50fb94723f83055b1bd79cb2c3d95c
|
|
7
|
+
data.tar.gz: 350f3c693c186594ccac2aa32e05cfd762f7baa069e01ebd6df8d20c744a3e72dfdfdd03a29728fcce77b5b173bd3bc6a2efcaedb55ca66fde43b8d55340bd9b
|
data/AGENTS.md
CHANGED
|
@@ -106,6 +106,11 @@ end
|
|
|
106
106
|
- Prefer concise Ruby that reads top-to-bottom without carrying unnecessary temporary state.
|
|
107
107
|
- Prefer small private helper objects only when they reduce complexity. Once extracted, give them Ruby-like method names and keep their public surface minimal.
|
|
108
108
|
|
|
109
|
+
## Test Style
|
|
110
|
+
- Avoid assertion roulette. Each example must verify one clearly named behavior so a failure identifies the broken behavior directly.
|
|
111
|
+
- Do not use loops, parameter tables, or metaprogramming to generate examples for distinct public operations. Write an explicit `it` block for each operation, even when their setup and expectations are similar.
|
|
112
|
+
- Split independent expectations into separate examples. Keep multiple expectations together only when they jointly describe one inseparable outcome.
|
|
113
|
+
|
|
109
114
|
## How to execute RSpec or Ruby
|
|
110
115
|
|
|
111
116
|
`ruby` uses macOS system ruby interpreter (typically 2.6 or older). rbenv should be used.
|
data/README.md
CHANGED
|
@@ -26,13 +26,14 @@ Capybara.save_path = 'tmp/capybara'
|
|
|
26
26
|
# run
|
|
27
27
|
Capybara.app_host = 'https://github.com'
|
|
28
28
|
visit '/'
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
find('button[aria-label^="Search or jump to"]').click
|
|
30
|
+
search_field = find('input[aria-label="Search or jump to"]')
|
|
31
|
+
search_field.fill_in(with: 'Capybara')
|
|
31
32
|
|
|
32
33
|
## [REMARK] We can use Playwright-native selector and action, instead of Capybara DSL.
|
|
33
|
-
#
|
|
34
|
+
# search_field.send_keys(:enter)
|
|
34
35
|
page.driver.with_playwright_page do |page|
|
|
35
|
-
page.get_by_label('
|
|
36
|
+
page.get_by_label('Search or jump to', exact: true).press('Enter')
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
all('[data-testid="results-list"] h3').each do |li|
|
|
@@ -43,6 +44,38 @@ end
|
|
|
43
44
|
|
|
44
45
|
Refer the [documentation](https://playwright-ruby-client.vercel.app/docs/article/guides/rails_integration) for more detailed configuration.
|
|
45
46
|
|
|
47
|
+
## Selenium compatibility
|
|
48
|
+
|
|
49
|
+
Visible text for ordinary HTML elements follows the browser's
|
|
50
|
+
[`innerText` behavior](https://html.spec.whatwg.org/multipage/dom.html#the-innertext-idl-attribute). This preserves
|
|
51
|
+
meaningful blank lines, but paragraph and rich-text-editor boundaries can differ from Selenium's WebDriver text
|
|
52
|
+
algorithm. When exact whitespace is not part of an assertion, use Capybara's `normalize_ws: true` option:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
expect(message).to have_text('Hello Regards', normalize_ws: true)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This driver delegates keyboard focus and caret behavior to Playwright rather than overriding it with JavaScript. One
|
|
59
|
+
observable difference is that when a `contenteditable` element loses focus between `send_keys` calls, Playwright may
|
|
60
|
+
place the caret differently from Selenium when the element is focused again. Specify the caret movement explicitly
|
|
61
|
+
when the test depends on it:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
editor.send_keys(:end, '@') # The page moves focus to an autocomplete control.
|
|
65
|
+
editor.send_keys(:end, 'alice')
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For text inputs and textareas, `fill_in` uses Playwright's typing behavior for every character regardless of the value
|
|
69
|
+
length. Playwright sends `keydown`, `keypress`/`input`, and `keyup` for characters on a US keyboard, but sends only
|
|
70
|
+
`input` for other characters, including Japanese text. This follows
|
|
71
|
+
[Playwright's documented keyboard behavior](https://playwright.dev/docs/api/class-keyboard#keyboard-type). Typing very
|
|
72
|
+
large values is slower than Playwright's native `fill`, which does not send per-character keyboard events. When input
|
|
73
|
+
speed matters more than those events, consider using `fill` directly:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
page.driver.with_playwright_page { |playwright_page| playwright_page.get_by_label('Body').fill(large_value) }
|
|
77
|
+
```
|
|
78
|
+
|
|
46
79
|
## Development
|
|
47
80
|
|
|
48
81
|
Prepare to run tests:
|
|
@@ -94,15 +94,25 @@ module Capybara
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def refresh
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
assert_page_alive {
|
|
98
|
+
if firefox?
|
|
99
|
+
# Preserve POST requests: https://github.com/microsoft/playwright/issues/39738
|
|
100
|
+
# Mark the old document so pushState and an earlier navigation's load
|
|
101
|
+
# cannot satisfy the wait for the reloaded document.
|
|
102
|
+
@playwright_page.evaluate(<<~JAVASCRIPT)
|
|
103
|
+
() => {
|
|
104
|
+
document[Symbol.for('capybara-playwright-driver.refresh')] = true;
|
|
105
|
+
location.reload(true);
|
|
106
|
+
}
|
|
107
|
+
JAVASCRIPT
|
|
108
|
+
@playwright_page.wait_for_function(<<~JAVASCRIPT, polling: 10, timeout: @default_navigation_timeout).dispose
|
|
109
|
+
() => !document[Symbol.for('capybara-playwright-driver.refresh')] && document.readyState === 'complete'
|
|
110
|
+
JAVASCRIPT
|
|
111
|
+
else
|
|
102
112
|
response = @playwright_page.reload
|
|
103
113
|
@playwright_page.capybara_set_last_response(response)
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
end
|
|
115
|
+
}
|
|
106
116
|
end
|
|
107
117
|
|
|
108
118
|
def find_xpath(query, **options)
|
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
module Capybara
|
|
2
2
|
module ElementClickOptionPatch
|
|
3
3
|
def perform_click_action(keys, **options)
|
|
4
|
-
|
|
5
|
-
if options[:wait].is_a?(Numeric)
|
|
6
|
-
options[:_playwright_wait] = options[:wait]
|
|
7
|
-
end
|
|
4
|
+
return super unless driver.is_a?(Capybara::Playwright::Driver)
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
wait = options[:wait]
|
|
7
|
+
|
|
8
|
+
# Expose `wait` value to the block given to perform_click_action.
|
|
9
|
+
options[:_playwright_wait] = wait if wait.is_a?(Numeric)
|
|
10
|
+
options[:wait] = 0 if wait == false
|
|
14
11
|
|
|
12
|
+
# Playwright waits for actionability while Capybara retries replaced elements.
|
|
15
13
|
super
|
|
16
14
|
end
|
|
17
15
|
end
|
|
18
16
|
Node::Element.prepend(ElementClickOptionPatch)
|
|
19
17
|
|
|
18
|
+
module ElementDragToPatch
|
|
19
|
+
def drag_to(target, **options)
|
|
20
|
+
return super unless driver.is_a?(Capybara::Playwright::Driver)
|
|
21
|
+
|
|
22
|
+
synchronize do
|
|
23
|
+
begin
|
|
24
|
+
base.drag_to(target.base, **options)
|
|
25
|
+
rescue Capybara::Playwright::Node::StaleReferenceError
|
|
26
|
+
# Capybara's synchronize reloads only the source element.
|
|
27
|
+
target.reload if session_options.automatic_reload
|
|
28
|
+
raise
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
Node::Element.prepend(ElementDragToPatch)
|
|
35
|
+
|
|
20
36
|
module WithElementHandlePatch
|
|
21
37
|
def with_playwright_element_handle(&block)
|
|
22
38
|
raise ArgumentError.new('block must be given') unless block
|
|
@@ -263,6 +279,21 @@ module Capybara
|
|
|
263
279
|
|
|
264
280
|
class NotActionableError < StandardError ; end
|
|
265
281
|
class StaleReferenceError < StandardError ; end
|
|
282
|
+
class MissingBoundingBoxError < StandardError ; end
|
|
283
|
+
class DragInterruptedError < StandardError ; end
|
|
284
|
+
|
|
285
|
+
class ElementGeometry
|
|
286
|
+
def self.bounding_box(element)
|
|
287
|
+
box = element.bounding_box
|
|
288
|
+
return box if box
|
|
289
|
+
|
|
290
|
+
unless element.evaluate('element => element.isConnected')
|
|
291
|
+
raise StaleReferenceError, 'Element is not attached to the DOM'
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
raise MissingBoundingBoxError, 'Element is attached but has no bounding box'
|
|
295
|
+
end
|
|
296
|
+
end
|
|
266
297
|
|
|
267
298
|
def all_text
|
|
268
299
|
assert_element_not_stale {
|
|
@@ -284,7 +315,7 @@ module Capybara
|
|
|
284
315
|
if (el.nodeName == 'TEXTAREA'){
|
|
285
316
|
return el.textContent;
|
|
286
317
|
} else if (el instanceof SVGElement) {
|
|
287
|
-
return el.textContent;
|
|
318
|
+
return el.textContent.replace(/\\n+/g, '\\n');
|
|
288
319
|
} else {
|
|
289
320
|
return el.innerText;
|
|
290
321
|
}
|
|
@@ -292,7 +323,6 @@ module Capybara
|
|
|
292
323
|
JAVASCRIPT
|
|
293
324
|
text.to_s.scrub.gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
|
|
294
325
|
.gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
|
|
295
|
-
.gsub(/\n+/, "\n")
|
|
296
326
|
.tr("\u00a0", ' ')
|
|
297
327
|
}
|
|
298
328
|
end
|
|
@@ -368,7 +398,9 @@ module Capybara
|
|
|
368
398
|
end
|
|
369
399
|
end
|
|
370
400
|
|
|
371
|
-
|
|
401
|
+
assert_element_not_stale do
|
|
402
|
+
settable_class.new(@element, capybara_default_wait_time, @internal_logger).set(value, **options)
|
|
403
|
+
end
|
|
372
404
|
rescue ::Playwright::TimeoutError => err
|
|
373
405
|
raise NotActionableError.new(err)
|
|
374
406
|
end
|
|
@@ -406,6 +438,7 @@ module Capybara
|
|
|
406
438
|
existing_text.length.times { @element.press('Backspace', timeout: @timeout) }
|
|
407
439
|
when :none
|
|
408
440
|
@element.press('End', timeout: @timeout)
|
|
441
|
+
ensure_attached
|
|
409
442
|
when Array
|
|
410
443
|
@internal_logger.warn "options { clear: #{options[:clear]} } is ignored"
|
|
411
444
|
end
|
|
@@ -417,51 +450,91 @@ module Capybara
|
|
|
417
450
|
text = text[0...-1]
|
|
418
451
|
end
|
|
419
452
|
|
|
420
|
-
|
|
453
|
+
keyboard = @element.owner_frame.page.keyboard
|
|
454
|
+
set_text(text, append: options[:clear] == :none, keyboard: keyboard)
|
|
421
455
|
|
|
422
|
-
if press_enter
|
|
423
|
-
@element.press('Enter', timeout: @timeout)
|
|
424
|
-
end
|
|
456
|
+
keyboard.press('Enter') if press_enter
|
|
425
457
|
rescue ::Playwright::TimeoutError
|
|
426
458
|
raise if @element.editable?
|
|
427
459
|
|
|
428
460
|
@internal_logger.info("Node#set: element is not editable. #{@element}")
|
|
429
461
|
end
|
|
430
462
|
|
|
431
|
-
private def set_text(text, append:)
|
|
463
|
+
private def set_text(text, append:, keyboard:)
|
|
432
464
|
# ElementHandle#type can refocus inherited contenteditable descendants and drop the input.
|
|
433
465
|
if @element.evaluate('el => el.isContentEditable') && !append
|
|
434
466
|
@element.fill(text, timeout: @timeout)
|
|
435
467
|
return
|
|
436
468
|
end
|
|
437
469
|
|
|
470
|
+
text = text.gsub(/\r\n?/, "\n")
|
|
471
|
+
|
|
438
472
|
if text.include?("\t")
|
|
439
|
-
type_tab_separated_text(text, append: append)
|
|
473
|
+
type_tab_separated_text(text, append: append, keyboard: keyboard)
|
|
440
474
|
return
|
|
441
475
|
end
|
|
442
476
|
|
|
443
|
-
grapheme_clusters = text.scan(/\X/)
|
|
444
|
-
fill_text = grapheme_clusters[0...-1].join
|
|
445
|
-
typed_text = grapheme_clusters[-1].to_s
|
|
446
|
-
|
|
447
477
|
if append
|
|
448
|
-
|
|
478
|
+
type_text(keyboard, text)
|
|
479
|
+
elsif text.empty?
|
|
480
|
+
@element.fill('', timeout: @timeout)
|
|
449
481
|
else
|
|
450
|
-
@element.
|
|
482
|
+
@element.select_text(timeout: @timeout)
|
|
483
|
+
replace_text(keyboard, text)
|
|
451
484
|
end
|
|
452
|
-
@element.type(typed_text, timeout: @timeout) unless typed_text.empty?
|
|
453
485
|
end
|
|
454
486
|
|
|
455
|
-
private def
|
|
456
|
-
head, *tail = text.split("\
|
|
457
|
-
|
|
458
|
-
|
|
487
|
+
private def replace_text(keyboard, text)
|
|
488
|
+
head, *tail = text.split("\n", -1)
|
|
489
|
+
if head.empty?
|
|
490
|
+
@element.press('Enter', timeout: @timeout)
|
|
491
|
+
head = tail.shift
|
|
492
|
+
keyboard.type(head) unless head.empty?
|
|
493
|
+
else
|
|
494
|
+
first_character = head[/\A\X/]
|
|
495
|
+
@element.type(first_character, timeout: @timeout)
|
|
496
|
+
ensure_attached
|
|
497
|
+
remaining_text = head[first_character.length..-1]
|
|
498
|
+
keyboard.type(remaining_text) unless remaining_text.empty?
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
type_lines(keyboard, tail)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
private def type_text(keyboard, text)
|
|
505
|
+
return if text.empty?
|
|
506
|
+
|
|
507
|
+
head, *tail = text.split("\n", -1)
|
|
508
|
+
keyboard.type(head) unless head.empty?
|
|
509
|
+
|
|
510
|
+
type_lines(keyboard, tail)
|
|
511
|
+
end
|
|
459
512
|
|
|
460
|
-
|
|
461
|
-
|
|
513
|
+
private def type_lines(keyboard, parts)
|
|
514
|
+
parts.each do |part|
|
|
515
|
+
keyboard.press('Enter')
|
|
462
516
|
keyboard.type(part) unless part.empty?
|
|
463
517
|
end
|
|
464
518
|
end
|
|
519
|
+
|
|
520
|
+
private def type_tab_separated_text(text, append:, keyboard:)
|
|
521
|
+
head, *tail = text.split("\t", -1)
|
|
522
|
+
set_text(head, append: append, keyboard: keyboard)
|
|
523
|
+
|
|
524
|
+
tail.each_with_index do |part, index|
|
|
525
|
+
if index.zero? && !append && head.empty?
|
|
526
|
+
@element.press('Tab', timeout: @timeout)
|
|
527
|
+
else
|
|
528
|
+
keyboard.press('Tab')
|
|
529
|
+
end
|
|
530
|
+
type_text(keyboard, part.gsub(/\r\n?/, "\n"))
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
private def ensure_attached
|
|
535
|
+
# Trigger Playwright's stale-element check without changing focus.
|
|
536
|
+
@element.enabled?
|
|
537
|
+
end
|
|
465
538
|
end
|
|
466
539
|
|
|
467
540
|
class FileUpload < Settable
|
|
@@ -570,20 +643,26 @@ module Capybara
|
|
|
570
643
|
end
|
|
571
644
|
|
|
572
645
|
def click(keys = [], **options)
|
|
573
|
-
|
|
574
|
-
|
|
646
|
+
assert_element_not_stale do
|
|
647
|
+
click_options = ClickOptions.new(@element, keys, options, capybara_default_wait_time)
|
|
648
|
+
@element.click(**click_options.as_params)
|
|
649
|
+
end
|
|
575
650
|
end
|
|
576
651
|
|
|
577
652
|
def right_click(keys = [], **options)
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
653
|
+
assert_element_not_stale do
|
|
654
|
+
click_options = ClickOptions.new(@element, keys, options, capybara_default_wait_time)
|
|
655
|
+
params = click_options.as_params
|
|
656
|
+
params[:button] = 'right'
|
|
657
|
+
@element.click(**params)
|
|
658
|
+
end
|
|
582
659
|
end
|
|
583
660
|
|
|
584
661
|
def double_click(keys = [], **options)
|
|
585
|
-
|
|
586
|
-
|
|
662
|
+
assert_element_not_stale do
|
|
663
|
+
click_options = ClickOptions.new(@element, keys, options, capybara_default_wait_time)
|
|
664
|
+
@element.dblclick(**click_options.as_params)
|
|
665
|
+
end
|
|
587
666
|
end
|
|
588
667
|
|
|
589
668
|
class ClickOptions
|
|
@@ -653,7 +732,7 @@ module Capybara
|
|
|
653
732
|
|
|
654
733
|
private def position
|
|
655
734
|
if @offset_center
|
|
656
|
-
box = @element
|
|
735
|
+
box = ElementGeometry.bounding_box(@element)
|
|
657
736
|
|
|
658
737
|
{
|
|
659
738
|
x: @coords[:x] + box['width'] / 2,
|
|
@@ -840,11 +919,17 @@ module Capybara
|
|
|
840
919
|
end
|
|
841
920
|
|
|
842
921
|
def hover
|
|
843
|
-
|
|
922
|
+
assert_element_not_stale do
|
|
923
|
+
@element.hover(timeout: capybara_default_wait_time)
|
|
924
|
+
end
|
|
844
925
|
end
|
|
845
926
|
|
|
846
|
-
def drag_to(
|
|
847
|
-
|
|
927
|
+
def drag_to(target, **options)
|
|
928
|
+
assert_element_not_stale do
|
|
929
|
+
target.send(:assert_element_not_stale) do
|
|
930
|
+
DragTo.new(@page, @element, target.element, options, capybara_default_wait_time).execute
|
|
931
|
+
end
|
|
932
|
+
end
|
|
848
933
|
end
|
|
849
934
|
|
|
850
935
|
class DragTo
|
|
@@ -861,22 +946,28 @@ module Capybara
|
|
|
861
946
|
# @param page [Playwright::Page]
|
|
862
947
|
# @param source [Playwright::ElementHandle]
|
|
863
948
|
# @param target [Playwright::ElementHandle]
|
|
864
|
-
def initialize(page, source, target, options)
|
|
949
|
+
def initialize(page, source, target, options, timeout)
|
|
865
950
|
@page = page
|
|
866
951
|
@source = source
|
|
867
952
|
@target = target
|
|
868
953
|
@options = options
|
|
954
|
+
@timeout = timeout
|
|
869
955
|
end
|
|
870
956
|
|
|
871
957
|
def execute
|
|
872
|
-
|
|
958
|
+
input_started = false
|
|
959
|
+
mouse_down = false
|
|
960
|
+
@target.wait_for_element_state('visible', timeout: @timeout)
|
|
961
|
+
@source.scroll_into_view_if_needed(timeout: @timeout)
|
|
873
962
|
|
|
874
|
-
# down
|
|
875
963
|
position_from = center_of(@source)
|
|
964
|
+
center_of(@target)
|
|
876
965
|
@page.mouse.move(*position_from)
|
|
966
|
+
input_started = true
|
|
967
|
+
mouse_down = true
|
|
877
968
|
@page.mouse.down
|
|
878
969
|
|
|
879
|
-
@target.scroll_into_view_if_needed
|
|
970
|
+
@target.scroll_into_view_if_needed(timeout: @timeout)
|
|
880
971
|
|
|
881
972
|
# move and up
|
|
882
973
|
sleep_delay
|
|
@@ -885,20 +976,33 @@ module Capybara
|
|
|
885
976
|
@page.mouse.move(*position_to, steps: 6)
|
|
886
977
|
sleep_delay
|
|
887
978
|
@page.mouse.up
|
|
979
|
+
mouse_down = false
|
|
888
980
|
end
|
|
889
981
|
sleep_delay
|
|
982
|
+
rescue StaleReferenceError, ::Playwright::Error => err
|
|
983
|
+
raise DragInterruptedError, "Drag was interrupted after input began: #{err.message}" if input_started
|
|
984
|
+
|
|
985
|
+
raise
|
|
986
|
+
ensure
|
|
987
|
+
@page.mouse.up if mouse_down
|
|
890
988
|
end
|
|
891
989
|
|
|
892
990
|
# @param element [Playwright::ElementHandle]
|
|
893
991
|
private def center_of(element)
|
|
894
|
-
box =
|
|
992
|
+
box = ElementGeometry.bounding_box(element)
|
|
993
|
+
|
|
895
994
|
[box["x"] + box["width"] / 2, box["y"] + box["height"] / 2]
|
|
896
995
|
end
|
|
897
996
|
|
|
898
|
-
private def with_key_pressing(keys
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
997
|
+
private def with_key_pressing(keys)
|
|
998
|
+
pressed_keys = []
|
|
999
|
+
keys.each do |key|
|
|
1000
|
+
@page.keyboard.down(key)
|
|
1001
|
+
pressed_keys << key
|
|
1002
|
+
end
|
|
1003
|
+
yield
|
|
1004
|
+
ensure
|
|
1005
|
+
pressed_keys.reverse_each { |key| @page.keyboard.up(key) }
|
|
902
1006
|
end
|
|
903
1007
|
|
|
904
1008
|
# @returns Array<String>
|
|
@@ -1078,7 +1182,9 @@ module Capybara
|
|
|
1078
1182
|
end
|
|
1079
1183
|
|
|
1080
1184
|
def obscured?
|
|
1081
|
-
|
|
1185
|
+
assert_element_not_stale do
|
|
1186
|
+
@element.capybara_obscured?
|
|
1187
|
+
end
|
|
1082
1188
|
end
|
|
1083
1189
|
|
|
1084
1190
|
def checked?
|