capybara-playwright-driver 0.5.9 → 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 +13 -0
- data/Gemfile +1 -0
- data/README.md +37 -4
- data/lib/capybara/playwright/browser.rb +44 -4
- data/lib/capybara/playwright/node.rb +182 -43
- 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
|
@@ -105,3 +105,16 @@ end
|
|
|
105
105
|
## General Guidance
|
|
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
|
+
|
|
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
|
+
|
|
114
|
+
## How to execute RSpec or Ruby
|
|
115
|
+
|
|
116
|
+
`ruby` uses macOS system ruby interpreter (typically 2.6 or older). rbenv should be used.
|
|
117
|
+
|
|
118
|
+
- Do not run the full `spec/capybara/playwright_spec.rb` or full RSpec suite locally; run broad coverage in GitHub Actions.
|
|
119
|
+
- To run a focused Playwright compatibility check locally, use an example filter, for example:
|
|
120
|
+
`RBENV_VERSION=$(cat .ruby-version) ~/.rbenv/shims/bundle exec rspec spec/capybara/playwright_spec.rb --example fill_in`
|
data/Gemfile
CHANGED
|
@@ -15,6 +15,7 @@ gem 'rack-test_server'
|
|
|
15
15
|
gem 'rake', '~> 13.0.3'
|
|
16
16
|
gem 'rspec', '~> 3.11.0'
|
|
17
17
|
gem 'rubocop-rspec'
|
|
18
|
+
gem 'selenium-webdriver', '~> 4.34.0' if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2')
|
|
18
19
|
gem 'sinatra', '>= 1.4.0'
|
|
19
20
|
gem 'webrick'
|
|
20
21
|
gem 'websocket-driver'
|
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:
|
|
@@ -89,9 +89,29 @@ module Capybara
|
|
|
89
89
|
}
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
private def firefox?
|
|
93
|
+
@playwright_browser.browser_type.name == 'firefox'
|
|
94
|
+
end
|
|
95
|
+
|
|
92
96
|
def refresh
|
|
93
97
|
assert_page_alive {
|
|
94
|
-
|
|
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
|
|
112
|
+
response = @playwright_page.reload
|
|
113
|
+
@playwright_page.capybara_set_last_response(response)
|
|
114
|
+
end
|
|
95
115
|
}
|
|
96
116
|
end
|
|
97
117
|
|
|
@@ -255,7 +275,8 @@ module Capybara
|
|
|
255
275
|
when /Element is not attached to the DOM/,
|
|
256
276
|
/Execution context was destroyed, most likely because of a navigation/,
|
|
257
277
|
/Cannot find context with specified id/,
|
|
258
|
-
/Unable to adopt element handle from a different document
|
|
278
|
+
/Unable to adopt element handle from a different document/,
|
|
279
|
+
/maybe frame was detached/
|
|
259
280
|
# ignore error for retry
|
|
260
281
|
@internal_logger.warn(err.message)
|
|
261
282
|
else
|
|
@@ -318,16 +339,35 @@ module Capybara
|
|
|
318
339
|
|
|
319
340
|
def window_size(handle)
|
|
320
341
|
on_window(handle) do |page|
|
|
321
|
-
page
|
|
342
|
+
outer_window_size(page)
|
|
322
343
|
end
|
|
323
344
|
end
|
|
324
345
|
|
|
325
346
|
def resize_window_to(handle, width, height)
|
|
326
347
|
on_window(handle) do |page|
|
|
327
|
-
page.viewport_size =
|
|
348
|
+
page.viewport_size = viewport_size_for(page, width, height)
|
|
328
349
|
end
|
|
329
350
|
end
|
|
330
351
|
|
|
352
|
+
# Capybara follows Selenium's outer window size semantics.
|
|
353
|
+
private def outer_window_size(page)
|
|
354
|
+
page.evaluate('() => [window.outerWidth || window.innerWidth, window.outerHeight || window.innerHeight]')
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
private def viewport_size_for(page, width, height)
|
|
358
|
+
inset_width, inset_height = page.evaluate(<<~JAVASCRIPT)
|
|
359
|
+
() => [
|
|
360
|
+
Math.max(0, (window.outerWidth || window.innerWidth) - window.innerWidth),
|
|
361
|
+
Math.max(0, (window.outerHeight || window.innerHeight) - window.innerHeight)
|
|
362
|
+
]
|
|
363
|
+
JAVASCRIPT
|
|
364
|
+
|
|
365
|
+
{
|
|
366
|
+
width: [width - inset_width, 0].max,
|
|
367
|
+
height: [height - inset_height, 0].max
|
|
368
|
+
}
|
|
369
|
+
end
|
|
370
|
+
|
|
331
371
|
def maximize_window(handle)
|
|
332
372
|
@internal_logger.warn("maximize_window is not supported in Playwright driver")
|
|
333
373
|
# incomplete in Playwright
|
|
@@ -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
|
|
@@ -258,11 +274,26 @@ module Capybara
|
|
|
258
274
|
end
|
|
259
275
|
|
|
260
276
|
private def capybara_default_wait_time
|
|
261
|
-
Capybara.default_max_wait_time * 1100 # with 10% buffer for allowing overhead.
|
|
277
|
+
Capybara.default_max_wait_time.to_f * 1100 # with 10% buffer for allowing overhead.
|
|
262
278
|
end
|
|
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,28 +438,102 @@ 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
|
|
412
445
|
|
|
413
446
|
text = value.to_s
|
|
414
|
-
if press_enter = text.end_with?("\n")
|
|
447
|
+
if press_enter = text.end_with?("\r\n")
|
|
448
|
+
text = text[0...-2]
|
|
449
|
+
elsif press_enter = text.end_with?("\n")
|
|
415
450
|
text = text[0...-1]
|
|
416
451
|
end
|
|
417
452
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
453
|
+
keyboard = @element.owner_frame.page.keyboard
|
|
454
|
+
set_text(text, append: options[:clear] == :none, keyboard: keyboard)
|
|
455
|
+
|
|
456
|
+
keyboard.press('Enter') if press_enter
|
|
457
|
+
rescue ::Playwright::TimeoutError
|
|
458
|
+
raise if @element.editable?
|
|
459
|
+
|
|
460
|
+
@internal_logger.info("Node#set: element is not editable. #{@element}")
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
private def set_text(text, append:, keyboard:)
|
|
464
|
+
# ElementHandle#type can refocus inherited contenteditable descendants and drop the input.
|
|
465
|
+
if @element.evaluate('el => el.isContentEditable') && !append
|
|
421
466
|
@element.fill(text, timeout: @timeout)
|
|
467
|
+
return
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
text = text.gsub(/\r\n?/, "\n")
|
|
471
|
+
|
|
472
|
+
if text.include?("\t")
|
|
473
|
+
type_tab_separated_text(text, append: append, keyboard: keyboard)
|
|
474
|
+
return
|
|
422
475
|
end
|
|
423
476
|
|
|
424
|
-
if
|
|
477
|
+
if append
|
|
478
|
+
type_text(keyboard, text)
|
|
479
|
+
elsif text.empty?
|
|
480
|
+
@element.fill('', timeout: @timeout)
|
|
481
|
+
else
|
|
482
|
+
@element.select_text(timeout: @timeout)
|
|
483
|
+
replace_text(keyboard, text)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
private def replace_text(keyboard, text)
|
|
488
|
+
head, *tail = text.split("\n", -1)
|
|
489
|
+
if head.empty?
|
|
425
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?
|
|
426
499
|
end
|
|
427
|
-
rescue ::Playwright::TimeoutError
|
|
428
|
-
raise if @element.editable?
|
|
429
500
|
|
|
430
|
-
|
|
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
|
|
512
|
+
|
|
513
|
+
private def type_lines(keyboard, parts)
|
|
514
|
+
parts.each do |part|
|
|
515
|
+
keyboard.press('Enter')
|
|
516
|
+
keyboard.type(part) unless part.empty?
|
|
517
|
+
end
|
|
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?
|
|
431
537
|
end
|
|
432
538
|
end
|
|
433
539
|
|
|
@@ -537,20 +643,26 @@ module Capybara
|
|
|
537
643
|
end
|
|
538
644
|
|
|
539
645
|
def click(keys = [], **options)
|
|
540
|
-
|
|
541
|
-
|
|
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
|
|
542
650
|
end
|
|
543
651
|
|
|
544
652
|
def right_click(keys = [], **options)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
|
549
659
|
end
|
|
550
660
|
|
|
551
661
|
def double_click(keys = [], **options)
|
|
552
|
-
|
|
553
|
-
|
|
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
|
|
554
666
|
end
|
|
555
667
|
|
|
556
668
|
class ClickOptions
|
|
@@ -620,7 +732,7 @@ module Capybara
|
|
|
620
732
|
|
|
621
733
|
private def position
|
|
622
734
|
if @offset_center
|
|
623
|
-
box = @element
|
|
735
|
+
box = ElementGeometry.bounding_box(@element)
|
|
624
736
|
|
|
625
737
|
{
|
|
626
738
|
x: @coords[:x] + box['width'] / 2,
|
|
@@ -807,11 +919,17 @@ module Capybara
|
|
|
807
919
|
end
|
|
808
920
|
|
|
809
921
|
def hover
|
|
810
|
-
|
|
922
|
+
assert_element_not_stale do
|
|
923
|
+
@element.hover(timeout: capybara_default_wait_time)
|
|
924
|
+
end
|
|
811
925
|
end
|
|
812
926
|
|
|
813
|
-
def drag_to(
|
|
814
|
-
|
|
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
|
|
815
933
|
end
|
|
816
934
|
|
|
817
935
|
class DragTo
|
|
@@ -828,22 +946,28 @@ module Capybara
|
|
|
828
946
|
# @param page [Playwright::Page]
|
|
829
947
|
# @param source [Playwright::ElementHandle]
|
|
830
948
|
# @param target [Playwright::ElementHandle]
|
|
831
|
-
def initialize(page, source, target, options)
|
|
949
|
+
def initialize(page, source, target, options, timeout)
|
|
832
950
|
@page = page
|
|
833
951
|
@source = source
|
|
834
952
|
@target = target
|
|
835
953
|
@options = options
|
|
954
|
+
@timeout = timeout
|
|
836
955
|
end
|
|
837
956
|
|
|
838
957
|
def execute
|
|
839
|
-
|
|
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)
|
|
840
962
|
|
|
841
|
-
# down
|
|
842
963
|
position_from = center_of(@source)
|
|
964
|
+
center_of(@target)
|
|
843
965
|
@page.mouse.move(*position_from)
|
|
966
|
+
input_started = true
|
|
967
|
+
mouse_down = true
|
|
844
968
|
@page.mouse.down
|
|
845
969
|
|
|
846
|
-
@target.scroll_into_view_if_needed
|
|
970
|
+
@target.scroll_into_view_if_needed(timeout: @timeout)
|
|
847
971
|
|
|
848
972
|
# move and up
|
|
849
973
|
sleep_delay
|
|
@@ -852,20 +976,33 @@ module Capybara
|
|
|
852
976
|
@page.mouse.move(*position_to, steps: 6)
|
|
853
977
|
sleep_delay
|
|
854
978
|
@page.mouse.up
|
|
979
|
+
mouse_down = false
|
|
855
980
|
end
|
|
856
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
|
|
857
988
|
end
|
|
858
989
|
|
|
859
990
|
# @param element [Playwright::ElementHandle]
|
|
860
991
|
private def center_of(element)
|
|
861
|
-
box =
|
|
992
|
+
box = ElementGeometry.bounding_box(element)
|
|
993
|
+
|
|
862
994
|
[box["x"] + box["width"] / 2, box["y"] + box["height"] / 2]
|
|
863
995
|
end
|
|
864
996
|
|
|
865
|
-
private def with_key_pressing(keys
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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) }
|
|
869
1006
|
end
|
|
870
1007
|
|
|
871
1008
|
# @returns Array<String>
|
|
@@ -1045,7 +1182,9 @@ module Capybara
|
|
|
1045
1182
|
end
|
|
1046
1183
|
|
|
1047
1184
|
def obscured?
|
|
1048
|
-
|
|
1185
|
+
assert_element_not_stale do
|
|
1186
|
+
@element.capybara_obscured?
|
|
1187
|
+
end
|
|
1049
1188
|
end
|
|
1050
1189
|
|
|
1051
1190
|
def checked?
|