capybara-playwright-driver 0.5.8 → 0.5.9
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 +107 -0
- data/lib/capybara/playwright/browser.rb +1 -1
- data/lib/capybara/playwright/node.rb +180 -39
- data/lib/capybara/playwright/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a319465a40c53ac5b47d86ccd4d15136c20dcf7f6dde6fe17b3906d44cb2fd94
|
|
4
|
+
data.tar.gz: 1e38fb6020a87a07e6eaa8e997084b3abd98a0a8d5cbec4e92819a960c525f62
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d1d8aeccb05f0c667c3b1a617cb1d2f19eb7fa096788a46b81985730f1a75b6ee7196ad952d2152c173b7401980090f738db399503b3ddceac097ced637678c
|
|
7
|
+
data.tar.gz: 141fd5024a9d322faf9031951e0f74a61426af350ed06d73017b2db90ec5b866e1af9b17edd5017e5c35f79f198cfa5916b220afcf621db9a2538c5fb50f443b
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Ruby Style
|
|
4
|
+
- This repository is Ruby-first. Prefer Ruby naming and control flow over Python- or Java-style ceremony.
|
|
5
|
+
- Use names that describe the role in the domain. Avoid vague plumbing names like `context`, `handler`, `data`, or `control` unless they are the clearest possible name.
|
|
6
|
+
- Boolean-returning methods must end with `?`.
|
|
7
|
+
- Prefer `find`, `any?`, early return, and guard clauses over `value = nil` plus later reassignment.
|
|
8
|
+
- Do not add `attr_reader` for every instance variable in a small internal object. If the state is only used internally, access the instance variable directly.
|
|
9
|
+
- If an object is already scoped to one responsibility, avoid redundant prefixes in every private method name.
|
|
10
|
+
- Do not prefix Ruby private methods with `_`. Use `private` for visibility and a normal method name for intent.
|
|
11
|
+
|
|
12
|
+
## Naming Examples
|
|
13
|
+
|
|
14
|
+
Bad:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
def _playwright_try_get_by_label(locator)
|
|
18
|
+
# ...
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Good:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
def find_by_label(locator)
|
|
26
|
+
# ...
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Bad:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
def click_associated_label(control)
|
|
34
|
+
return true if control.evaluate("el => !!el.checked")
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Good:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
def click_associated_label?(playwright_locator)
|
|
42
|
+
return true if playwright_locator.evaluate("el => !!el.checked")
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Bad:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
def initialize(node_context:, selector:, locator:, checked:)
|
|
50
|
+
@node_context = node_context
|
|
51
|
+
@selector = selector
|
|
52
|
+
@locator = locator
|
|
53
|
+
@checked = checked
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Good:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
def initialize(node:, node_type:, locator:, checked:)
|
|
61
|
+
@node = node
|
|
62
|
+
@node_type = node_type
|
|
63
|
+
@locator = locator
|
|
64
|
+
@checked = checked
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Bad:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
control = nil
|
|
72
|
+
|
|
73
|
+
candidates.each do |candidate|
|
|
74
|
+
next unless matches?(candidate)
|
|
75
|
+
|
|
76
|
+
control = candidate
|
|
77
|
+
break
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
control
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Good:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
candidates.find do |element_handle|
|
|
87
|
+
matches?(element_handle)
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Bad:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
attr_reader :node, :node_type, :locator, :checked
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Good:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
def click_associated_label?(playwright_locator)
|
|
101
|
+
return true if playwright_locator.evaluate("el => !!el.checked") == @checked
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## General Guidance
|
|
106
|
+
- Prefer concise Ruby that reads top-to-bottom without carrying unnecessary temporary state.
|
|
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.
|
|
@@ -31,30 +31,127 @@ module Capybara
|
|
|
31
31
|
Node::Element.prepend(WithElementHandlePatch)
|
|
32
32
|
|
|
33
33
|
module NodeActionsAllowLabelClickPatch
|
|
34
|
+
class SelectableElementHandler
|
|
35
|
+
def initialize(node:, node_type:, locator:, checked:)
|
|
36
|
+
@node = node
|
|
37
|
+
@node_type = node_type
|
|
38
|
+
@locator = locator
|
|
39
|
+
@checked = checked
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def set_checked_state_via_label?
|
|
43
|
+
playwright_checkable = find_playwright_element_handle_by_non_label_locator
|
|
44
|
+
playwright_checkable ||= playwright_locator_by_label unless @locator.nil?
|
|
45
|
+
return false unless playwright_checkable
|
|
46
|
+
|
|
47
|
+
click_associated_label?(playwright_checkable)
|
|
48
|
+
rescue Capybara::ElementNotFound, Capybara::ExpectationNotMet, ::Playwright::Error
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def playwright_locator_by_label
|
|
55
|
+
driver.with_playwright_page do |playwright_page|
|
|
56
|
+
return playwright_page.get_by_label(@locator.to_s)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def click_associated_label?(playwright_element_handle_or_locator)
|
|
61
|
+
return true if playwright_element_handle_or_locator.evaluate('el => !!el.checked') == @checked
|
|
62
|
+
|
|
63
|
+
label_element_handle = playwright_element_handle_or_locator.evaluate_handle('(el) => (el.labels && el.labels[0]) || el.closest("label") || null')
|
|
64
|
+
return false unless label_element_handle.is_a?(::Playwright::ElementHandle)
|
|
65
|
+
|
|
66
|
+
label_element_handle.click
|
|
67
|
+
|
|
68
|
+
playwright_element_handle_or_locator.evaluate('el => !!el.checked') == @checked
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def find_playwright_element_handle_by_non_label_locator
|
|
72
|
+
return nil if @locator.nil?
|
|
73
|
+
|
|
74
|
+
locator_string = @locator.to_s
|
|
75
|
+
test_id_attr = session_options.test_id&.to_s
|
|
76
|
+
|
|
77
|
+
driver.with_playwright_page do |playwright_page|
|
|
78
|
+
return non_label_playwright_element_handle_candidates(playwright_page).find do |element_handle|
|
|
79
|
+
attribute_values = element_handle.evaluate(<<~JAVASCRIPT, arg: test_id_attr)
|
|
80
|
+
(el, testIdAttr) => ({
|
|
81
|
+
id: el.id || '',
|
|
82
|
+
name: el.getAttribute('name') || '',
|
|
83
|
+
testId: testIdAttr ? (el.getAttribute(testIdAttr) || '') : '',
|
|
84
|
+
})
|
|
85
|
+
JAVASCRIPT
|
|
86
|
+
[attribute_values['id'], attribute_values['name'], attribute_values['testId']].include?(locator_string)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
rescue ::Playwright::Error
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def non_label_playwright_element_handle_candidates(playwright_page)
|
|
94
|
+
input_type =
|
|
95
|
+
case @node_type
|
|
96
|
+
when :checkbox
|
|
97
|
+
'checkbox'
|
|
98
|
+
when :radio_button
|
|
99
|
+
'radio'
|
|
100
|
+
else
|
|
101
|
+
return []
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
current_scope = scope_element
|
|
105
|
+
return current_scope.query_selector_all(%(input[type="#{input_type}"])) if current_scope
|
|
106
|
+
|
|
107
|
+
playwright_page.capybara_current_frame.query_selector_all(%(input[type="#{input_type}"]))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def scope_element
|
|
111
|
+
return nil unless @node.is_a?(Capybara::Node::Element)
|
|
112
|
+
return nil unless @node.send(:base).is_a?(Capybara::Playwright::Node)
|
|
113
|
+
|
|
114
|
+
@node.send(:base).send(:element)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def driver
|
|
118
|
+
@node.send(:driver)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def session_options
|
|
122
|
+
@node.send(:session_options)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
34
126
|
def choose(locator = nil, **options)
|
|
35
|
-
|
|
127
|
+
check_via_label_click(:radio_button, locator, checked: true, **options) { super }
|
|
36
128
|
end
|
|
37
129
|
|
|
38
130
|
def check(locator = nil, **options)
|
|
39
|
-
|
|
131
|
+
check_via_label_click(:checkbox, locator, checked: true, **options) { super }
|
|
40
132
|
end
|
|
41
133
|
|
|
42
134
|
def uncheck(locator = nil, **options)
|
|
43
|
-
|
|
135
|
+
check_via_label_click(:checkbox, locator, checked: false, **options) { super }
|
|
44
136
|
end
|
|
45
137
|
|
|
46
|
-
private def
|
|
47
|
-
unless
|
|
138
|
+
private def check_via_label_click(node_type, locator, checked:, allow_label_click: session_options.automatic_label_click, **options)
|
|
139
|
+
unless should_use_label_click?(allow_label_click, options)
|
|
48
140
|
return yield
|
|
49
141
|
end
|
|
50
142
|
|
|
51
|
-
|
|
143
|
+
handler = SelectableElementHandler.new(
|
|
144
|
+
node: self,
|
|
145
|
+
node_type: node_type,
|
|
146
|
+
locator: locator,
|
|
147
|
+
checked: checked,
|
|
148
|
+
)
|
|
149
|
+
return self if handler.set_checked_state_via_label?
|
|
52
150
|
|
|
53
151
|
yield
|
|
54
152
|
end
|
|
55
153
|
|
|
56
|
-
private def
|
|
57
|
-
return false if locator.nil?
|
|
154
|
+
private def should_use_label_click?(allow_label_click, options)
|
|
58
155
|
return false unless allow_label_click
|
|
59
156
|
return false unless driver.is_a?(Capybara::Playwright::Driver)
|
|
60
157
|
return false if Hash.try_convert(allow_label_click)
|
|
@@ -62,33 +159,6 @@ module Capybara
|
|
|
62
159
|
|
|
63
160
|
true
|
|
64
161
|
end
|
|
65
|
-
|
|
66
|
-
private def _playwright_try_get_by_label(locator, checked:)
|
|
67
|
-
handled = false
|
|
68
|
-
driver.with_playwright_page do |playwright_page|
|
|
69
|
-
begin
|
|
70
|
-
control_locator = playwright_page.get_by_label(locator.to_s)
|
|
71
|
-
control = control_locator
|
|
72
|
-
|
|
73
|
-
if control.evaluate('el => !!el.checked') != checked
|
|
74
|
-
label = control.evaluate_handle('(el) => (el.labels && el.labels[0]) || el.closest("label") || null')
|
|
75
|
-
next unless label.is_a?(::Playwright::ElementHandle)
|
|
76
|
-
|
|
77
|
-
label.click
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
next unless control.evaluate('el => !!el.checked') == checked
|
|
81
|
-
|
|
82
|
-
handled = true
|
|
83
|
-
rescue ::Playwright::Error
|
|
84
|
-
handled = false
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
handled
|
|
89
|
-
rescue StandardError
|
|
90
|
-
false
|
|
91
|
-
end
|
|
92
162
|
end
|
|
93
163
|
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7')
|
|
94
164
|
# Prepend to Node::Base instead of Node::Actions because Ruby < 3.1
|
|
@@ -128,6 +198,20 @@ module Capybara
|
|
|
128
198
|
end
|
|
129
199
|
::Playwright::ElementHandle.prepend(CapybaraObscuredPatch)
|
|
130
200
|
|
|
201
|
+
# ref: https://github.com/teamcapybara/capybara/pull/2424
|
|
202
|
+
module ElementDropPathCompatPatch
|
|
203
|
+
def drop(*args)
|
|
204
|
+
options = args.map { |arg| arg.respond_to?(:to_path) ? arg.to_path : arg }
|
|
205
|
+
synchronize { base.drop(*options) }
|
|
206
|
+
self
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
if Gem::Version.new(Capybara::VERSION) < Gem::Version.new('3.34.0')
|
|
210
|
+
# Older Capybara returns early for Pathname arguments in Element#drop,
|
|
211
|
+
# so the driver implementation never runs.
|
|
212
|
+
Node::Element.prepend(ElementDropPathCompatPatch)
|
|
213
|
+
end
|
|
214
|
+
|
|
131
215
|
module Playwright
|
|
132
216
|
# Selector and checking methods are derived from twapole/apparition
|
|
133
217
|
# Action methods (click, select_option, ...) uses playwright.
|
|
@@ -315,13 +399,31 @@ module Capybara
|
|
|
315
399
|
|
|
316
400
|
class TextInput < Settable
|
|
317
401
|
def set(value, **options)
|
|
402
|
+
case options[:clear]
|
|
403
|
+
when :backspace
|
|
404
|
+
@element.press('End', timeout: @timeout)
|
|
405
|
+
existing_text = @element.evaluate('el => el.value')
|
|
406
|
+
existing_text.length.times { @element.press('Backspace', timeout: @timeout) }
|
|
407
|
+
when :none
|
|
408
|
+
@element.press('End', timeout: @timeout)
|
|
409
|
+
when Array
|
|
410
|
+
@internal_logger.warn "options { clear: #{options[:clear]} } is ignored"
|
|
411
|
+
end
|
|
412
|
+
|
|
318
413
|
text = value.to_s
|
|
319
|
-
if text.end_with?("\n")
|
|
320
|
-
|
|
321
|
-
|
|
414
|
+
if press_enter = text.end_with?("\n")
|
|
415
|
+
text = text[0...-1]
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
if options[:clear] == :none
|
|
419
|
+
@element.type(text, timeout: @timeout)
|
|
322
420
|
else
|
|
323
421
|
@element.fill(text, timeout: @timeout)
|
|
324
422
|
end
|
|
423
|
+
|
|
424
|
+
if press_enter
|
|
425
|
+
@element.press('Enter', timeout: @timeout)
|
|
426
|
+
end
|
|
325
427
|
rescue ::Playwright::TimeoutError
|
|
326
428
|
raise if @element.editable?
|
|
327
429
|
|
|
@@ -782,8 +884,47 @@ module Capybara
|
|
|
782
884
|
end
|
|
783
885
|
end
|
|
784
886
|
|
|
887
|
+
ATTACH_FILE = <<~JAVASCRIPT
|
|
888
|
+
() => {
|
|
889
|
+
const input = document.createElement('INPUT');
|
|
890
|
+
input.type = 'file';
|
|
891
|
+
input.multiple = true;
|
|
892
|
+
input.style.display = 'none';
|
|
893
|
+
document.body.appendChild(input);
|
|
894
|
+
return input;
|
|
895
|
+
}
|
|
896
|
+
JAVASCRIPT
|
|
897
|
+
|
|
898
|
+
DROP_FILE = <<~JAVASCRIPT
|
|
899
|
+
(el, input) => {
|
|
900
|
+
const dt = new DataTransfer();
|
|
901
|
+
for (const file of input.files) { dt.items.add(file); }
|
|
902
|
+
input.remove();
|
|
903
|
+
el.dispatchEvent(new DragEvent('drop', {
|
|
904
|
+
cancelable: true, bubbles: true, dataTransfer: dt
|
|
905
|
+
}));
|
|
906
|
+
}
|
|
907
|
+
JAVASCRIPT
|
|
908
|
+
|
|
909
|
+
DROP_STRING = <<~JAVASCRIPT
|
|
910
|
+
(el, items) => {
|
|
911
|
+
const dt = new DataTransfer();
|
|
912
|
+
for (const item of items) { dt.items.add(item.data, item.type); }
|
|
913
|
+
el.dispatchEvent(new DragEvent('drop', {
|
|
914
|
+
cancelable: true, bubbles: true, dataTransfer: dt
|
|
915
|
+
}));
|
|
916
|
+
}
|
|
917
|
+
JAVASCRIPT
|
|
918
|
+
|
|
785
919
|
def drop(*args)
|
|
786
|
-
|
|
920
|
+
if args.first.is_a?(String) || args.first.is_a?(Pathname)
|
|
921
|
+
input = @page.evaluate_handle(ATTACH_FILE)
|
|
922
|
+
input.as_element.set_input_files(args.map(&:to_s))
|
|
923
|
+
@element.evaluate(DROP_FILE, arg: input)
|
|
924
|
+
else
|
|
925
|
+
items = args.flat_map { |arg| arg.map { |(type, data)| { type: type, data: data } } }
|
|
926
|
+
@element.evaluate(DROP_STRING, arg: items)
|
|
927
|
+
end
|
|
787
928
|
end
|
|
788
929
|
|
|
789
930
|
def scroll_by(x, y)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: capybara-playwright-driver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- YusukeIwaki
|
|
@@ -58,6 +58,7 @@ extensions: []
|
|
|
58
58
|
extra_rdoc_files: []
|
|
59
59
|
files:
|
|
60
60
|
- ".rspec"
|
|
61
|
+
- AGENTS.md
|
|
61
62
|
- CODE_OF_CONDUCT.md
|
|
62
63
|
- Gemfile
|
|
63
64
|
- LICENSE.txt
|