capybara-playwright-driver 0.5.8 → 0.5.10
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 +115 -0
- data/Gemfile +1 -0
- data/lib/capybara/playwright/browser.rb +36 -6
- data/lib/capybara/playwright/node.rb +215 -41
- 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: a9766817ce50a7580acf4f72c062780da5a2363ea838dea3a8f17207c359c144
|
|
4
|
+
data.tar.gz: 5b48eb5b0eb4145088457e8b0c1a26c5843f80dc3e3d9acf120c095fd2d7691a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7b079c7c184643a16cfae94e7aaafd325460a3a13e3ece9cf453b127045a44a8ea008b29b65c326325ad3cbb7b3a8f092ff325267df512ac3f1645565ef6baa
|
|
7
|
+
data.tar.gz: 46290aabe5623d54094efa2cd30369261ca32525b99835e0f6e393ddc48de3e9d1e21011e64cd61dfd00531bf05818dab69525c1cb450f9f2d46679b9c19b0c5
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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.
|
|
108
|
+
|
|
109
|
+
## How to execute RSpec or Ruby
|
|
110
|
+
|
|
111
|
+
`ruby` uses macOS system ruby interpreter (typically 2.6 or older). rbenv should be used.
|
|
112
|
+
|
|
113
|
+
- Do not run the full `spec/capybara/playwright_spec.rb` or full RSpec suite locally; run broad coverage in GitHub Actions.
|
|
114
|
+
- To run a focused Playwright compatibility check locally, use an example filter, for example:
|
|
115
|
+
`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'
|
|
@@ -48,7 +48,7 @@ module Capybara
|
|
|
48
48
|
private def create_page(browser_context)
|
|
49
49
|
browser_context.new_page.tap do |page|
|
|
50
50
|
page.on('close', -> {
|
|
51
|
-
if @playwright_page
|
|
51
|
+
if @playwright_page&.guid == page.guid
|
|
52
52
|
@playwright_page = nil
|
|
53
53
|
end
|
|
54
54
|
})
|
|
@@ -89,10 +89,20 @@ 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
|
+
if firefox?
|
|
98
|
+
# ref: https://github.com/microsoft/playwright/issues/39738
|
|
94
99
|
@playwright_page.capybara_current_frame.evaluate('() => { location.reload(true) }')
|
|
95
|
-
|
|
100
|
+
else
|
|
101
|
+
assert_page_alive {
|
|
102
|
+
response = @playwright_page.reload
|
|
103
|
+
@playwright_page.capybara_set_last_response(response)
|
|
104
|
+
}
|
|
105
|
+
end
|
|
96
106
|
end
|
|
97
107
|
|
|
98
108
|
def find_xpath(query, **options)
|
|
@@ -255,7 +265,8 @@ module Capybara
|
|
|
255
265
|
when /Element is not attached to the DOM/,
|
|
256
266
|
/Execution context was destroyed, most likely because of a navigation/,
|
|
257
267
|
/Cannot find context with specified id/,
|
|
258
|
-
/Unable to adopt element handle from a different document
|
|
268
|
+
/Unable to adopt element handle from a different document/,
|
|
269
|
+
/maybe frame was detached/
|
|
259
270
|
# ignore error for retry
|
|
260
271
|
@internal_logger.warn(err.message)
|
|
261
272
|
else
|
|
@@ -318,16 +329,35 @@ module Capybara
|
|
|
318
329
|
|
|
319
330
|
def window_size(handle)
|
|
320
331
|
on_window(handle) do |page|
|
|
321
|
-
page
|
|
332
|
+
outer_window_size(page)
|
|
322
333
|
end
|
|
323
334
|
end
|
|
324
335
|
|
|
325
336
|
def resize_window_to(handle, width, height)
|
|
326
337
|
on_window(handle) do |page|
|
|
327
|
-
page.viewport_size =
|
|
338
|
+
page.viewport_size = viewport_size_for(page, width, height)
|
|
328
339
|
end
|
|
329
340
|
end
|
|
330
341
|
|
|
342
|
+
# Capybara follows Selenium's outer window size semantics.
|
|
343
|
+
private def outer_window_size(page)
|
|
344
|
+
page.evaluate('() => [window.outerWidth || window.innerWidth, window.outerHeight || window.innerHeight]')
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
private def viewport_size_for(page, width, height)
|
|
348
|
+
inset_width, inset_height = page.evaluate(<<~JAVASCRIPT)
|
|
349
|
+
() => [
|
|
350
|
+
Math.max(0, (window.outerWidth || window.innerWidth) - window.innerWidth),
|
|
351
|
+
Math.max(0, (window.outerHeight || window.innerHeight) - window.innerHeight)
|
|
352
|
+
]
|
|
353
|
+
JAVASCRIPT
|
|
354
|
+
|
|
355
|
+
{
|
|
356
|
+
width: [width - inset_width, 0].max,
|
|
357
|
+
height: [height - inset_height, 0].max
|
|
358
|
+
}
|
|
359
|
+
end
|
|
360
|
+
|
|
331
361
|
def maximize_window(handle)
|
|
332
362
|
@internal_logger.warn("maximize_window is not supported in Playwright driver")
|
|
333
363
|
# incomplete in Playwright
|
|
@@ -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.
|
|
@@ -174,7 +258,7 @@ module Capybara
|
|
|
174
258
|
end
|
|
175
259
|
|
|
176
260
|
private def capybara_default_wait_time
|
|
177
|
-
Capybara.default_max_wait_time * 1100 # with 10% buffer for allowing overhead.
|
|
261
|
+
Capybara.default_max_wait_time.to_f * 1100 # with 10% buffer for allowing overhead.
|
|
178
262
|
end
|
|
179
263
|
|
|
180
264
|
class NotActionableError < StandardError ; end
|
|
@@ -315,18 +399,69 @@ 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
|
-
|
|
414
|
+
if press_enter = text.end_with?("\r\n")
|
|
415
|
+
text = text[0...-2]
|
|
416
|
+
elsif press_enter = text.end_with?("\n")
|
|
417
|
+
text = text[0...-1]
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
set_text(text, append: options[:clear] == :none)
|
|
421
|
+
|
|
422
|
+
if press_enter
|
|
321
423
|
@element.press('Enter', timeout: @timeout)
|
|
322
|
-
else
|
|
323
|
-
@element.fill(text, timeout: @timeout)
|
|
324
424
|
end
|
|
325
425
|
rescue ::Playwright::TimeoutError
|
|
326
426
|
raise if @element.editable?
|
|
327
427
|
|
|
328
428
|
@internal_logger.info("Node#set: element is not editable. #{@element}")
|
|
329
429
|
end
|
|
430
|
+
|
|
431
|
+
private def set_text(text, append:)
|
|
432
|
+
# ElementHandle#type can refocus inherited contenteditable descendants and drop the input.
|
|
433
|
+
if @element.evaluate('el => el.isContentEditable') && !append
|
|
434
|
+
@element.fill(text, timeout: @timeout)
|
|
435
|
+
return
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
if text.include?("\t")
|
|
439
|
+
type_tab_separated_text(text, append: append)
|
|
440
|
+
return
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
grapheme_clusters = text.scan(/\X/)
|
|
444
|
+
fill_text = grapheme_clusters[0...-1].join
|
|
445
|
+
typed_text = grapheme_clusters[-1].to_s
|
|
446
|
+
|
|
447
|
+
if append
|
|
448
|
+
@element.type(fill_text, timeout: @timeout) unless fill_text.empty?
|
|
449
|
+
else
|
|
450
|
+
@element.fill(fill_text, timeout: @timeout)
|
|
451
|
+
end
|
|
452
|
+
@element.type(typed_text, timeout: @timeout) unless typed_text.empty?
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
private def type_tab_separated_text(text, append:)
|
|
456
|
+
head, *tail = text.split("\t", -1)
|
|
457
|
+
set_text(head, append: append)
|
|
458
|
+
keyboard = @element.owner_frame.page.keyboard
|
|
459
|
+
|
|
460
|
+
tail.each do |part|
|
|
461
|
+
keyboard.press('Tab')
|
|
462
|
+
keyboard.type(part) unless part.empty?
|
|
463
|
+
end
|
|
464
|
+
end
|
|
330
465
|
end
|
|
331
466
|
|
|
332
467
|
class FileUpload < Settable
|
|
@@ -782,8 +917,47 @@ module Capybara
|
|
|
782
917
|
end
|
|
783
918
|
end
|
|
784
919
|
|
|
920
|
+
ATTACH_FILE = <<~JAVASCRIPT
|
|
921
|
+
() => {
|
|
922
|
+
const input = document.createElement('INPUT');
|
|
923
|
+
input.type = 'file';
|
|
924
|
+
input.multiple = true;
|
|
925
|
+
input.style.display = 'none';
|
|
926
|
+
document.body.appendChild(input);
|
|
927
|
+
return input;
|
|
928
|
+
}
|
|
929
|
+
JAVASCRIPT
|
|
930
|
+
|
|
931
|
+
DROP_FILE = <<~JAVASCRIPT
|
|
932
|
+
(el, input) => {
|
|
933
|
+
const dt = new DataTransfer();
|
|
934
|
+
for (const file of input.files) { dt.items.add(file); }
|
|
935
|
+
input.remove();
|
|
936
|
+
el.dispatchEvent(new DragEvent('drop', {
|
|
937
|
+
cancelable: true, bubbles: true, dataTransfer: dt
|
|
938
|
+
}));
|
|
939
|
+
}
|
|
940
|
+
JAVASCRIPT
|
|
941
|
+
|
|
942
|
+
DROP_STRING = <<~JAVASCRIPT
|
|
943
|
+
(el, items) => {
|
|
944
|
+
const dt = new DataTransfer();
|
|
945
|
+
for (const item of items) { dt.items.add(item.data, item.type); }
|
|
946
|
+
el.dispatchEvent(new DragEvent('drop', {
|
|
947
|
+
cancelable: true, bubbles: true, dataTransfer: dt
|
|
948
|
+
}));
|
|
949
|
+
}
|
|
950
|
+
JAVASCRIPT
|
|
951
|
+
|
|
785
952
|
def drop(*args)
|
|
786
|
-
|
|
953
|
+
if args.first.is_a?(String) || args.first.is_a?(Pathname)
|
|
954
|
+
input = @page.evaluate_handle(ATTACH_FILE)
|
|
955
|
+
input.as_element.set_input_files(args.map(&:to_s))
|
|
956
|
+
@element.evaluate(DROP_FILE, arg: input)
|
|
957
|
+
else
|
|
958
|
+
items = args.flat_map { |arg| arg.map { |(type, data)| { type: type, data: data } } }
|
|
959
|
+
@element.evaluate(DROP_STRING, arg: items)
|
|
960
|
+
end
|
|
787
961
|
end
|
|
788
962
|
|
|
789
963
|
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.10
|
|
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
|