puppeteer-bidi 0.0.1.beta10 → 0.0.1
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 +44 -0
- data/API_COVERAGE.md +345 -0
- data/CLAUDE/porting_puppeteer.md +20 -0
- data/CLAUDE.md +2 -1
- data/DEVELOPMENT.md +14 -0
- data/README.md +47 -415
- data/development/generate_api_coverage.rb +411 -0
- data/development/puppeteer_revision.txt +1 -0
- data/lib/puppeteer/bidi/browser.rb +118 -22
- data/lib/puppeteer/bidi/browser_context.rb +185 -2
- data/lib/puppeteer/bidi/connection.rb +16 -5
- data/lib/puppeteer/bidi/cookie_utils.rb +192 -0
- data/lib/puppeteer/bidi/core/browsing_context.rb +83 -40
- data/lib/puppeteer/bidi/core/realm.rb +6 -0
- data/lib/puppeteer/bidi/core/request.rb +79 -35
- data/lib/puppeteer/bidi/core/user_context.rb +5 -3
- data/lib/puppeteer/bidi/element_handle.rb +200 -8
- data/lib/puppeteer/bidi/errors.rb +4 -0
- data/lib/puppeteer/bidi/frame.rb +115 -11
- data/lib/puppeteer/bidi/http_request.rb +577 -0
- data/lib/puppeteer/bidi/http_response.rb +161 -10
- data/lib/puppeteer/bidi/locator.rb +792 -0
- data/lib/puppeteer/bidi/page.rb +859 -7
- data/lib/puppeteer/bidi/query_handler.rb +1 -1
- data/lib/puppeteer/bidi/version.rb +1 -1
- data/lib/puppeteer/bidi.rb +39 -6
- data/sig/puppeteer/bidi/browser.rbs +53 -6
- data/sig/puppeteer/bidi/browser_context.rbs +36 -0
- data/sig/puppeteer/bidi/cookie_utils.rbs +64 -0
- data/sig/puppeteer/bidi/core/browsing_context.rbs +16 -6
- data/sig/puppeteer/bidi/core/request.rbs +14 -11
- data/sig/puppeteer/bidi/core/user_context.rbs +2 -2
- data/sig/puppeteer/bidi/element_handle.rbs +28 -0
- data/sig/puppeteer/bidi/errors.rbs +4 -0
- data/sig/puppeteer/bidi/frame.rbs +17 -0
- data/sig/puppeteer/bidi/http_request.rbs +162 -0
- data/sig/puppeteer/bidi/http_response.rbs +67 -8
- data/sig/puppeteer/bidi/locator.rbs +267 -0
- data/sig/puppeteer/bidi/page.rbs +170 -0
- data/sig/puppeteer/bidi.rbs +15 -3
- metadata +12 -1
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Puppeteer
|
|
5
|
+
module Bidi
|
|
6
|
+
# Visibility options for locators.
|
|
7
|
+
module VisibilityOption
|
|
8
|
+
HIDDEN = "hidden"
|
|
9
|
+
VISIBLE = "visible"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Events emitted by locators.
|
|
13
|
+
module LocatorEvent
|
|
14
|
+
ACTION = :action
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class RetryableError < StandardError; end
|
|
18
|
+
|
|
19
|
+
# Locators describe a strategy of locating objects and performing an action on them.
|
|
20
|
+
# Actions are retried when the element is not ready.
|
|
21
|
+
class Locator
|
|
22
|
+
RETRY_DELAY = 0.1
|
|
23
|
+
|
|
24
|
+
attr_reader :timeout #: Numeric
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@visibility = nil
|
|
28
|
+
@timeout = 30_000
|
|
29
|
+
@ensure_element_is_in_viewport = true
|
|
30
|
+
@wait_for_enabled = true
|
|
31
|
+
@wait_for_stable_bounding_box = true
|
|
32
|
+
@emitter = Core::EventEmitter.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Create a race between multiple locators.
|
|
36
|
+
# @rbs *locators: Array[Locator] -- Locators to race
|
|
37
|
+
# @rbs return: Locator -- Locator that resolves to the first match
|
|
38
|
+
def self.race(*locators)
|
|
39
|
+
locators = locators.first if locators.length == 1 && locators.first.is_a?(Array)
|
|
40
|
+
locators.each do |locator|
|
|
41
|
+
raise Error, "Unknown locator for race candidate" unless locator.is_a?(Locator)
|
|
42
|
+
end
|
|
43
|
+
RaceLocator.new(locators)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Register an event listener.
|
|
47
|
+
# @rbs event: Symbol | String -- Event name
|
|
48
|
+
# @rbs &block: (untyped) -> void -- Event handler
|
|
49
|
+
# @rbs return: Locator -- This locator
|
|
50
|
+
def on(event, &block)
|
|
51
|
+
@emitter.on(event, &block)
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Register a one-time event listener.
|
|
56
|
+
# @rbs event: Symbol | String -- Event name
|
|
57
|
+
# @rbs &block: (untyped) -> void -- Event handler
|
|
58
|
+
# @rbs return: Locator -- This locator
|
|
59
|
+
def once(event, &block)
|
|
60
|
+
@emitter.once(event, &block)
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Remove an event listener.
|
|
65
|
+
# @rbs event: Symbol | String -- Event name
|
|
66
|
+
# @rbs &block: ((untyped) -> void)? -- Handler to remove
|
|
67
|
+
# @rbs return: Locator -- This locator
|
|
68
|
+
def off(event, &block)
|
|
69
|
+
@emitter.off(event, &block)
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Clone the locator.
|
|
74
|
+
# @rbs return: Locator -- Cloned locator
|
|
75
|
+
def clone
|
|
76
|
+
_clone
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Set the total timeout for locator actions.
|
|
80
|
+
# Pass 0 to disable timeout.
|
|
81
|
+
# @rbs timeout: Numeric -- Timeout in ms
|
|
82
|
+
# @rbs return: Locator -- Cloned locator with timeout
|
|
83
|
+
def set_timeout(timeout)
|
|
84
|
+
locator = _clone
|
|
85
|
+
locator.send(:timeout=, timeout)
|
|
86
|
+
locator
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Set visibility checks for the locator.
|
|
90
|
+
# @rbs visibility: String? -- Visibility option ("hidden", "visible", or nil)
|
|
91
|
+
# @rbs return: Locator -- Cloned locator with visibility option
|
|
92
|
+
def set_visibility(visibility)
|
|
93
|
+
locator = _clone
|
|
94
|
+
locator.send(:visibility=, visibility&.to_s)
|
|
95
|
+
locator
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Set whether to wait for elements to become enabled.
|
|
99
|
+
# @rbs value: bool -- Whether to wait for enabled state
|
|
100
|
+
# @rbs return: Locator -- Cloned locator with enabled check
|
|
101
|
+
def set_wait_for_enabled(value)
|
|
102
|
+
locator = _clone
|
|
103
|
+
locator.send(:wait_for_enabled=, value)
|
|
104
|
+
locator
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Set whether to ensure elements are in the viewport.
|
|
108
|
+
# @rbs value: bool -- Whether to ensure viewport visibility
|
|
109
|
+
# @rbs return: Locator -- Cloned locator with viewport check
|
|
110
|
+
def set_ensure_element_is_in_the_viewport(value)
|
|
111
|
+
locator = _clone
|
|
112
|
+
locator.send(:ensure_element_is_in_viewport=, value)
|
|
113
|
+
locator
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Set whether to wait for a stable bounding box.
|
|
117
|
+
# @rbs value: bool -- Whether to wait for stable bounding box
|
|
118
|
+
# @rbs return: Locator -- Cloned locator with stable bounding box check
|
|
119
|
+
def set_wait_for_stable_bounding_box(value)
|
|
120
|
+
locator = _clone
|
|
121
|
+
locator.send(:wait_for_stable_bounding_box=, value)
|
|
122
|
+
locator
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Wait for the locator to produce a handle.
|
|
126
|
+
# @rbs return: JSHandle -- Handle to located value
|
|
127
|
+
def wait_handle
|
|
128
|
+
with_retry("Locator.wait_handle") do |deadline, remaining_ms|
|
|
129
|
+
_wait(timeout_ms: remaining_ms, deadline: deadline)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Wait for the locator to produce a JSON-serializable value.
|
|
134
|
+
# @rbs return: untyped -- JSON-serializable value
|
|
135
|
+
def wait
|
|
136
|
+
handle = wait_handle
|
|
137
|
+
begin
|
|
138
|
+
handle.json_value
|
|
139
|
+
ensure
|
|
140
|
+
handle.dispose if handle.respond_to?(:dispose)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Map the locator using a JavaScript mapper.
|
|
145
|
+
# @rbs mapper: String -- JavaScript mapper function
|
|
146
|
+
# @rbs &block: () -> String -- Optional block returning mapper string
|
|
147
|
+
# @rbs return: Locator -- Mapped locator
|
|
148
|
+
def map(mapper = nil, &block)
|
|
149
|
+
mapper = mapper || block&.call
|
|
150
|
+
raise ArgumentError, "mapper is required" unless mapper
|
|
151
|
+
|
|
152
|
+
map_handle(mapper)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Filter the locator using a JavaScript predicate.
|
|
156
|
+
# @rbs predicate: String -- JavaScript predicate function
|
|
157
|
+
# @rbs &block: () -> String -- Optional block returning predicate string
|
|
158
|
+
# @rbs return: Locator -- Filtered locator
|
|
159
|
+
def filter(predicate = nil, &block)
|
|
160
|
+
predicate = predicate || block&.call
|
|
161
|
+
raise ArgumentError, "predicate is required" unless predicate
|
|
162
|
+
|
|
163
|
+
FilteredLocator.new(_clone, predicate)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Click the located element.
|
|
167
|
+
# @rbs button: String -- Mouse button ('left', 'right', 'middle')
|
|
168
|
+
# @rbs count: Integer -- Number of clicks
|
|
169
|
+
# @rbs delay: Numeric? -- Delay between clicks in ms
|
|
170
|
+
# @rbs offset: Hash[Symbol, Numeric]? -- Click offset from element center
|
|
171
|
+
# @rbs return: void
|
|
172
|
+
def click(button: "left", count: 1, delay: nil, offset: nil)
|
|
173
|
+
perform_action("Locator.click", wait_for_enabled: true) do |handle|
|
|
174
|
+
handle.click(button: button, count: count, delay: delay, offset: offset)
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Fill the located element with the provided value.
|
|
180
|
+
# @rbs value: String -- Value to fill
|
|
181
|
+
# @rbs return: void
|
|
182
|
+
def fill(value)
|
|
183
|
+
perform_action("Locator.fill", wait_for_enabled: true) do |handle|
|
|
184
|
+
input_type = handle.evaluate(<<~JS)
|
|
185
|
+
(el) => {
|
|
186
|
+
if (el instanceof HTMLSelectElement) {
|
|
187
|
+
return "select";
|
|
188
|
+
}
|
|
189
|
+
if (el instanceof HTMLTextAreaElement) {
|
|
190
|
+
return "typeable-input";
|
|
191
|
+
}
|
|
192
|
+
if (el instanceof HTMLInputElement) {
|
|
193
|
+
if (
|
|
194
|
+
new Set([
|
|
195
|
+
"textarea",
|
|
196
|
+
"text",
|
|
197
|
+
"url",
|
|
198
|
+
"tel",
|
|
199
|
+
"search",
|
|
200
|
+
"password",
|
|
201
|
+
"number",
|
|
202
|
+
"email",
|
|
203
|
+
]).has(el.type)
|
|
204
|
+
) {
|
|
205
|
+
return "typeable-input";
|
|
206
|
+
}
|
|
207
|
+
return "other-input";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (el.isContentEditable) {
|
|
211
|
+
return "contenteditable";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return "unknown";
|
|
215
|
+
}
|
|
216
|
+
JS
|
|
217
|
+
|
|
218
|
+
case input_type
|
|
219
|
+
when "select"
|
|
220
|
+
handle.select(value)
|
|
221
|
+
when "contenteditable", "typeable-input"
|
|
222
|
+
text_to_type = handle.evaluate(<<~JS, value)
|
|
223
|
+
(input, newValue) => {
|
|
224
|
+
const currentValue = input.isContentEditable
|
|
225
|
+
? input.innerText
|
|
226
|
+
: input.value;
|
|
227
|
+
|
|
228
|
+
if (
|
|
229
|
+
newValue.length <= currentValue.length ||
|
|
230
|
+
!newValue.startsWith(input.value)
|
|
231
|
+
) {
|
|
232
|
+
if (input.isContentEditable) {
|
|
233
|
+
input.innerText = "";
|
|
234
|
+
} else {
|
|
235
|
+
input.value = "";
|
|
236
|
+
}
|
|
237
|
+
return newValue;
|
|
238
|
+
}
|
|
239
|
+
const originalValue = input.isContentEditable
|
|
240
|
+
? input.innerText
|
|
241
|
+
: input.value;
|
|
242
|
+
|
|
243
|
+
if (input.isContentEditable) {
|
|
244
|
+
input.innerText = "";
|
|
245
|
+
input.innerText = originalValue;
|
|
246
|
+
} else {
|
|
247
|
+
input.value = "";
|
|
248
|
+
input.value = originalValue;
|
|
249
|
+
}
|
|
250
|
+
return newValue.substring(originalValue.length);
|
|
251
|
+
}
|
|
252
|
+
JS
|
|
253
|
+
handle.type(text_to_type)
|
|
254
|
+
when "other-input"
|
|
255
|
+
handle.focus
|
|
256
|
+
handle.evaluate(<<~JS, value)
|
|
257
|
+
(input, newValue) => {
|
|
258
|
+
input.value = newValue;
|
|
259
|
+
input.dispatchEvent(new Event("input", {bubbles: true}));
|
|
260
|
+
input.dispatchEvent(new Event("change", {bubbles: true}));
|
|
261
|
+
}
|
|
262
|
+
JS
|
|
263
|
+
else
|
|
264
|
+
raise StandardError, "Element cannot be filled out."
|
|
265
|
+
end
|
|
266
|
+
nil
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Hover over the located element.
|
|
271
|
+
# @rbs return: void
|
|
272
|
+
def hover
|
|
273
|
+
perform_action("Locator.hover") do |handle|
|
|
274
|
+
handle.hover
|
|
275
|
+
nil
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Scroll the located element.
|
|
280
|
+
# @rbs scroll_top: Numeric? -- Scroll top offset
|
|
281
|
+
# @rbs scroll_left: Numeric? -- Scroll left offset
|
|
282
|
+
# @rbs return: void
|
|
283
|
+
def scroll(scroll_top: nil, scroll_left: nil)
|
|
284
|
+
perform_action("Locator.scroll") do |handle|
|
|
285
|
+
handle.evaluate(<<~JS, scroll_top, scroll_left)
|
|
286
|
+
(el, scrollTop, scrollLeft) => {
|
|
287
|
+
if (scrollTop !== undefined && scrollTop !== null) {
|
|
288
|
+
el.scrollTop = scrollTop;
|
|
289
|
+
}
|
|
290
|
+
if (scrollLeft !== undefined && scrollLeft !== null) {
|
|
291
|
+
el.scrollLeft = scrollLeft;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
JS
|
|
295
|
+
nil
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
protected
|
|
300
|
+
|
|
301
|
+
def copy_options(locator)
|
|
302
|
+
@timeout = locator.timeout
|
|
303
|
+
@visibility = locator.visibility
|
|
304
|
+
@wait_for_enabled = locator.wait_for_enabled?
|
|
305
|
+
@ensure_element_is_in_viewport = locator.ensure_element_is_in_viewport?
|
|
306
|
+
@wait_for_stable_bounding_box = locator.wait_for_stable_bounding_box?
|
|
307
|
+
self
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def visibility
|
|
311
|
+
@visibility
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def wait_for_enabled?
|
|
315
|
+
@wait_for_enabled
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def ensure_element_is_in_viewport?
|
|
319
|
+
@ensure_element_is_in_viewport
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def wait_for_stable_bounding_box?
|
|
323
|
+
@wait_for_stable_bounding_box
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def map_handle(mapper)
|
|
327
|
+
MappedLocator.new(_clone, mapper)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def _clone
|
|
331
|
+
raise NotImplementedError, "#{self.class}#_clone must be implemented"
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def _wait(timeout_ms:, deadline:)
|
|
335
|
+
raise NotImplementedError, "#{self.class}#_wait must be implemented"
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
private
|
|
339
|
+
|
|
340
|
+
attr_writer :timeout,
|
|
341
|
+
:visibility,
|
|
342
|
+
:wait_for_enabled,
|
|
343
|
+
:ensure_element_is_in_viewport,
|
|
344
|
+
:wait_for_stable_bounding_box
|
|
345
|
+
|
|
346
|
+
# @rbs cause: String -- Action name
|
|
347
|
+
# @rbs wait_for_enabled: bool -- Whether to wait for enabled
|
|
348
|
+
# @rbs &block: (ElementHandle) -> void -- Action to perform
|
|
349
|
+
# @rbs return: void
|
|
350
|
+
def perform_action(cause, wait_for_enabled: false, &block)
|
|
351
|
+
with_retry(cause) do |deadline, remaining_ms|
|
|
352
|
+
handle = _wait(timeout_ms: remaining_ms, deadline: deadline)
|
|
353
|
+
begin
|
|
354
|
+
ensure_element_is_in_viewport_if_needed(handle, deadline)
|
|
355
|
+
wait_for_stable_bounding_box_if_needed(handle, deadline)
|
|
356
|
+
wait_for_enabled_if_needed(handle, deadline) if wait_for_enabled
|
|
357
|
+
@emitter.emit(LocatorEvent::ACTION, nil)
|
|
358
|
+
block.call(handle)
|
|
359
|
+
rescue StandardError => error
|
|
360
|
+
handle.dispose if handle.respond_to?(:dispose)
|
|
361
|
+
raise error
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# @rbs cause: String -- Action name
|
|
367
|
+
# @rbs &block: (Numeric?, Numeric?) -> untyped -- Retry block
|
|
368
|
+
# @rbs return: untyped
|
|
369
|
+
def with_retry(cause, &block)
|
|
370
|
+
deadline = build_deadline
|
|
371
|
+
loop do
|
|
372
|
+
remaining_ms = deadline ? remaining_time_ms(deadline) : nil
|
|
373
|
+
raise_timeout if deadline && remaining_ms <= 0
|
|
374
|
+
begin
|
|
375
|
+
return block.call(deadline, remaining_ms)
|
|
376
|
+
rescue TimeoutError
|
|
377
|
+
raise
|
|
378
|
+
rescue StandardError
|
|
379
|
+
raise_timeout if deadline && remaining_time_ms(deadline) <= 0
|
|
380
|
+
sleep RETRY_DELAY
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def build_deadline
|
|
386
|
+
return nil if @timeout.nil? || @timeout == 0
|
|
387
|
+
|
|
388
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) + (@timeout / 1000.0)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def remaining_time_ms(deadline)
|
|
392
|
+
((deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)) * 1000.0).ceil
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def raise_timeout
|
|
396
|
+
raise TimeoutError, "Timed out after waiting #{@timeout}ms"
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# @rbs deadline: Numeric? -- Deadline timestamp
|
|
400
|
+
# @rbs &block: () -> boolish -- Condition block
|
|
401
|
+
# @rbs return: void
|
|
402
|
+
def wait_until(deadline, &block)
|
|
403
|
+
loop do
|
|
404
|
+
return if block.call
|
|
405
|
+
raise_timeout if deadline && remaining_time_ms(deadline) <= 0
|
|
406
|
+
sleep RETRY_DELAY
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def ensure_element_is_in_viewport_if_needed(handle, deadline)
|
|
411
|
+
return unless @ensure_element_is_in_viewport
|
|
412
|
+
|
|
413
|
+
wait_until(deadline) do
|
|
414
|
+
next true if handle.intersecting_viewport?(threshold: 0)
|
|
415
|
+
|
|
416
|
+
handle.scroll_into_view
|
|
417
|
+
false
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def wait_for_stable_bounding_box_if_needed(handle, deadline)
|
|
422
|
+
return unless @wait_for_stable_bounding_box
|
|
423
|
+
|
|
424
|
+
wait_until(deadline) do
|
|
425
|
+
rects = handle.evaluate(<<~JS)
|
|
426
|
+
(element) => {
|
|
427
|
+
return new Promise(resolve => {
|
|
428
|
+
window.requestAnimationFrame(() => {
|
|
429
|
+
const rect1 = element.getBoundingClientRect();
|
|
430
|
+
window.requestAnimationFrame(() => {
|
|
431
|
+
const rect2 = element.getBoundingClientRect();
|
|
432
|
+
resolve([
|
|
433
|
+
{
|
|
434
|
+
x: rect1.x,
|
|
435
|
+
y: rect1.y,
|
|
436
|
+
width: rect1.width,
|
|
437
|
+
height: rect1.height,
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
x: rect2.x,
|
|
441
|
+
y: rect2.y,
|
|
442
|
+
width: rect2.width,
|
|
443
|
+
height: rect2.height,
|
|
444
|
+
},
|
|
445
|
+
]);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
JS
|
|
451
|
+
|
|
452
|
+
rect1 = rects[0]
|
|
453
|
+
rect2 = rects[1]
|
|
454
|
+
rect1["x"] == rect2["x"] &&
|
|
455
|
+
rect1["y"] == rect2["y"] &&
|
|
456
|
+
rect1["width"] == rect2["width"] &&
|
|
457
|
+
rect1["height"] == rect2["height"]
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def wait_for_enabled_if_needed(handle, deadline)
|
|
462
|
+
return unless @wait_for_enabled
|
|
463
|
+
|
|
464
|
+
wait_until(deadline) do
|
|
465
|
+
handle.evaluate(<<~JS)
|
|
466
|
+
(element) => {
|
|
467
|
+
if (!(element instanceof HTMLElement)) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
const isNativeFormControl = [
|
|
471
|
+
"BUTTON",
|
|
472
|
+
"INPUT",
|
|
473
|
+
"SELECT",
|
|
474
|
+
"TEXTAREA",
|
|
475
|
+
"OPTION",
|
|
476
|
+
"OPTGROUP",
|
|
477
|
+
].includes(element.nodeName);
|
|
478
|
+
return !isNativeFormControl || !element.hasAttribute("disabled");
|
|
479
|
+
}
|
|
480
|
+
JS
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Locator implementation based on JavaScript functions.
|
|
486
|
+
class FunctionLocator < Locator
|
|
487
|
+
# @rbs page_or_frame: Page | Frame -- Page or frame to evaluate in
|
|
488
|
+
# @rbs function: String -- JavaScript function to evaluate
|
|
489
|
+
# @rbs return: Locator -- Function locator
|
|
490
|
+
def self.create(page_or_frame, function)
|
|
491
|
+
timeout = if page_or_frame.respond_to?(:default_timeout)
|
|
492
|
+
page_or_frame.default_timeout
|
|
493
|
+
else
|
|
494
|
+
page_or_frame.page.default_timeout
|
|
495
|
+
end
|
|
496
|
+
new(page_or_frame, function).set_timeout(timeout)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def initialize(page_or_frame, function)
|
|
500
|
+
super()
|
|
501
|
+
@page_or_frame = page_or_frame
|
|
502
|
+
@function = function
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
protected
|
|
506
|
+
|
|
507
|
+
def _clone
|
|
508
|
+
FunctionLocator.new(@page_or_frame, @function).copy_options(self)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def _wait(timeout_ms:, deadline:)
|
|
512
|
+
handle = @page_or_frame.evaluate_handle(@function)
|
|
513
|
+
begin
|
|
514
|
+
truthy = handle.evaluate("(value) => Boolean(value)")
|
|
515
|
+
raise RetryableError unless truthy
|
|
516
|
+
handle
|
|
517
|
+
rescue StandardError
|
|
518
|
+
handle.dispose
|
|
519
|
+
raise
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Abstract locator that delegates to another locator.
|
|
525
|
+
class DelegatedLocator < Locator
|
|
526
|
+
def initialize(delegate)
|
|
527
|
+
super()
|
|
528
|
+
@delegate = delegate
|
|
529
|
+
copy_options(@delegate)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def set_timeout(timeout)
|
|
533
|
+
locator = super
|
|
534
|
+
locator.delegate = @delegate.set_timeout(timeout)
|
|
535
|
+
locator
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def set_visibility(visibility)
|
|
539
|
+
locator = super
|
|
540
|
+
locator.delegate = @delegate.set_visibility(visibility)
|
|
541
|
+
locator
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def set_wait_for_enabled(value)
|
|
545
|
+
locator = super
|
|
546
|
+
locator.delegate = @delegate.set_wait_for_enabled(value)
|
|
547
|
+
locator
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def set_ensure_element_is_in_the_viewport(value)
|
|
551
|
+
locator = super
|
|
552
|
+
locator.delegate = @delegate.set_ensure_element_is_in_the_viewport(value)
|
|
553
|
+
locator
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def set_wait_for_stable_bounding_box(value)
|
|
557
|
+
locator = super
|
|
558
|
+
locator.delegate = @delegate.set_wait_for_stable_bounding_box(value)
|
|
559
|
+
locator
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
protected
|
|
563
|
+
|
|
564
|
+
attr_accessor :delegate
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# Locator that filters results using a predicate.
|
|
568
|
+
class FilteredLocator < DelegatedLocator
|
|
569
|
+
def initialize(delegate, predicate)
|
|
570
|
+
super(delegate)
|
|
571
|
+
@predicate = predicate
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
protected
|
|
575
|
+
|
|
576
|
+
def _clone
|
|
577
|
+
FilteredLocator.new(delegate.clone, @predicate).copy_options(self)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def _wait(timeout_ms:, deadline:)
|
|
581
|
+
handle = delegate.__send__(:_wait, timeout_ms: timeout_ms, deadline: deadline)
|
|
582
|
+
matched = handle.evaluate(@predicate)
|
|
583
|
+
raise RetryableError unless matched
|
|
584
|
+
|
|
585
|
+
handle
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Locator that maps results using a mapper.
|
|
590
|
+
class MappedLocator < DelegatedLocator
|
|
591
|
+
def initialize(delegate, mapper)
|
|
592
|
+
super(delegate)
|
|
593
|
+
@mapper = mapper
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
protected
|
|
597
|
+
|
|
598
|
+
def _clone
|
|
599
|
+
MappedLocator.new(delegate.clone, @mapper).copy_options(self)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def _wait(timeout_ms:, deadline:)
|
|
603
|
+
handle = delegate.__send__(:_wait, timeout_ms: timeout_ms, deadline: deadline)
|
|
604
|
+
handle.evaluate_handle(@mapper)
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
# Locator that queries nodes by selector or handle.
|
|
609
|
+
class NodeLocator < Locator
|
|
610
|
+
# @rbs page_or_frame: Page | Frame -- Page or frame to query
|
|
611
|
+
# @rbs selector: String -- Selector to query
|
|
612
|
+
# @rbs return: Locator -- Node locator
|
|
613
|
+
def self.create(page_or_frame, selector)
|
|
614
|
+
timeout = if page_or_frame.respond_to?(:default_timeout)
|
|
615
|
+
page_or_frame.default_timeout
|
|
616
|
+
else
|
|
617
|
+
page_or_frame.page.default_timeout
|
|
618
|
+
end
|
|
619
|
+
new(page_or_frame, selector).set_timeout(timeout)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# @rbs page_or_frame: Page | Frame -- Page or frame to query
|
|
623
|
+
# @rbs handle: ElementHandle -- Element handle to wrap
|
|
624
|
+
# @rbs return: Locator -- Node locator
|
|
625
|
+
def self.create_from_handle(page_or_frame, handle)
|
|
626
|
+
timeout = if page_or_frame.respond_to?(:default_timeout)
|
|
627
|
+
page_or_frame.default_timeout
|
|
628
|
+
else
|
|
629
|
+
page_or_frame.page.default_timeout
|
|
630
|
+
end
|
|
631
|
+
new(page_or_frame, handle).set_timeout(timeout)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def initialize(page_or_frame, selector_or_handle)
|
|
635
|
+
super()
|
|
636
|
+
@page_or_frame = page_or_frame
|
|
637
|
+
@selector_or_handle = selector_or_handle
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
protected
|
|
641
|
+
|
|
642
|
+
def _clone
|
|
643
|
+
NodeLocator.new(@page_or_frame, @selector_or_handle).copy_options(self)
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def _wait(timeout_ms:, deadline:)
|
|
647
|
+
handle = if @selector_or_handle.is_a?(String)
|
|
648
|
+
query_selector_with_pseudo_selectors(@selector_or_handle)
|
|
649
|
+
else
|
|
650
|
+
@selector_or_handle
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
raise RetryableError unless handle
|
|
654
|
+
raise RetryableError if handle.respond_to?(:disposed?) && handle.disposed?
|
|
655
|
+
|
|
656
|
+
if visibility
|
|
657
|
+
matches_visibility = case visibility
|
|
658
|
+
when VisibilityOption::VISIBLE
|
|
659
|
+
handle.visible?
|
|
660
|
+
when VisibilityOption::HIDDEN
|
|
661
|
+
handle.hidden?
|
|
662
|
+
else
|
|
663
|
+
true
|
|
664
|
+
end
|
|
665
|
+
unless matches_visibility
|
|
666
|
+
handle.dispose if @selector_or_handle.is_a?(String) && handle.respond_to?(:dispose)
|
|
667
|
+
raise RetryableError
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
handle
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def query_selector_with_pseudo_selectors(selector)
|
|
675
|
+
candidates = p_selector_candidates(selector)
|
|
676
|
+
candidates.each do |candidate|
|
|
677
|
+
handle = @page_or_frame.query_selector(candidate)
|
|
678
|
+
return handle if handle
|
|
679
|
+
end
|
|
680
|
+
nil
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def p_selector_candidates(selector)
|
|
684
|
+
return [selector] unless selector.include?("::-p-")
|
|
685
|
+
|
|
686
|
+
parts = split_selector_list(selector)
|
|
687
|
+
candidates = parts.filter_map do |part|
|
|
688
|
+
part = part.strip
|
|
689
|
+
match = part.match(/\A::\-p\-(text|xpath)\((.*)\)\z/)
|
|
690
|
+
next unless match
|
|
691
|
+
|
|
692
|
+
name = match[1]
|
|
693
|
+
value = unquote_selector_value(match[2])
|
|
694
|
+
prefix = name == "text" ? "text/" : "xpath/"
|
|
695
|
+
"#{prefix}#{value}"
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
candidates.empty? ? [selector] : candidates
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def split_selector_list(selector)
|
|
702
|
+
parts = []
|
|
703
|
+
current = +""
|
|
704
|
+
depth = 0
|
|
705
|
+
in_string = nil
|
|
706
|
+
escape = false
|
|
707
|
+
|
|
708
|
+
selector.each_char do |char|
|
|
709
|
+
if escape
|
|
710
|
+
current << char
|
|
711
|
+
escape = false
|
|
712
|
+
next
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
if in_string
|
|
716
|
+
if char == "\\"
|
|
717
|
+
escape = true
|
|
718
|
+
elsif char == in_string
|
|
719
|
+
in_string = nil
|
|
720
|
+
end
|
|
721
|
+
current << char
|
|
722
|
+
next
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
case char
|
|
726
|
+
when "'", '"'
|
|
727
|
+
in_string = char
|
|
728
|
+
when '('
|
|
729
|
+
depth += 1
|
|
730
|
+
when ')'
|
|
731
|
+
depth -= 1 if depth.positive?
|
|
732
|
+
when ','
|
|
733
|
+
if depth.zero?
|
|
734
|
+
parts << current
|
|
735
|
+
current = +""
|
|
736
|
+
next
|
|
737
|
+
end
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
current << char
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
parts << current unless current.empty?
|
|
744
|
+
parts
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def unquote_selector_value(value)
|
|
748
|
+
stripped = value.strip
|
|
749
|
+
return stripped if stripped.length < 2
|
|
750
|
+
|
|
751
|
+
quote = stripped[0]
|
|
752
|
+
return stripped unless quote == "'" || quote == '"'
|
|
753
|
+
return stripped unless stripped.end_with?(quote)
|
|
754
|
+
|
|
755
|
+
stripped[1..-2].gsub(/\\([\s\S])/, '\1')
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
# Locator that races multiple locators.
|
|
760
|
+
class RaceLocator < Locator
|
|
761
|
+
def initialize(locators)
|
|
762
|
+
super()
|
|
763
|
+
@locators = locators
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
protected
|
|
767
|
+
|
|
768
|
+
def _clone
|
|
769
|
+
RaceLocator.new(@locators.map(&:clone)).copy_options(self)
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def _wait(timeout_ms:, deadline:)
|
|
773
|
+
found = nil
|
|
774
|
+
|
|
775
|
+
wait_until(deadline) do
|
|
776
|
+
@locators.each do |locator|
|
|
777
|
+
begin
|
|
778
|
+
found = locator.__send__(:_wait, timeout_ms: timeout_ms, deadline: deadline)
|
|
779
|
+
break
|
|
780
|
+
rescue RetryableError
|
|
781
|
+
next
|
|
782
|
+
end
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
!found.nil?
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
found
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
end
|
|
792
|
+
end
|