capybara-playwright-driver 0.5.13 → 0.5.14
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/README.md +31 -8
- data/lib/capybara/playwright/browser.rb +1 -1
- data/lib/capybara/playwright/node.rb +179 -221
- 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: 35fdde9b43c2e26b14f271e049ca2a9599d36552a777a00dacaaf6e498b96581
|
|
4
|
+
data.tar.gz: acf38394ff2727fb1cf519d67401c8c1012bd9b7410dfbc3e7cbeb599fc9401d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 991e90ee94c3ed6e7cb44fc1a1f838e95b7de72cb29ce271ce7c65e0069c712a8c569c8e836bcf6b62bd3932dfcb79ba5569b35497cf6e8dc6659099de8adeca
|
|
7
|
+
data.tar.gz: 2da9229fcb4f1856f34220aaddb8bff505eb12cda450706e754f678372c6b48c7c133099cdf8420788abcf80ffa35658cb740024410e4ad2e1a30dae2483542a
|
data/README.md
CHANGED
|
@@ -46,6 +46,16 @@ Refer the [documentation](https://playwright-ruby-client.vercel.app/docs/article
|
|
|
46
46
|
|
|
47
47
|
## Selenium compatibility
|
|
48
48
|
|
|
49
|
+
`visible?` and selector filters such as `visible: true` do not account for ancestor overflow clipping. An element
|
|
50
|
+
completely clipped by an ancestor with `overflow: hidden` can still be considered visible by this driver, while
|
|
51
|
+
Selenium considers it hidden. This can change the results of queries such as `all('.item', visible: true)` when
|
|
52
|
+
migrating Selenium tests.
|
|
53
|
+
|
|
54
|
+
Playwright's native visibility methods (`ElementHandle#visible?` and `Locator#visible?` in playwright-ruby-client)
|
|
55
|
+
also consider these clipped elements visible. If a test needs to count items actually displayed inside a clipped
|
|
56
|
+
container, check their geometry relative to the container explicitly. See
|
|
57
|
+
[#174](https://github.com/YusukeIwaki/capybara-playwright-driver/issues/174) for the driver comparison and discussion.
|
|
58
|
+
|
|
49
59
|
Visible text for ordinary HTML elements follows the browser's
|
|
50
60
|
[`innerText` behavior](https://html.spec.whatwg.org/multipage/dom.html#the-innertext-idl-attribute). This preserves
|
|
51
61
|
meaningful blank lines, but paragraph and rich-text-editor boundaries can differ from Selenium's WebDriver text
|
|
@@ -55,18 +65,31 @@ algorithm. When exact whitespace is not part of an assertion, use Capybara's `no
|
|
|
55
65
|
expect(message).to have_text('Hello Regards', normalize_ws: true)
|
|
56
66
|
```
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
Element `send_keys` follows WebDriver's focus and selection rules: an unfocused text input, textarea, or
|
|
69
|
+
`contenteditable` starts at the end of its content; an already focused element keeps its caret or selection.
|
|
70
|
+
The element is prepared once per call. Subsequent keys follow focus changes made by the application, including Tab
|
|
71
|
+
navigation and autocomplete controls. For example, a new call can return to an editor after its autocomplete takes focus:
|
|
62
72
|
|
|
63
73
|
```ruby
|
|
64
|
-
editor.send_keys(
|
|
65
|
-
editor.send_keys(
|
|
74
|
+
editor.send_keys('@') # The page moves focus to an autocomplete control.
|
|
75
|
+
editor.send_keys('alice') # Focus the editor again and append at its end.
|
|
66
76
|
```
|
|
67
77
|
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
`set`, `fill_in`, and `send_keys` use the same keyboard sequence handling, including Enter for newlines and Tab for
|
|
79
|
+
tab characters. Replacement selects the target content before typing, including inherited `contenteditable` children.
|
|
80
|
+
`clear: :none` preserves an existing selection when the input already has focus; `clear: :backspace` and key arrays
|
|
81
|
+
send their clearing keys before the replacement text. Contenteditable replacement retains Playwright's visibility,
|
|
82
|
+
enabled-state, and editability waiting. Element key presses retain navigation waiting while following the current
|
|
83
|
+
focus. Elements replaced during waiting, focus, or selection can be retried before typing starts. Once typing begins,
|
|
84
|
+
the sequence is not replayed when the original element is replaced. A Playwright error during dispatch raises
|
|
85
|
+
`Capybara::Playwright::Node::InputInterruptedError` rather than retrying potentially delivered input.
|
|
86
|
+
|
|
87
|
+
The compatibility specs record two intentional differences from Capybara Selenium: setting an empty string clears
|
|
88
|
+
`contenteditable` content, and a focused input inside a shadow root keeps its selection instead of moving to the end.
|
|
89
|
+
They also retain Playwright's wait for ARIA disabled/readonly state, which Selenium does not apply to contenteditable.
|
|
90
|
+
|
|
91
|
+
Text entry uses Playwright's typing behavior for every character regardless of the value length. Playwright sends
|
|
92
|
+
`keydown`, `keypress`/`input`, and `keyup` for characters on a US keyboard, but sends only
|
|
70
93
|
`input` for other characters, including Japanese text. This follows
|
|
71
94
|
[Playwright's documented keyboard behavior](https://playwright.dev/docs/api/class-keyboard#keyboard-type). Typing very
|
|
72
95
|
large values is slower than Playwright's native `fill`, which does not send per-character keyboard events. When input
|
|
@@ -56,86 +56,26 @@ module Capybara
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def set_checked_state_via_label?
|
|
59
|
-
|
|
60
|
-
playwright_checkable ||= playwright_locator_by_label unless @locator.nil?
|
|
61
|
-
return false unless playwright_checkable
|
|
59
|
+
return false if @locator.nil?
|
|
62
60
|
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
checkable = @node.find(@node_type, @locator, visible: :all, wait: 0)
|
|
62
|
+
click_associated_label?(checkable.native)
|
|
63
|
+
rescue Capybara::ElementNotFound, Capybara::ExpectationNotMet,
|
|
64
|
+
Capybara::Playwright::Node::StaleReferenceError, ::Playwright::Error
|
|
65
65
|
false
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
private
|
|
69
69
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
return playwright_page.get_by_label(@locator.to_s)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def click_associated_label?(playwright_element_handle_or_locator)
|
|
77
|
-
return true if playwright_element_handle_or_locator.evaluate('el => !!el.checked') == @checked
|
|
70
|
+
def click_associated_label?(element_handle)
|
|
71
|
+
return true if element_handle.evaluate('el => !!el.checked') == @checked
|
|
78
72
|
|
|
79
|
-
label_element_handle =
|
|
73
|
+
label_element_handle = element_handle.evaluate_handle('(el) => (el.labels && el.labels[0]) || el.closest("label") || null')
|
|
80
74
|
return false unless label_element_handle.is_a?(::Playwright::ElementHandle)
|
|
81
75
|
|
|
82
76
|
label_element_handle.click
|
|
83
77
|
|
|
84
|
-
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def find_playwright_element_handle_by_non_label_locator
|
|
88
|
-
return nil if @locator.nil?
|
|
89
|
-
|
|
90
|
-
locator_string = @locator.to_s
|
|
91
|
-
test_id_attr = session_options.test_id&.to_s
|
|
92
|
-
|
|
93
|
-
driver.with_playwright_page do |playwright_page|
|
|
94
|
-
return non_label_playwright_element_handle_candidates(playwright_page).find do |element_handle|
|
|
95
|
-
attribute_values = element_handle.evaluate(<<~JAVASCRIPT, arg: test_id_attr)
|
|
96
|
-
(el, testIdAttr) => ({
|
|
97
|
-
id: el.id || '',
|
|
98
|
-
name: el.getAttribute('name') || '',
|
|
99
|
-
testId: testIdAttr ? (el.getAttribute(testIdAttr) || '') : '',
|
|
100
|
-
})
|
|
101
|
-
JAVASCRIPT
|
|
102
|
-
[attribute_values['id'], attribute_values['name'], attribute_values['testId']].include?(locator_string)
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
rescue ::Playwright::Error
|
|
106
|
-
nil
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def non_label_playwright_element_handle_candidates(playwright_page)
|
|
110
|
-
input_type =
|
|
111
|
-
case @node_type
|
|
112
|
-
when :checkbox
|
|
113
|
-
'checkbox'
|
|
114
|
-
when :radio_button
|
|
115
|
-
'radio'
|
|
116
|
-
else
|
|
117
|
-
return []
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
current_scope = scope_element
|
|
121
|
-
return current_scope.query_selector_all(%(input[type="#{input_type}"])) if current_scope
|
|
122
|
-
|
|
123
|
-
playwright_page.capybara_current_frame.query_selector_all(%(input[type="#{input_type}"]))
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def scope_element
|
|
127
|
-
return nil unless @node.is_a?(Capybara::Node::Element)
|
|
128
|
-
return nil unless @node.send(:base).is_a?(Capybara::Playwright::Node)
|
|
129
|
-
|
|
130
|
-
@node.send(:base).send(:element)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def driver
|
|
134
|
-
@node.send(:driver)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def session_options
|
|
138
|
-
@node.send(:session_options)
|
|
78
|
+
element_handle.evaluate('el => !!el.checked') == @checked
|
|
139
79
|
end
|
|
140
80
|
end
|
|
141
81
|
|
|
@@ -279,6 +219,7 @@ module Capybara
|
|
|
279
219
|
|
|
280
220
|
class NotActionableError < StandardError ; end
|
|
281
221
|
class StaleReferenceError < StandardError ; end
|
|
222
|
+
class InputInterruptedError < StandardError ; end
|
|
282
223
|
class MissingBoundingBoxError < StandardError ; end
|
|
283
224
|
class DragInterruptedError < StandardError ; end
|
|
284
225
|
|
|
@@ -431,109 +372,154 @@ module Capybara
|
|
|
431
372
|
|
|
432
373
|
class TextInput < Settable
|
|
433
374
|
def set(value, **options)
|
|
375
|
+
text = value.to_s
|
|
376
|
+
input = KeyboardInput.new(@element, @timeout)
|
|
377
|
+
|
|
434
378
|
case options[:clear]
|
|
435
|
-
when :backspace
|
|
436
|
-
@element.press('End', timeout: @timeout)
|
|
437
|
-
existing_text = @element.evaluate('el => el.value')
|
|
438
|
-
existing_text.length.times { @element.press('Backspace', timeout: @timeout) }
|
|
439
379
|
when :none
|
|
440
|
-
|
|
441
|
-
|
|
380
|
+
input.send_keys(text)
|
|
381
|
+
when :backspace
|
|
382
|
+
length = @element.evaluate('el => el.value || el.textContent || ""').length
|
|
383
|
+
input.send_keys(:end, *Array.new(length, :backspace), text)
|
|
442
384
|
when Array
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
text = text[0...-1]
|
|
385
|
+
input.send_keys(*options[:clear], text)
|
|
386
|
+
else
|
|
387
|
+
if text.empty?
|
|
388
|
+
@element.fill('', timeout: @timeout)
|
|
389
|
+
else
|
|
390
|
+
input.replace(text)
|
|
391
|
+
end
|
|
451
392
|
end
|
|
452
|
-
|
|
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
393
|
rescue ::Playwright::TimeoutError
|
|
458
394
|
raise if @element.editable?
|
|
459
395
|
|
|
460
396
|
@internal_logger.info("Node#set: element is not editable. #{@element}")
|
|
461
397
|
end
|
|
398
|
+
end
|
|
462
399
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
400
|
+
# Prepare the selection once, then let the browser route the whole sequence.
|
|
401
|
+
# Inherited contenteditable descendants are selected independently of their
|
|
402
|
+
# editing host, which is the element that can actually receive focus.
|
|
403
|
+
class KeyboardInput
|
|
404
|
+
def initialize(element, timeout)
|
|
405
|
+
@element = element
|
|
406
|
+
@timeout = timeout
|
|
407
|
+
end
|
|
471
408
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
409
|
+
def replace(text)
|
|
410
|
+
if @element.evaluate('element => element.isContentEditable')
|
|
411
|
+
# This path used fill(), which waits for visibility and editability
|
|
412
|
+
# (including enabled state). select_text() alone only waits for visibility.
|
|
413
|
+
# https://playwright.dev/docs/actionability
|
|
414
|
+
@element.wait_for_element_state('visible', timeout: @timeout)
|
|
415
|
+
@element.wait_for_element_state('editable', timeout: @timeout)
|
|
475
416
|
end
|
|
417
|
+
@element.select_text(timeout: @timeout)
|
|
418
|
+
type_keys([text])
|
|
419
|
+
end
|
|
476
420
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
421
|
+
def send_keys(*keys)
|
|
422
|
+
# WebDriver Element Send Keys moves the caret to the end only when the
|
|
423
|
+
# element was not focused. Preserve the selection in an active editor.
|
|
424
|
+
# https://www.w3.org/TR/2026/WD-webdriver2-20260702/#element-send-keys
|
|
425
|
+
#
|
|
426
|
+
# References in ChromeDriver's element_commands.cc:
|
|
427
|
+
# - SendKeysToElement: append to newly focused text controls, then send
|
|
428
|
+
# the whole sequence to the window without refocusing for each key.
|
|
429
|
+
# https://github.com/chromium/chromium/blob/852d53b2cdde761fff9003021300b31c15fc1636/chrome/test/chromedriver/element_commands.cc#L191-L222
|
|
430
|
+
# - ExecuteSendKeysToElement: find the top editable ancestor for focus,
|
|
431
|
+
# but collapse a Range over the requested element's own contents.
|
|
432
|
+
# https://github.com/chromium/chromium/blob/852d53b2cdde761fff9003021300b31c15fc1636/chrome/test/chromedriver/element_commands.cc#L735-L799
|
|
433
|
+
# This adapts the caret preparation, not all of ChromeDriver's
|
|
434
|
+
# interactability checks; our differential specs cover the contract.
|
|
435
|
+
@element.evaluate(<<~JAVASCRIPT)
|
|
436
|
+
element => {
|
|
437
|
+
let host = element;
|
|
438
|
+
if (element.isContentEditable) {
|
|
439
|
+
while (host.parentElement && host.parentElement.isContentEditable)
|
|
440
|
+
host = host.parentElement;
|
|
441
|
+
}
|
|
442
|
+
// Our shadow DOM adaptation of ChromeDriver's active-element check:
|
|
443
|
+
// document.activeElement would identify the shadow host instead.
|
|
444
|
+
const root = host.getRootNode();
|
|
445
|
+
if (root.activeElement === host) return;
|
|
446
|
+
|
|
447
|
+
// Focus first so it cannot reset the Range we are about to create.
|
|
448
|
+
host.focus();
|
|
449
|
+
if (element.isContentEditable) {
|
|
450
|
+
// Use the target's document so the Range also belongs to its iframe.
|
|
451
|
+
const range = element.ownerDocument.createRange();
|
|
452
|
+
range.selectNodeContents(element);
|
|
453
|
+
range.collapse(false);
|
|
454
|
+
const selection = element.ownerDocument.getSelection();
|
|
455
|
+
selection.removeAllRanges();
|
|
456
|
+
selection.addRange(range);
|
|
457
|
+
} else if (element.nodeName === 'TEXTAREA' ||
|
|
458
|
+
(element.nodeName === 'INPUT' && /^(text|search|tel|url|password)$/.test(element.type))) {
|
|
459
|
+
// ChromeDriver's kTextControlTypes: other input types do not
|
|
460
|
+
// support setSelectionRange (e.g. number and email).
|
|
461
|
+
element.setSelectionRange(element.value.length, element.value.length);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
JAVASCRIPT
|
|
465
|
+
type_keys(keys)
|
|
485
466
|
end
|
|
486
467
|
|
|
487
|
-
private def
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
468
|
+
private def type_keys(keys)
|
|
469
|
+
keyboard = FocusedKeyboard.new(@element.owner_frame.page, @timeout)
|
|
470
|
+
sequence = SendKeys.new(keyboard, keys)
|
|
471
|
+
# Focus/select can replace the element in a microtask. Check before
|
|
472
|
+
# dispatch so Capybara can reload it without replaying any keystrokes.
|
|
473
|
+
@element.enabled?
|
|
474
|
+
# Do not recheck the original handle after input has begun: the app may
|
|
475
|
+
# deliberately replace it or move focus, and replaying can duplicate input.
|
|
476
|
+
begin
|
|
477
|
+
sequence.execute
|
|
478
|
+
rescue ::Playwright::Error => err
|
|
479
|
+
# A navigation or a detached current focus can fail after earlier
|
|
480
|
+
# keys were delivered. Do not turn that into a retry of the whole set.
|
|
481
|
+
raise InputInterruptedError.new(err)
|
|
499
482
|
end
|
|
500
|
-
|
|
501
|
-
type_lines(keyboard, tail)
|
|
502
483
|
end
|
|
484
|
+
end
|
|
503
485
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
keyboard.type(head) unless head.empty?
|
|
509
|
-
|
|
510
|
-
type_lines(keyboard, tail)
|
|
486
|
+
class FocusedKeyboard
|
|
487
|
+
def initialize(page, timeout)
|
|
488
|
+
@page = page
|
|
489
|
+
@timeout = timeout
|
|
511
490
|
end
|
|
512
491
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
keyboard.press('Enter')
|
|
516
|
-
keyboard.type(part) unless part.empty?
|
|
517
|
-
end
|
|
492
|
+
def type(text)
|
|
493
|
+
@page.keyboard.type(text)
|
|
518
494
|
end
|
|
519
495
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
end
|
|
530
|
-
type_text(keyboard, part.gsub(/\r\n?/, "\n"))
|
|
531
|
-
end
|
|
496
|
+
def press(key)
|
|
497
|
+
# ElementHandle#press waits for navigation triggered by the key, unlike
|
|
498
|
+
# Keyboard#press. Use the current focus so Tab/autocomplete can redirect
|
|
499
|
+
# later keys without returning to the original element.
|
|
500
|
+
# https://playwright.dev/docs/api/class-elementhandle#element-handle-press
|
|
501
|
+
focused = focused_element(@page.main_frame)
|
|
502
|
+
focused.press(key, timeout: @timeout)
|
|
503
|
+
ensure
|
|
504
|
+
focused.dispose if focused
|
|
532
505
|
end
|
|
533
506
|
|
|
534
|
-
private def
|
|
535
|
-
#
|
|
536
|
-
|
|
507
|
+
private def focused_element(frame)
|
|
508
|
+
# Local focus traversal through shadow roots and frames. Pressing the iframe
|
|
509
|
+
# itself would blur its editor. content_frame also works across origins.
|
|
510
|
+
focused = frame.evaluate_handle(<<~JAVASCRIPT)
|
|
511
|
+
() => {
|
|
512
|
+
let element = document.activeElement;
|
|
513
|
+
while (element && element.shadowRoot && element.shadowRoot.activeElement)
|
|
514
|
+
element = element.shadowRoot.activeElement;
|
|
515
|
+
return element;
|
|
516
|
+
}
|
|
517
|
+
JAVASCRIPT
|
|
518
|
+
child_frame = focused.content_frame
|
|
519
|
+
return focused unless child_frame
|
|
520
|
+
|
|
521
|
+
focused.dispose
|
|
522
|
+
focused_element(child_frame)
|
|
537
523
|
end
|
|
538
524
|
end
|
|
539
525
|
|
|
@@ -745,7 +731,9 @@ module Capybara
|
|
|
745
731
|
end
|
|
746
732
|
|
|
747
733
|
def send_keys(*args)
|
|
748
|
-
|
|
734
|
+
assert_element_not_stale do
|
|
735
|
+
KeyboardInput.new(@element, capybara_default_wait_time).send_keys(*args)
|
|
736
|
+
end
|
|
749
737
|
end
|
|
750
738
|
|
|
751
739
|
class SendKeys
|
|
@@ -817,93 +805,63 @@ module Capybara
|
|
|
817
805
|
command: 'Meta',
|
|
818
806
|
}
|
|
819
807
|
|
|
820
|
-
def initialize(
|
|
821
|
-
@
|
|
808
|
+
def initialize(keyboard, keys)
|
|
809
|
+
@keyboard = keyboard
|
|
810
|
+
@executables = []
|
|
811
|
+
add_keys(keys)
|
|
812
|
+
end
|
|
822
813
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
modifiers: [],
|
|
836
|
-
)
|
|
837
|
-
when Array
|
|
838
|
-
_key = key.last
|
|
839
|
-
code =
|
|
840
|
-
if _key.is_a?(String) && _key.length == 1
|
|
841
|
-
_key
|
|
842
|
-
elsif _key.is_a?(Symbol)
|
|
843
|
-
key_for(_key)
|
|
844
|
-
else
|
|
845
|
-
raise ArgumentError.new("invalid key: #{_key}. Symbol of 1-length String is expected.")
|
|
846
|
-
end
|
|
847
|
-
modifiers = key.first(key.size - 1).map { |k| modifier_for(k) }
|
|
848
|
-
executables << PressKey.new(
|
|
849
|
-
key: code,
|
|
850
|
-
modifiers: modifiers,
|
|
851
|
-
)
|
|
852
|
-
end
|
|
853
|
-
else
|
|
854
|
-
modifiers = holding_keys.map { |k| modifier_for(k) }
|
|
855
|
-
|
|
856
|
-
case key
|
|
857
|
-
when String
|
|
858
|
-
key.each_char do |char|
|
|
859
|
-
executables << PressKey.new(
|
|
860
|
-
key: char,
|
|
861
|
-
modifiers: modifiers,
|
|
862
|
-
)
|
|
863
|
-
end
|
|
864
|
-
when Symbol
|
|
865
|
-
executables << PressKey.new(
|
|
866
|
-
key: key_for(key),
|
|
867
|
-
modifiers: modifiers
|
|
868
|
-
)
|
|
814
|
+
private def add_keys(keys, modifiers = [])
|
|
815
|
+
keys.each do |key|
|
|
816
|
+
case key
|
|
817
|
+
when Array
|
|
818
|
+
add_keys(key, modifiers.dup)
|
|
819
|
+
when String
|
|
820
|
+
key.scan(/\r\n|[\r\n\t]|[^\r\n\t]+/).each do |part|
|
|
821
|
+
case part
|
|
822
|
+
when "\t"
|
|
823
|
+
add_key('Tab', modifiers)
|
|
824
|
+
when "\r", "\n", "\r\n"
|
|
825
|
+
add_key('Enter', modifiers)
|
|
869
826
|
else
|
|
870
|
-
|
|
827
|
+
if modifiers.empty?
|
|
828
|
+
@executables << TypeText.new(part)
|
|
829
|
+
else
|
|
830
|
+
part.each_char { |character| add_key(character, modifiers) }
|
|
831
|
+
end
|
|
871
832
|
end
|
|
872
833
|
end
|
|
834
|
+
when Symbol
|
|
835
|
+
if MODIFIERS.key?(key)
|
|
836
|
+
modifiers << MODIFIERS.fetch(key)
|
|
837
|
+
elsif key == :null
|
|
838
|
+
modifiers.clear
|
|
839
|
+
else
|
|
840
|
+
add_key(KEYS.fetch(key) { raise ArgumentError, "invalid key specified: #{key}" }, modifiers)
|
|
841
|
+
end
|
|
842
|
+
else
|
|
843
|
+
raise ArgumentError, "invalid key specified: #{key.inspect}"
|
|
873
844
|
end
|
|
874
845
|
end
|
|
875
846
|
end
|
|
876
847
|
|
|
877
|
-
private def
|
|
878
|
-
|
|
879
|
-
end
|
|
880
|
-
|
|
881
|
-
private def key_for(key)
|
|
882
|
-
KEYS[key] or raise ArgumentError.new("invalid key specified: #{key}")
|
|
848
|
+
private def add_key(key, modifiers)
|
|
849
|
+
@executables << PressKey.new(key: key, modifiers: modifiers)
|
|
883
850
|
end
|
|
884
851
|
|
|
885
852
|
def execute
|
|
886
|
-
@executables.each
|
|
887
|
-
executable.execute_for(@element_or_keyboard)
|
|
888
|
-
end
|
|
853
|
+
@executables.each { |executable| executable.execute_for(@keyboard) }
|
|
889
854
|
end
|
|
890
855
|
|
|
891
856
|
class PressKey
|
|
892
857
|
def initialize(key:, modifiers:)
|
|
893
|
-
#
|
|
894
|
-
|
|
895
|
-
key =
|
|
896
|
-
|
|
897
|
-
# puts "PressKey: key=#{key} modifiers: #{modifiers}"
|
|
898
|
-
if modifiers.empty?
|
|
899
|
-
@key = key
|
|
900
|
-
else
|
|
901
|
-
@key = (modifiers + [key]).join('+')
|
|
902
|
-
end
|
|
858
|
+
# Playwright needs the uppercase key when Shift modifies a letter.
|
|
859
|
+
key = key.upcase if modifiers.include?('Shift') && key.match?(/^[a-z]$/)
|
|
860
|
+
@key = (modifiers + [key]).uniq.join('+')
|
|
903
861
|
end
|
|
904
862
|
|
|
905
|
-
def execute_for(
|
|
906
|
-
|
|
863
|
+
def execute_for(keyboard)
|
|
864
|
+
keyboard.press(@key)
|
|
907
865
|
end
|
|
908
866
|
end
|
|
909
867
|
|
|
@@ -912,8 +870,8 @@ module Capybara
|
|
|
912
870
|
@text = text
|
|
913
871
|
end
|
|
914
872
|
|
|
915
|
-
def execute_for(
|
|
916
|
-
|
|
873
|
+
def execute_for(keyboard)
|
|
874
|
+
keyboard.type(@text)
|
|
917
875
|
end
|
|
918
876
|
end
|
|
919
877
|
end
|