puppeteer-ruby 0.50.0.alpha5 → 0.50.0.alpha6
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 +1 -0
- data/CLAUDE/porting_puppeteer.md +30 -0
- data/CLAUDE/spec_migration_plans.md +8 -10
- data/README.md +3 -2
- data/docs/api_coverage.md +20 -20
- data/lib/puppeteer/element_handle.rb +5 -0
- data/lib/puppeteer/frame.rb +15 -0
- data/lib/puppeteer/js_handle.rb +22 -3
- data/lib/puppeteer/locators.rb +733 -0
- data/lib/puppeteer/p_query_handler.rb +367 -0
- data/lib/puppeteer/p_selector_parser.rb +241 -0
- data/lib/puppeteer/page.rb +15 -0
- data/lib/puppeteer/query_handler_manager.rb +14 -60
- data/lib/puppeteer/version.rb +1 -1
- data/lib/puppeteer.rb +3 -0
- data/sig/puppeteer/element_handle.rbs +3 -0
- data/sig/puppeteer/frame.rbs +7 -0
- data/sig/puppeteer/locators.rbs +222 -0
- data/sig/puppeteer/p_query_handler.rbs +73 -0
- data/sig/puppeteer/p_selector_parser.rbs +31 -0
- data/sig/puppeteer/page.rbs +7 -0
- metadata +8 -2
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module LocatorEvent
|
|
5
|
+
Action = 'action'
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Puppeteer::Locator
|
|
10
|
+
include Puppeteer::EventCallbackable
|
|
11
|
+
|
|
12
|
+
RETRY_DELAY_SECONDS = 0.1
|
|
13
|
+
|
|
14
|
+
class TimeoutController
|
|
15
|
+
def initialize(timeout)
|
|
16
|
+
@timeout = timeout
|
|
17
|
+
@deadline = nil
|
|
18
|
+
|
|
19
|
+
return if @timeout.nil? || @timeout == 0
|
|
20
|
+
|
|
21
|
+
@deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (@timeout / 1000.0)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_reader :timeout
|
|
25
|
+
|
|
26
|
+
def remaining_timeout
|
|
27
|
+
return 0 if @timeout == 0
|
|
28
|
+
return nil unless @deadline
|
|
29
|
+
|
|
30
|
+
remaining = (@deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)) * 1000.0
|
|
31
|
+
remaining.negative? ? 0 : remaining
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def exceeded?
|
|
35
|
+
return false unless @deadline
|
|
36
|
+
|
|
37
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) >= @deadline
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def check!(cause = nil)
|
|
41
|
+
return unless exceeded?
|
|
42
|
+
|
|
43
|
+
error = Puppeteer::TimeoutError.new("Timed out after waiting #{@timeout}ms")
|
|
44
|
+
error.cause = cause if cause
|
|
45
|
+
raise error
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialize
|
|
50
|
+
@visibility = nil
|
|
51
|
+
@timeout = 30_000
|
|
52
|
+
@ensure_element_is_in_viewport = true
|
|
53
|
+
@wait_for_enabled = true
|
|
54
|
+
@wait_for_stable_bounding_box = true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @rbs locators: Array[Puppeteer::Locator] -- Locator candidates
|
|
58
|
+
# @rbs return: Puppeteer::Locator -- Locator that races candidates
|
|
59
|
+
def self.race(locators)
|
|
60
|
+
proxy = locators.find { |locator| locator.is_a?(Puppeteer::ReactorRunner::Proxy) }
|
|
61
|
+
return Puppeteer::RaceLocator.create(locators) unless proxy
|
|
62
|
+
|
|
63
|
+
runner = proxy.instance_variable_get(:@runner)
|
|
64
|
+
locators.each do |locator|
|
|
65
|
+
next unless locator.is_a?(Puppeteer::ReactorRunner::Proxy)
|
|
66
|
+
|
|
67
|
+
locator_runner = locator.instance_variable_get(:@runner)
|
|
68
|
+
unless locator_runner == runner
|
|
69
|
+
raise ArgumentError.new('Locators for race must belong to the same runner')
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
runner.sync do
|
|
74
|
+
unwrapped = locators.map { |locator| runner.send(:unwrap, locator) }
|
|
75
|
+
runner.wrap(Puppeteer::RaceLocator.create(unwrapped))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @rbs input: String -- Input string to check
|
|
80
|
+
# @rbs return: bool -- Whether string looks like a JS function
|
|
81
|
+
def self.function_string?(input)
|
|
82
|
+
return false unless input.is_a?(String)
|
|
83
|
+
|
|
84
|
+
stripped = input.lstrip
|
|
85
|
+
return true if input.include?('=>')
|
|
86
|
+
return true if stripped.start_with?('async function')
|
|
87
|
+
|
|
88
|
+
stripped.start_with?('function')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @rbs return: Numeric -- Timeout in milliseconds
|
|
92
|
+
def timeout
|
|
93
|
+
@timeout
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @rbs timeout: Numeric -- Timeout in milliseconds
|
|
97
|
+
# @rbs return: Puppeteer::Locator -- Updated locator
|
|
98
|
+
def set_timeout(timeout)
|
|
99
|
+
locator = _clone
|
|
100
|
+
locator.instance_variable_set(:@timeout, timeout)
|
|
101
|
+
locator
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @rbs visibility: String? -- 'visible', 'hidden', or nil
|
|
105
|
+
# @rbs return: Puppeteer::Locator -- Updated locator
|
|
106
|
+
def set_visibility(visibility)
|
|
107
|
+
locator = _clone
|
|
108
|
+
locator.instance_variable_set(:@visibility, visibility&.to_s)
|
|
109
|
+
locator
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @rbs value: bool -- Whether to wait for enabled state
|
|
113
|
+
# @rbs return: Puppeteer::Locator -- Updated locator
|
|
114
|
+
def set_wait_for_enabled(value)
|
|
115
|
+
locator = _clone
|
|
116
|
+
locator.instance_variable_set(:@wait_for_enabled, value)
|
|
117
|
+
locator
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @rbs value: bool -- Whether to ensure element is in viewport
|
|
121
|
+
# @rbs return: Puppeteer::Locator -- Updated locator
|
|
122
|
+
def set_ensure_element_is_in_the_viewport(value)
|
|
123
|
+
locator = _clone
|
|
124
|
+
locator.instance_variable_set(:@ensure_element_is_in_viewport, value)
|
|
125
|
+
locator
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @rbs value: bool -- Whether to wait for stable bounding box
|
|
129
|
+
# @rbs return: Puppeteer::Locator -- Updated locator
|
|
130
|
+
def set_wait_for_stable_bounding_box(value)
|
|
131
|
+
locator = _clone
|
|
132
|
+
locator.instance_variable_set(:@wait_for_stable_bounding_box, value)
|
|
133
|
+
locator
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @rbs locator: Puppeteer::Locator -- Locator to copy options from
|
|
137
|
+
# @rbs return: self -- Locator with copied options
|
|
138
|
+
def copy_options(locator)
|
|
139
|
+
@timeout = locator.timeout
|
|
140
|
+
@visibility = locator.instance_variable_get(:@visibility)
|
|
141
|
+
@wait_for_enabled = locator.instance_variable_get(:@wait_for_enabled)
|
|
142
|
+
@ensure_element_is_in_viewport = locator.instance_variable_get(:@ensure_element_is_in_viewport)
|
|
143
|
+
@wait_for_stable_bounding_box = locator.instance_variable_get(:@wait_for_stable_bounding_box)
|
|
144
|
+
self
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @rbs return: Puppeteer::Locator -- Cloned locator
|
|
148
|
+
def clone(freeze: nil)
|
|
149
|
+
_clone
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @rbs return: Puppeteer::JSHandle -- Handle for located value
|
|
153
|
+
def wait_handle
|
|
154
|
+
with_retry('Locator.waitHandle') do |options|
|
|
155
|
+
_wait(options)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @rbs return: untyped -- JSON-serializable value
|
|
160
|
+
def wait
|
|
161
|
+
handle = wait_handle
|
|
162
|
+
begin
|
|
163
|
+
handle.json_value
|
|
164
|
+
ensure
|
|
165
|
+
handle.dispose
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# @rbs mapper: String -- JS mapper function
|
|
170
|
+
# @rbs return: Puppeteer::Locator -- Mapped locator
|
|
171
|
+
def map(mapper)
|
|
172
|
+
Puppeteer::MappedLocator.new(_clone, lambda { |handle, _options|
|
|
173
|
+
handle.evaluate_handle(mapper)
|
|
174
|
+
})
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# @rbs predicate: String -- JS predicate function
|
|
178
|
+
# @rbs return: Puppeteer::Locator -- Filtered locator
|
|
179
|
+
def filter(predicate)
|
|
180
|
+
Puppeteer::FilteredLocator.new(_clone, lambda { |handle, options|
|
|
181
|
+
handle.frame.wait_for_function(predicate, args: [handle], timeout: @timeout)
|
|
182
|
+
true
|
|
183
|
+
})
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @rbs predicate: Proc -- Handle predicate
|
|
187
|
+
# @rbs return: Puppeteer::Locator -- Filtered locator
|
|
188
|
+
def filter_handle(predicate)
|
|
189
|
+
Puppeteer::FilteredLocator.new(_clone, predicate)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @rbs mapper: Proc -- Handle mapper
|
|
193
|
+
# @rbs return: Puppeteer::Locator -- Mapped locator
|
|
194
|
+
def map_handle(mapper)
|
|
195
|
+
Puppeteer::MappedLocator.new(_clone, mapper)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @rbs delay: Numeric? -- Delay between down and up (ms)
|
|
199
|
+
# @rbs button: String? -- Mouse button
|
|
200
|
+
# @rbs click_count: Integer? -- Deprecated click count
|
|
201
|
+
# @rbs count: Integer? -- Number of clicks
|
|
202
|
+
# @rbs offset: Hash[Symbol, Numeric]? -- Click offset
|
|
203
|
+
# @rbs return: void -- No return value
|
|
204
|
+
def click(delay: nil, button: nil, click_count: nil, count: nil, offset: nil)
|
|
205
|
+
perform_action('Locator.click',
|
|
206
|
+
conditions: [
|
|
207
|
+
method(:ensure_element_is_in_viewport_if_needed),
|
|
208
|
+
method(:wait_for_stable_bounding_box_if_needed),
|
|
209
|
+
method(:wait_for_enabled_if_needed),
|
|
210
|
+
]) do |handle, _options|
|
|
211
|
+
handle.click(delay: delay, button: button, click_count: click_count, count: count, offset: offset)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# @rbs value: String -- Value to fill
|
|
216
|
+
# @rbs return: void -- No return value
|
|
217
|
+
def fill(value)
|
|
218
|
+
perform_action('Locator.fill',
|
|
219
|
+
conditions: [
|
|
220
|
+
method(:ensure_element_is_in_viewport_if_needed),
|
|
221
|
+
method(:wait_for_stable_bounding_box_if_needed),
|
|
222
|
+
method(:wait_for_enabled_if_needed),
|
|
223
|
+
]) do |handle, _options|
|
|
224
|
+
fill_element(handle, value)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# @rbs return: void -- No return value
|
|
229
|
+
def hover
|
|
230
|
+
perform_action('Locator.hover',
|
|
231
|
+
conditions: [
|
|
232
|
+
method(:ensure_element_is_in_viewport_if_needed),
|
|
233
|
+
method(:wait_for_stable_bounding_box_if_needed),
|
|
234
|
+
]) do |handle, _options|
|
|
235
|
+
handle.hover
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# @rbs scroll_top: Numeric? -- Scroll top position
|
|
240
|
+
# @rbs scroll_left: Numeric? -- Scroll left position
|
|
241
|
+
# @rbs return: void -- No return value
|
|
242
|
+
def scroll(scroll_top: nil, scroll_left: nil)
|
|
243
|
+
perform_action('Locator.scroll',
|
|
244
|
+
conditions: [
|
|
245
|
+
method(:ensure_element_is_in_viewport_if_needed),
|
|
246
|
+
method(:wait_for_stable_bounding_box_if_needed),
|
|
247
|
+
]) do |handle, _options|
|
|
248
|
+
js = <<~JAVASCRIPT
|
|
249
|
+
(el, scrollTop, scrollLeft) => {
|
|
250
|
+
if (scrollTop !== undefined && scrollTop !== null) {
|
|
251
|
+
el.scrollTop = scrollTop;
|
|
252
|
+
}
|
|
253
|
+
if (scrollLeft !== undefined && scrollLeft !== null) {
|
|
254
|
+
el.scrollLeft = scrollLeft;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
JAVASCRIPT
|
|
258
|
+
handle.evaluate(js, scroll_top, scroll_left)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# @rbs event_name: String -- Event name
|
|
263
|
+
# @rbs block: Proc -- Event handler
|
|
264
|
+
# @rbs return: Puppeteer::Locator -- Locator for chaining
|
|
265
|
+
def on(event_name, &block)
|
|
266
|
+
add_event_listener(event_name, &block)
|
|
267
|
+
self
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# @rbs event_name: String -- Event name
|
|
271
|
+
# @rbs block: Proc -- Event handler
|
|
272
|
+
# @rbs return: Puppeteer::Locator -- Locator for chaining
|
|
273
|
+
def once(event_name, &block)
|
|
274
|
+
observe_first(event_name, &block)
|
|
275
|
+
self
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
protected def _clone
|
|
279
|
+
raise NotImplementedError
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
protected def _wait(_options)
|
|
283
|
+
raise NotImplementedError
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
private def perform_action(name, conditions:, &block)
|
|
287
|
+
with_retry(name) do |options|
|
|
288
|
+
handle = _wait(options)
|
|
289
|
+
begin
|
|
290
|
+
run_conditions(handle, options, conditions)
|
|
291
|
+
emit_event(Puppeteer::LocatorEvent::Action)
|
|
292
|
+
block.call(handle, options)
|
|
293
|
+
nil
|
|
294
|
+
rescue => err
|
|
295
|
+
begin
|
|
296
|
+
handle.dispose
|
|
297
|
+
rescue StandardError
|
|
298
|
+
# Ignore disposal errors after a failed action.
|
|
299
|
+
end
|
|
300
|
+
raise err
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
private def with_retry(_name, &block)
|
|
306
|
+
timeout_controller = TimeoutController.new(@timeout)
|
|
307
|
+
last_error = nil
|
|
308
|
+
|
|
309
|
+
loop do
|
|
310
|
+
timeout_controller.check!(last_error)
|
|
311
|
+
|
|
312
|
+
options = build_action_options(timeout_controller)
|
|
313
|
+
begin
|
|
314
|
+
remaining = options[:timeout]
|
|
315
|
+
if remaining && remaining > 0
|
|
316
|
+
return Puppeteer::AsyncUtils.async_timeout(remaining, -> { block.call(options) }).wait
|
|
317
|
+
end
|
|
318
|
+
return block.call(options)
|
|
319
|
+
rescue Async::TimeoutError
|
|
320
|
+
timeout_controller.check!(last_error)
|
|
321
|
+
rescue => err
|
|
322
|
+
last_error = err
|
|
323
|
+
timeout_controller.check!(last_error)
|
|
324
|
+
|
|
325
|
+
Puppeteer::AsyncUtils.sleep_seconds(RETRY_DELAY_SECONDS)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
private def build_action_options(timeout_controller)
|
|
331
|
+
{
|
|
332
|
+
timeout: timeout_controller.remaining_timeout,
|
|
333
|
+
timeout_controller: timeout_controller,
|
|
334
|
+
}
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
private def run_conditions(handle, options, conditions)
|
|
338
|
+
return if conditions.empty?
|
|
339
|
+
|
|
340
|
+
tasks = conditions.map do |condition|
|
|
341
|
+
proc { condition.call(handle, options) }
|
|
342
|
+
end
|
|
343
|
+
Puppeteer::AsyncUtils.await_promise_all(*tasks)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
private def wait_for_enabled_if_needed(handle, options)
|
|
347
|
+
return unless @wait_for_enabled
|
|
348
|
+
|
|
349
|
+
js = <<~JAVASCRIPT
|
|
350
|
+
element => {
|
|
351
|
+
if (!(element instanceof HTMLElement)) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
const isNativeFormControl = [
|
|
355
|
+
'BUTTON',
|
|
356
|
+
'INPUT',
|
|
357
|
+
'SELECT',
|
|
358
|
+
'TEXTAREA',
|
|
359
|
+
'OPTION',
|
|
360
|
+
'OPTGROUP',
|
|
361
|
+
].includes(element.nodeName);
|
|
362
|
+
return !isNativeFormControl || !element.hasAttribute('disabled');
|
|
363
|
+
}
|
|
364
|
+
JAVASCRIPT
|
|
365
|
+
|
|
366
|
+
result = handle.frame.wait_for_function(js, args: [handle], timeout: options[:timeout])
|
|
367
|
+
result.dispose
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
private def wait_for_stable_bounding_box_if_needed(handle, options)
|
|
371
|
+
return unless @wait_for_stable_bounding_box
|
|
372
|
+
|
|
373
|
+
js = <<~JAVASCRIPT
|
|
374
|
+
element => {
|
|
375
|
+
return new Promise(resolve => {
|
|
376
|
+
window.requestAnimationFrame(() => {
|
|
377
|
+
const rect1 = element.getBoundingClientRect();
|
|
378
|
+
window.requestAnimationFrame(() => {
|
|
379
|
+
const rect2 = element.getBoundingClientRect();
|
|
380
|
+
resolve([
|
|
381
|
+
{ x: rect1.x, y: rect1.y, width: rect1.width, height: rect1.height },
|
|
382
|
+
{ x: rect2.x, y: rect2.y, width: rect2.width, height: rect2.height },
|
|
383
|
+
]);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
JAVASCRIPT
|
|
389
|
+
|
|
390
|
+
loop do
|
|
391
|
+
rects = handle.evaluate(js)
|
|
392
|
+
rect1 = rects[0]
|
|
393
|
+
rect2 = rects[1]
|
|
394
|
+
if rect1 && rect2 &&
|
|
395
|
+
rect1['x'] == rect2['x'] &&
|
|
396
|
+
rect1['y'] == rect2['y'] &&
|
|
397
|
+
rect1['width'] == rect2['width'] &&
|
|
398
|
+
rect1['height'] == rect2['height']
|
|
399
|
+
|
|
400
|
+
return
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
options[:timeout_controller].check!
|
|
404
|
+
Puppeteer::AsyncUtils.sleep_seconds(RETRY_DELAY_SECONDS)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
private def ensure_element_is_in_viewport_if_needed(handle, options)
|
|
409
|
+
return unless @ensure_element_is_in_viewport
|
|
410
|
+
|
|
411
|
+
loop do
|
|
412
|
+
intersects = handle.intersecting_viewport?(threshold: 0)
|
|
413
|
+
return if intersects
|
|
414
|
+
|
|
415
|
+
handle.scroll_into_view_if_needed
|
|
416
|
+
|
|
417
|
+
intersects = handle.intersecting_viewport?(threshold: 0)
|
|
418
|
+
return if intersects
|
|
419
|
+
|
|
420
|
+
options[:timeout_controller].check!
|
|
421
|
+
Puppeteer::AsyncUtils.sleep_seconds(RETRY_DELAY_SECONDS)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
private def fill_element(handle, value)
|
|
426
|
+
input_type = handle.evaluate(<<~JAVASCRIPT)
|
|
427
|
+
el => {
|
|
428
|
+
if (el instanceof HTMLSelectElement) {
|
|
429
|
+
return 'select';
|
|
430
|
+
}
|
|
431
|
+
if (el instanceof HTMLTextAreaElement) {
|
|
432
|
+
return 'typeable-input';
|
|
433
|
+
}
|
|
434
|
+
if (el instanceof HTMLInputElement) {
|
|
435
|
+
if (
|
|
436
|
+
new Set([
|
|
437
|
+
'textarea',
|
|
438
|
+
'text',
|
|
439
|
+
'url',
|
|
440
|
+
'tel',
|
|
441
|
+
'search',
|
|
442
|
+
'password',
|
|
443
|
+
'number',
|
|
444
|
+
'email',
|
|
445
|
+
]).has(el.type)
|
|
446
|
+
) {
|
|
447
|
+
return 'typeable-input';
|
|
448
|
+
}
|
|
449
|
+
return 'other-input';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (el.isContentEditable) {
|
|
453
|
+
return 'contenteditable';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return 'unknown';
|
|
457
|
+
}
|
|
458
|
+
JAVASCRIPT
|
|
459
|
+
|
|
460
|
+
case input_type
|
|
461
|
+
when 'select'
|
|
462
|
+
handle.select(value)
|
|
463
|
+
when 'contenteditable', 'typeable-input'
|
|
464
|
+
text_to_type = handle.evaluate(<<~JAVASCRIPT, value)
|
|
465
|
+
(input, newValue) => {
|
|
466
|
+
const currentValue = input.isContentEditable
|
|
467
|
+
? input.innerText
|
|
468
|
+
: input.value;
|
|
469
|
+
|
|
470
|
+
if (
|
|
471
|
+
newValue.length <= currentValue.length ||
|
|
472
|
+
!newValue.startsWith(input.value)
|
|
473
|
+
) {
|
|
474
|
+
if (input.isContentEditable) {
|
|
475
|
+
input.innerText = '';
|
|
476
|
+
} else {
|
|
477
|
+
input.value = '';
|
|
478
|
+
}
|
|
479
|
+
return newValue;
|
|
480
|
+
}
|
|
481
|
+
const originalValue = input.isContentEditable
|
|
482
|
+
? input.innerText
|
|
483
|
+
: input.value;
|
|
484
|
+
|
|
485
|
+
if (input.isContentEditable) {
|
|
486
|
+
input.innerText = '';
|
|
487
|
+
input.innerText = originalValue;
|
|
488
|
+
} else {
|
|
489
|
+
input.value = '';
|
|
490
|
+
input.value = originalValue;
|
|
491
|
+
}
|
|
492
|
+
return newValue.substring(originalValue.length);
|
|
493
|
+
}
|
|
494
|
+
JAVASCRIPT
|
|
495
|
+
text_to_type = text_to_type.to_s
|
|
496
|
+
handle.type_text(text_to_type)
|
|
497
|
+
when 'other-input'
|
|
498
|
+
handle.focus
|
|
499
|
+
handle.evaluate(<<~JAVASCRIPT, value)
|
|
500
|
+
(input, newValue) => {
|
|
501
|
+
input.value = newValue;
|
|
502
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
503
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
504
|
+
}
|
|
505
|
+
JAVASCRIPT
|
|
506
|
+
else
|
|
507
|
+
raise Puppeteer::Error.new('Element cannot be filled out.')
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
class Puppeteer::FunctionLocator < Puppeteer::Locator
|
|
513
|
+
# @rbs page_or_frame: Puppeteer::Page | Puppeteer::Frame -- Page or frame
|
|
514
|
+
# @rbs func: String -- JS function to evaluate
|
|
515
|
+
# @rbs return: Puppeteer::Locator -- Function locator
|
|
516
|
+
def self.create(page_or_frame, func)
|
|
517
|
+
new(page_or_frame, func).set_timeout(default_timeout_for(page_or_frame))
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def initialize(page_or_frame, func)
|
|
521
|
+
super()
|
|
522
|
+
@page_or_frame = page_or_frame
|
|
523
|
+
@func = func
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
protected def _clone
|
|
527
|
+
self.class.new(@page_or_frame, @func).copy_options(self)
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
protected def _wait(options)
|
|
531
|
+
@page_or_frame.wait_for_function(@func, timeout: options[:timeout])
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def self.default_timeout_for(page_or_frame)
|
|
535
|
+
if page_or_frame.respond_to?(:default_timeout)
|
|
536
|
+
page_or_frame.default_timeout
|
|
537
|
+
else
|
|
538
|
+
page_or_frame.page.default_timeout
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
private_class_method :default_timeout_for
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
class Puppeteer::DelegatedLocator < Puppeteer::Locator
|
|
546
|
+
def initialize(delegate)
|
|
547
|
+
super()
|
|
548
|
+
@delegate = delegate
|
|
549
|
+
copy_options(@delegate)
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
protected def delegate
|
|
553
|
+
@delegate
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def set_timeout(timeout)
|
|
557
|
+
locator = super
|
|
558
|
+
locator.instance_variable_set(:@delegate, @delegate.set_timeout(timeout))
|
|
559
|
+
locator
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def set_visibility(visibility)
|
|
563
|
+
locator = super
|
|
564
|
+
locator.instance_variable_set(:@delegate, @delegate.set_visibility(visibility))
|
|
565
|
+
locator
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def set_wait_for_enabled(value)
|
|
569
|
+
locator = super
|
|
570
|
+
locator.instance_variable_set(:@delegate, @delegate.set_wait_for_enabled(value))
|
|
571
|
+
locator
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def set_ensure_element_is_in_the_viewport(value)
|
|
575
|
+
locator = super
|
|
576
|
+
locator.instance_variable_set(:@delegate, @delegate.set_ensure_element_is_in_the_viewport(value))
|
|
577
|
+
locator
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def set_wait_for_stable_bounding_box(value)
|
|
581
|
+
locator = super
|
|
582
|
+
locator.instance_variable_set(:@delegate, @delegate.set_wait_for_stable_bounding_box(value))
|
|
583
|
+
locator
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
protected def _clone
|
|
587
|
+
raise NotImplementedError
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
protected def _wait(_options)
|
|
591
|
+
raise NotImplementedError
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
class Puppeteer::FilteredLocator < Puppeteer::DelegatedLocator
|
|
596
|
+
def initialize(base, predicate)
|
|
597
|
+
super(base)
|
|
598
|
+
@predicate = predicate
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
protected def _clone
|
|
602
|
+
self.class.new(delegate.clone, @predicate).copy_options(self)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
protected def _wait(options)
|
|
606
|
+
handle = delegate.send(:_wait, options)
|
|
607
|
+
result = @predicate.call(handle, options)
|
|
608
|
+
return handle if result
|
|
609
|
+
|
|
610
|
+
raise Puppeteer::Error.new('Locator predicate did not match')
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
class Puppeteer::MappedLocator < Puppeteer::DelegatedLocator
|
|
615
|
+
def initialize(base, mapper)
|
|
616
|
+
super(base)
|
|
617
|
+
@mapper = mapper
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
protected def _clone
|
|
621
|
+
self.class.new(delegate.clone, @mapper).copy_options(self)
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
protected def _wait(options)
|
|
625
|
+
handle = delegate.send(:_wait, options)
|
|
626
|
+
@mapper.call(handle, options)
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
class Puppeteer::NodeLocator < Puppeteer::Locator
|
|
631
|
+
# @rbs page_or_frame: Puppeteer::Page | Puppeteer::Frame -- Page or frame
|
|
632
|
+
# @rbs selector: String -- Selector
|
|
633
|
+
# @rbs return: Puppeteer::Locator -- Node locator
|
|
634
|
+
def self.create(page_or_frame, selector)
|
|
635
|
+
new(page_or_frame, selector).set_timeout(default_timeout_for(page_or_frame))
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# @rbs page_or_frame: Puppeteer::Page | Puppeteer::Frame -- Page or frame
|
|
639
|
+
# @rbs handle: Puppeteer::ElementHandle -- Element handle
|
|
640
|
+
# @rbs return: Puppeteer::Locator -- Node locator
|
|
641
|
+
def self.create_from_handle(page_or_frame, handle)
|
|
642
|
+
new(page_or_frame, handle).set_timeout(default_timeout_for(page_or_frame))
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def initialize(page_or_frame, selector_or_handle)
|
|
646
|
+
super()
|
|
647
|
+
@page_or_frame = page_or_frame
|
|
648
|
+
@selector_or_handle = selector_or_handle
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
protected def _clone
|
|
652
|
+
self.class.new(@page_or_frame, @selector_or_handle).copy_options(self)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
protected def _wait(options)
|
|
656
|
+
handle = if @selector_or_handle.is_a?(String)
|
|
657
|
+
selector = @selector_or_handle
|
|
658
|
+
@page_or_frame.wait_for_selector(selector, visible: false, timeout: options[:timeout])
|
|
659
|
+
else
|
|
660
|
+
@selector_or_handle
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
raise Puppeteer::Error.new('No element found for selector') unless handle
|
|
664
|
+
|
|
665
|
+
wait_for_visibility_if_needed(handle, options)
|
|
666
|
+
handle
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
private def wait_for_visibility_if_needed(handle, options)
|
|
670
|
+
return unless @visibility
|
|
671
|
+
|
|
672
|
+
loop do
|
|
673
|
+
case @visibility
|
|
674
|
+
when 'visible'
|
|
675
|
+
return if handle.visible?
|
|
676
|
+
when 'hidden'
|
|
677
|
+
return if handle.hidden?
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
options[:timeout_controller].check!
|
|
681
|
+
Puppeteer::AsyncUtils.sleep_seconds(RETRY_DELAY_SECONDS)
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def self.default_timeout_for(page_or_frame)
|
|
686
|
+
if page_or_frame.respond_to?(:default_timeout)
|
|
687
|
+
page_or_frame.default_timeout
|
|
688
|
+
else
|
|
689
|
+
page_or_frame.page.default_timeout
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
private_class_method :default_timeout_for
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
class Puppeteer::RaceLocator < Puppeteer::Locator
|
|
697
|
+
def self.create(locators)
|
|
698
|
+
array = check_locator_array(locators)
|
|
699
|
+
new(array)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def initialize(locators)
|
|
703
|
+
super()
|
|
704
|
+
@locators = locators
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
protected def _clone
|
|
708
|
+
self.class.new(@locators.map(&:clone)).copy_options(self)
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
protected def _wait(options)
|
|
712
|
+
tasks = @locators.map do |locator|
|
|
713
|
+
proc { locator.send(:_wait, options) }
|
|
714
|
+
end
|
|
715
|
+
Puppeteer::AsyncUtils.await_promise_race(*tasks)
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def self.check_locator_array(locators)
|
|
719
|
+
unless locators.is_a?(Array)
|
|
720
|
+
raise ArgumentError.new('Unknown locator for race candidate')
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
locators.each do |locator|
|
|
724
|
+
unless locator.is_a?(Puppeteer::Locator)
|
|
725
|
+
raise ArgumentError.new('Unknown locator for race candidate')
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
locators
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
private_class_method :check_locator_array
|
|
733
|
+
end
|