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
|
@@ -6,6 +6,26 @@ module Puppeteer
|
|
|
6
6
|
module Core
|
|
7
7
|
# Request represents a network request
|
|
8
8
|
class Request < EventEmitter
|
|
9
|
+
DESTINATION_RESOURCE_TYPES = {
|
|
10
|
+
"style" => "stylesheet",
|
|
11
|
+
"document" => "document",
|
|
12
|
+
"script" => "script",
|
|
13
|
+
"image" => "image",
|
|
14
|
+
"font" => "font",
|
|
15
|
+
"audio" => "media",
|
|
16
|
+
"video" => "media",
|
|
17
|
+
"track" => "texttrack",
|
|
18
|
+
"manifest" => "manifest",
|
|
19
|
+
"iframe" => "document",
|
|
20
|
+
"frame" => "document",
|
|
21
|
+
"fetch" => "fetch",
|
|
22
|
+
"object" => "other",
|
|
23
|
+
"embed" => "other",
|
|
24
|
+
"worker" => "other",
|
|
25
|
+
"sharedworker" => "other",
|
|
26
|
+
"serviceworker" => "other",
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
9
29
|
include Disposable::DisposableMixin
|
|
10
30
|
|
|
11
31
|
# Create a request instance from a beforeRequestSent event
|
|
@@ -27,8 +47,8 @@ module Puppeteer
|
|
|
27
47
|
@error = nil
|
|
28
48
|
@redirect = nil
|
|
29
49
|
@response = nil
|
|
30
|
-
@
|
|
31
|
-
@
|
|
50
|
+
@response_content_task = nil
|
|
51
|
+
@request_body_task = nil
|
|
32
52
|
@disposables = Disposable::DisposableStack.new
|
|
33
53
|
end
|
|
34
54
|
|
|
@@ -101,7 +121,13 @@ module Puppeteer
|
|
|
101
121
|
# Get resource type (non-standard)
|
|
102
122
|
# @rbs return: String? -- Resource type
|
|
103
123
|
def resource_type
|
|
104
|
-
@event.dig('request', 'goog:resourceType')
|
|
124
|
+
resource_type = @event.dig('request', 'goog:resourceType')
|
|
125
|
+
return resource_type if resource_type
|
|
126
|
+
|
|
127
|
+
destination = @event.dig('request', 'destination')
|
|
128
|
+
return nil unless destination
|
|
129
|
+
|
|
130
|
+
DESTINATION_RESOURCE_TYPES.fetch(destination, destination)
|
|
105
131
|
end
|
|
106
132
|
|
|
107
133
|
# Get POST data (non-standard)
|
|
@@ -128,7 +154,7 @@ module Puppeteer
|
|
|
128
154
|
# @rbs headers: Array[Hash[String, untyped]]? -- Modified headers
|
|
129
155
|
# @rbs cookies: Array[Hash[String, untyped]]? -- Modified cookies
|
|
130
156
|
# @rbs body: Hash[String, untyped]? -- Modified body
|
|
131
|
-
# @rbs return: untyped
|
|
157
|
+
# @rbs return: Async::Task[untyped]
|
|
132
158
|
def continue_request(url: nil, method: nil, headers: nil, cookies: nil, body: nil)
|
|
133
159
|
params = { request: id }
|
|
134
160
|
params[:url] = url if url
|
|
@@ -137,12 +163,13 @@ module Puppeteer
|
|
|
137
163
|
params[:cookies] = cookies if cookies
|
|
138
164
|
params[:body] = body if body
|
|
139
165
|
|
|
140
|
-
session.
|
|
166
|
+
session.async_send_command('network.continueRequest', params)
|
|
141
167
|
end
|
|
142
168
|
|
|
143
169
|
# Fail the request
|
|
170
|
+
# @rbs return: Async::Task[untyped]
|
|
144
171
|
def fail_request
|
|
145
|
-
session.
|
|
172
|
+
session.async_send_command('network.failRequest', { request: id })
|
|
146
173
|
end
|
|
147
174
|
|
|
148
175
|
# Provide a response for the request
|
|
@@ -150,7 +177,7 @@ module Puppeteer
|
|
|
150
177
|
# @rbs reason_phrase: String? -- Response reason phrase
|
|
151
178
|
# @rbs headers: Array[Hash[String, untyped]]? -- Response headers
|
|
152
179
|
# @rbs body: Hash[String, untyped]? -- Response body
|
|
153
|
-
# @rbs return: untyped
|
|
180
|
+
# @rbs return: Async::Task[untyped]
|
|
154
181
|
def provide_response(status_code: nil, reason_phrase: nil, headers: nil, body: nil)
|
|
155
182
|
params = { request: id }
|
|
156
183
|
params[:statusCode] = status_code if status_code
|
|
@@ -158,20 +185,24 @@ module Puppeteer
|
|
|
158
185
|
params[:headers] = headers if headers
|
|
159
186
|
params[:body] = body if body
|
|
160
187
|
|
|
161
|
-
session.
|
|
188
|
+
session.async_send_command('network.provideResponse', params)
|
|
162
189
|
end
|
|
163
190
|
|
|
164
191
|
# Fetch POST data for the request
|
|
165
|
-
# @rbs return: String? -- POST data
|
|
192
|
+
# @rbs return: Async::Task[String?] -- POST data
|
|
166
193
|
def fetch_post_data
|
|
167
|
-
|
|
168
|
-
|
|
194
|
+
unless has_post_data?
|
|
195
|
+
return Async do
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
return @request_body_task if @request_body_task
|
|
169
200
|
|
|
170
|
-
@
|
|
171
|
-
result = session.
|
|
201
|
+
@request_body_task = Async do
|
|
202
|
+
result = session.async_send_command('network.getData', {
|
|
172
203
|
dataType: 'request',
|
|
173
204
|
request: id
|
|
174
|
-
})
|
|
205
|
+
}).wait
|
|
175
206
|
|
|
176
207
|
bytes = result['bytes']
|
|
177
208
|
if bytes['type'] == 'string'
|
|
@@ -183,34 +214,36 @@ module Puppeteer
|
|
|
183
214
|
end
|
|
184
215
|
|
|
185
216
|
# Get response content
|
|
186
|
-
# @rbs return: String -- Response content as binary string
|
|
217
|
+
# @rbs return: Async::Task[String] -- Response content as binary string
|
|
187
218
|
def response_content
|
|
188
|
-
return @
|
|
189
|
-
|
|
190
|
-
@
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
219
|
+
return @response_content_task if @response_content_task
|
|
220
|
+
|
|
221
|
+
@response_content_task = Async do
|
|
222
|
+
begin
|
|
223
|
+
result = session.async_send_command('network.getData', {
|
|
224
|
+
dataType: 'response',
|
|
225
|
+
request: id
|
|
226
|
+
}).wait
|
|
227
|
+
|
|
228
|
+
bytes = result['bytes']
|
|
229
|
+
if bytes['type'] == 'base64'
|
|
230
|
+
[bytes['value']].pack('m0')
|
|
231
|
+
else
|
|
232
|
+
bytes['value']
|
|
233
|
+
end
|
|
234
|
+
rescue => e
|
|
235
|
+
if e.message.include?('No resource with given identifier found')
|
|
236
|
+
raise 'Could not load response body for this request. This might happen if the request is a preflight request.'
|
|
237
|
+
end
|
|
238
|
+
raise
|
|
205
239
|
end
|
|
206
|
-
raise
|
|
207
240
|
end
|
|
208
241
|
end
|
|
209
242
|
|
|
210
243
|
# Continue with authentication
|
|
211
244
|
# @rbs action: String -- 'provideCredentials', 'default', or 'cancel'
|
|
212
245
|
# @rbs credentials: Hash[String, untyped]? -- Credentials hash with username and password
|
|
213
|
-
# @rbs return: untyped
|
|
246
|
+
# @rbs return: Async::Task[untyped]
|
|
214
247
|
def continue_with_auth(action:, credentials: nil)
|
|
215
248
|
params = {
|
|
216
249
|
request: id,
|
|
@@ -218,7 +251,7 @@ module Puppeteer
|
|
|
218
251
|
}
|
|
219
252
|
params[:credentials] = credentials if action == 'provideCredentials'
|
|
220
253
|
|
|
221
|
-
session.
|
|
254
|
+
session.async_send_command('network.continueWithAuth', params)
|
|
222
255
|
end
|
|
223
256
|
|
|
224
257
|
protected
|
|
@@ -283,6 +316,17 @@ module Puppeteer
|
|
|
283
316
|
dispose
|
|
284
317
|
end
|
|
285
318
|
|
|
319
|
+
# Listen for response started
|
|
320
|
+
session.on('network.responseStarted') do |event|
|
|
321
|
+
next unless event['context'] == @browsing_context.id
|
|
322
|
+
next unless event.dig('request', 'request') == id
|
|
323
|
+
next unless event['redirectCount'] == @event['redirectCount']
|
|
324
|
+
|
|
325
|
+
@response = event['response']
|
|
326
|
+
@event['request']['timings'] = event.dig('request', 'timings')
|
|
327
|
+
emit(:response, @response)
|
|
328
|
+
end
|
|
329
|
+
|
|
286
330
|
# Listen for response completed
|
|
287
331
|
session.on('network.responseCompleted') do |event|
|
|
288
332
|
next unless event['context'] == @browsing_context.id
|
|
@@ -97,7 +97,7 @@ module Puppeteer
|
|
|
97
97
|
|
|
98
98
|
# Get cookies for this user context
|
|
99
99
|
# @rbs source_origin: String? -- Source origin
|
|
100
|
-
# @rbs return: Array[Hash[String, untyped]] -- Cookies
|
|
100
|
+
# @rbs return: Async::Task[Array[Hash[String, untyped]]] -- Cookies
|
|
101
101
|
def get_cookies(**options)
|
|
102
102
|
raise UserContextClosedError, @reason if closed?
|
|
103
103
|
|
|
@@ -109,8 +109,10 @@ module Puppeteer
|
|
|
109
109
|
}
|
|
110
110
|
params[:partition][:sourceOrigin] = source_origin if source_origin
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
Async do
|
|
113
|
+
result = session.async_send_command('storage.getCookies', params).wait
|
|
114
|
+
result['cookies']
|
|
115
|
+
end
|
|
114
116
|
end
|
|
115
117
|
|
|
116
118
|
# Set a cookie in this user context
|
|
@@ -240,6 +240,65 @@ module Puppeteer
|
|
|
240
240
|
evaluate('element => element.focus()')
|
|
241
241
|
end
|
|
242
242
|
|
|
243
|
+
# Select options on a <select> element
|
|
244
|
+
# Triggers 'change' and 'input' events once all options are selected.
|
|
245
|
+
# If not a select element, throws an error.
|
|
246
|
+
# @rbs *values: String -- Option values to select
|
|
247
|
+
# @rbs return: Array[String] -- Actually selected option values
|
|
248
|
+
def select(*values)
|
|
249
|
+
assert_not_disposed
|
|
250
|
+
|
|
251
|
+
# Validate all values are strings
|
|
252
|
+
values.each_with_index do |value, index|
|
|
253
|
+
unless value.is_a?(String)
|
|
254
|
+
raise ArgumentError, "Values must be strings. Found value of type #{value.class} at index #{index}"
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Use isolated realm to avoid user modifications to global objects (like Event).
|
|
259
|
+
realm = frame.isolated_realm
|
|
260
|
+
adopted_element = realm.adopt_handle(self)
|
|
261
|
+
|
|
262
|
+
begin
|
|
263
|
+
adopted_element.evaluate(<<~JS, values)
|
|
264
|
+
(element, vals) => {
|
|
265
|
+
const values = new Set(vals);
|
|
266
|
+
if (!(element instanceof HTMLSelectElement)) {
|
|
267
|
+
throw new Error('Element is not a <select> element.');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const selectedValues = new Set();
|
|
271
|
+
if (!element.multiple) {
|
|
272
|
+
for (const option of element.options) {
|
|
273
|
+
option.selected = false;
|
|
274
|
+
}
|
|
275
|
+
for (const option of element.options) {
|
|
276
|
+
if (values.has(option.value)) {
|
|
277
|
+
option.selected = true;
|
|
278
|
+
selectedValues.add(option.value);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
for (const option of element.options) {
|
|
284
|
+
option.selected = values.has(option.value);
|
|
285
|
+
if (option.selected) {
|
|
286
|
+
selectedValues.add(option.value);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
element.dispatchEvent(new Event('input', {bubbles: true}));
|
|
292
|
+
element.dispatchEvent(new Event('change', {bubbles: true}));
|
|
293
|
+
|
|
294
|
+
return Array.from(selectedValues.values());
|
|
295
|
+
}
|
|
296
|
+
JS
|
|
297
|
+
ensure
|
|
298
|
+
adopted_element&.dispose
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
243
302
|
# Hover over the element
|
|
244
303
|
# Scrolls element into view if needed and moves mouse to element center
|
|
245
304
|
# @rbs return: void
|
|
@@ -299,6 +358,70 @@ module Puppeteer
|
|
|
299
358
|
evaluate('element => element.scrollIntoView({block: "center", inline: "center", behavior: "instant"})')
|
|
300
359
|
end
|
|
301
360
|
|
|
361
|
+
# Create a locator based on this element handle.
|
|
362
|
+
# @rbs return: Locator -- Locator instance
|
|
363
|
+
def as_locator
|
|
364
|
+
assert_not_disposed
|
|
365
|
+
|
|
366
|
+
NodeLocator.create_from_handle(frame, self)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Take a screenshot of the element.
|
|
370
|
+
# Following Puppeteer's implementation: ElementHandle.screenshot
|
|
371
|
+
# @rbs path: String? -- File path to save screenshot
|
|
372
|
+
# @rbs type: String -- Image type ('png' or 'jpeg')
|
|
373
|
+
# @rbs clip: Hash[Symbol, Numeric]? -- Clip region relative to element
|
|
374
|
+
# @rbs scroll_into_view: bool -- Scroll element into view before screenshot
|
|
375
|
+
# @rbs return: String -- Base64-encoded image data
|
|
376
|
+
def screenshot(path: nil, type: 'png', clip: nil, scroll_into_view: true)
|
|
377
|
+
assert_not_disposed
|
|
378
|
+
|
|
379
|
+
page = frame.page
|
|
380
|
+
|
|
381
|
+
# Scroll into view if needed
|
|
382
|
+
scroll_into_view_if_needed if scroll_into_view
|
|
383
|
+
|
|
384
|
+
# Get element's bounding box - must not be empty
|
|
385
|
+
# Note: bounding_box returns viewport-relative coordinates from getBoundingClientRect()
|
|
386
|
+
element_box = non_empty_visible_bounding_box
|
|
387
|
+
|
|
388
|
+
# Get page scroll offset from visualViewport to convert to document coordinates
|
|
389
|
+
scroll_offset = evaluate(<<~JS)
|
|
390
|
+
() => {
|
|
391
|
+
if (!window.visualViewport) {
|
|
392
|
+
throw new Error('window.visualViewport is not supported.');
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
pageLeft: window.visualViewport.pageLeft,
|
|
396
|
+
pageTop: window.visualViewport.pageTop
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
JS
|
|
400
|
+
|
|
401
|
+
# Build element clip in document coordinates (viewport coords + scroll offset)
|
|
402
|
+
# Following Puppeteer's implementation: elementClip.x += pageLeft; elementClip.y += pageTop
|
|
403
|
+
element_clip = {
|
|
404
|
+
x: element_box.x + scroll_offset['pageLeft'],
|
|
405
|
+
y: element_box.y + scroll_offset['pageTop'],
|
|
406
|
+
width: element_box.width,
|
|
407
|
+
height: element_box.height
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
# Apply user-specified clip if provided
|
|
411
|
+
if clip
|
|
412
|
+
element_clip[:x] += clip[:x]
|
|
413
|
+
element_clip[:y] += clip[:y]
|
|
414
|
+
element_clip[:width] = clip[:width]
|
|
415
|
+
element_clip[:height] = clip[:height]
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
page.screenshot(
|
|
419
|
+
path: path,
|
|
420
|
+
type: type,
|
|
421
|
+
clip: element_clip
|
|
422
|
+
)
|
|
423
|
+
end
|
|
424
|
+
|
|
302
425
|
# Check if element is intersecting the viewport
|
|
303
426
|
# @rbs threshold: Numeric -- Intersection ratio threshold
|
|
304
427
|
# @rbs return: bool -- Whether element intersects viewport
|
|
@@ -353,6 +476,10 @@ module Puppeteer
|
|
|
353
476
|
if (!(element instanceof Element)) {
|
|
354
477
|
return null;
|
|
355
478
|
}
|
|
479
|
+
// Element is not visible
|
|
480
|
+
if (element.getClientRects().length === 0) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
356
483
|
const rect = element.getBoundingClientRect();
|
|
357
484
|
return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
|
|
358
485
|
}
|
|
@@ -360,12 +487,12 @@ module Puppeteer
|
|
|
360
487
|
|
|
361
488
|
return nil unless result
|
|
362
489
|
|
|
363
|
-
|
|
364
|
-
return nil
|
|
490
|
+
offset = top_left_corner_of_frame
|
|
491
|
+
return nil unless offset
|
|
365
492
|
|
|
366
493
|
BoundingBox.new(
|
|
367
|
-
x: result['x'],
|
|
368
|
-
y: result['y'],
|
|
494
|
+
x: result['x'] + offset[:x],
|
|
495
|
+
y: result['y'] + offset[:y],
|
|
369
496
|
width: result['width'],
|
|
370
497
|
height: result['height']
|
|
371
498
|
)
|
|
@@ -450,12 +577,23 @@ module Puppeteer
|
|
|
450
577
|
|
|
451
578
|
return nil unless model
|
|
452
579
|
|
|
580
|
+
offset = top_left_corner_of_frame
|
|
581
|
+
return nil unless offset
|
|
582
|
+
|
|
453
583
|
# Convert raw arrays to Point objects for each quad
|
|
454
584
|
BoxModel.new(
|
|
455
|
-
content: model['content'].map
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
585
|
+
content: model['content'].map do |p|
|
|
586
|
+
Point.new(x: p['x'] + offset[:x], y: p['y'] + offset[:y])
|
|
587
|
+
end,
|
|
588
|
+
padding: model['padding'].map do |p|
|
|
589
|
+
Point.new(x: p['x'] + offset[:x], y: p['y'] + offset[:y])
|
|
590
|
+
end,
|
|
591
|
+
border: model['border'].map do |p|
|
|
592
|
+
Point.new(x: p['x'] + offset[:x], y: p['y'] + offset[:y])
|
|
593
|
+
end,
|
|
594
|
+
margin: model['margin'].map do |p|
|
|
595
|
+
Point.new(x: p['x'] + offset[:x], y: p['y'] + offset[:y])
|
|
596
|
+
end,
|
|
459
597
|
width: model['width'],
|
|
460
598
|
height: model['height']
|
|
461
599
|
)
|
|
@@ -591,6 +729,60 @@ module Puppeteer
|
|
|
591
729
|
JS
|
|
592
730
|
end
|
|
593
731
|
|
|
732
|
+
# Get bounding box ensuring it's non-empty and visible
|
|
733
|
+
# @rbs return: BoundingBox -- Non-empty bounding box
|
|
734
|
+
def non_empty_visible_bounding_box
|
|
735
|
+
box = bounding_box
|
|
736
|
+
raise 'Node is either not visible or not an HTMLElement' unless box
|
|
737
|
+
raise 'Node has 0 width.' if box.width.zero?
|
|
738
|
+
raise 'Node has 0 height.' if box.height.zero?
|
|
739
|
+
|
|
740
|
+
box
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
# Get top-left corner offset of the element's frame relative to the main frame
|
|
744
|
+
# @rbs return: Hash[Symbol, Numeric]? -- Offset hash or nil if not visible
|
|
745
|
+
def top_left_corner_of_frame
|
|
746
|
+
point = { x: 0, y: 0 }
|
|
747
|
+
current_frame = frame
|
|
748
|
+
|
|
749
|
+
while (parent_frame = current_frame.parent_frame)
|
|
750
|
+
handle = current_frame.frame_element
|
|
751
|
+
raise 'Unsupported frame type' unless handle
|
|
752
|
+
|
|
753
|
+
begin
|
|
754
|
+
parent_box = handle.evaluate(<<~JS)
|
|
755
|
+
element => {
|
|
756
|
+
// Element is not visible.
|
|
757
|
+
if (element.getClientRects().length === 0) {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
const rect = element.getBoundingClientRect();
|
|
761
|
+
const style = window.getComputedStyle(element);
|
|
762
|
+
return {
|
|
763
|
+
left: rect.left +
|
|
764
|
+
parseInt(style.paddingLeft, 10) +
|
|
765
|
+
parseInt(style.borderLeftWidth, 10),
|
|
766
|
+
top: rect.top +
|
|
767
|
+
parseInt(style.paddingTop, 10) +
|
|
768
|
+
parseInt(style.borderTopWidth, 10)
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
JS
|
|
772
|
+
ensure
|
|
773
|
+
handle.dispose unless handle.disposed?
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
return nil unless parent_box
|
|
777
|
+
|
|
778
|
+
point[:x] += parent_box['left']
|
|
779
|
+
point[:y] += parent_box['top']
|
|
780
|
+
current_frame = parent_frame
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
point
|
|
784
|
+
end
|
|
785
|
+
|
|
594
786
|
# String representation includes element type
|
|
595
787
|
# @rbs return: String -- String representation
|
|
596
788
|
def to_s
|
data/lib/puppeteer/bidi/frame.rb
CHANGED
|
@@ -117,6 +117,24 @@ module Puppeteer
|
|
|
117
117
|
end
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
+
# Create a locator for a selector or function.
|
|
121
|
+
# @rbs selector: String? -- Selector to locate
|
|
122
|
+
# @rbs function: String? -- JavaScript function for function locator
|
|
123
|
+
# @rbs return: Locator -- Locator instance
|
|
124
|
+
def locator(selector = nil, function: nil)
|
|
125
|
+
assert_not_detached
|
|
126
|
+
|
|
127
|
+
if function
|
|
128
|
+
raise ArgumentError, "selector and function cannot both be set" if selector
|
|
129
|
+
|
|
130
|
+
FunctionLocator.create(self, function)
|
|
131
|
+
elsif selector
|
|
132
|
+
NodeLocator.create(self, selector)
|
|
133
|
+
else
|
|
134
|
+
raise ArgumentError, "selector or function must be provided"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
120
138
|
# Evaluate a function on the first element matching the selector
|
|
121
139
|
# @rbs selector: String -- Selector to query
|
|
122
140
|
# @rbs page_function: String -- JavaScript function to evaluate
|
|
@@ -199,6 +217,24 @@ module Puppeteer
|
|
|
199
217
|
end
|
|
200
218
|
end
|
|
201
219
|
|
|
220
|
+
# Select options on a <select> element matching the selector
|
|
221
|
+
# Triggers 'change' and 'input' events once all options are selected.
|
|
222
|
+
# @rbs selector: String -- Selector for <select> element
|
|
223
|
+
# @rbs *values: String -- Option values to select
|
|
224
|
+
# @rbs return: Array[String] -- Actually selected option values
|
|
225
|
+
def select(selector, *values)
|
|
226
|
+
assert_not_detached
|
|
227
|
+
|
|
228
|
+
handle = query_selector(selector)
|
|
229
|
+
raise SelectorNotFoundError, selector unless handle
|
|
230
|
+
|
|
231
|
+
begin
|
|
232
|
+
handle.select(*values)
|
|
233
|
+
ensure
|
|
234
|
+
handle.dispose
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
202
238
|
# Get the frame URL
|
|
203
239
|
# @rbs return: String -- Current URL
|
|
204
240
|
def url
|
|
@@ -214,10 +250,7 @@ module Puppeteer
|
|
|
214
250
|
response = wait_for_navigation(timeout: timeout, wait_until: wait_until) do
|
|
215
251
|
@browsing_context.navigate(url, wait: 'interactive').wait
|
|
216
252
|
end
|
|
217
|
-
|
|
218
|
-
# Note: Currently we don't track HTTP status codes from BiDi protocol
|
|
219
|
-
# Assuming successful navigation (200 OK)
|
|
220
|
-
HTTPResponse.new(url: @browsing_context.url, status: 200)
|
|
253
|
+
response
|
|
221
254
|
end
|
|
222
255
|
|
|
223
256
|
# Set frame content
|
|
@@ -367,6 +400,12 @@ module Puppeteer
|
|
|
367
400
|
# Track navigation type for response creation
|
|
368
401
|
navigation_type = nil # :full_page, :fragment, or :history
|
|
369
402
|
navigation_obj = nil # The navigation object we're waiting for
|
|
403
|
+
load_listener_registered = false
|
|
404
|
+
|
|
405
|
+
# Define load_listener upfront to satisfy type checker
|
|
406
|
+
load_listener = proc do
|
|
407
|
+
promise.resolve(:full_page) unless promise.resolved?
|
|
408
|
+
end
|
|
370
409
|
|
|
371
410
|
# Helper to set up navigation listeners
|
|
372
411
|
setup_navigation_listeners = proc do |navigation|
|
|
@@ -389,8 +428,9 @@ module Puppeteer
|
|
|
389
428
|
end
|
|
390
429
|
|
|
391
430
|
# Also listen for load/domcontentloaded events to complete navigation
|
|
392
|
-
|
|
393
|
-
|
|
431
|
+
unless load_listener_registered
|
|
432
|
+
@browsing_context.once(load_event, &load_listener)
|
|
433
|
+
load_listener_registered = true
|
|
394
434
|
end
|
|
395
435
|
end
|
|
396
436
|
|
|
@@ -461,11 +501,9 @@ module Puppeteer
|
|
|
461
501
|
end
|
|
462
502
|
|
|
463
503
|
# Return HTTPResponse for full page navigation, nil for fragment/history
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
nil
|
|
468
|
-
end
|
|
504
|
+
return nil unless result == :full_page
|
|
505
|
+
|
|
506
|
+
navigation_response_for(navigation_obj)
|
|
469
507
|
rescue Async::TimeoutError
|
|
470
508
|
raise Puppeteer::Bidi::TimeoutError, "Navigation timeout of #{timeout}ms exceeded"
|
|
471
509
|
ensure
|
|
@@ -474,6 +512,7 @@ module Puppeteer
|
|
|
474
512
|
@browsing_context.off(:history_updated, &history_listener)
|
|
475
513
|
@browsing_context.off(:fragment_navigated, &fragment_listener)
|
|
476
514
|
@browsing_context.off(:closed, &closed_listener)
|
|
515
|
+
@browsing_context.off(load_event, &load_listener) if load_listener_registered
|
|
477
516
|
end
|
|
478
517
|
end
|
|
479
518
|
|
|
@@ -556,6 +595,26 @@ module Puppeteer
|
|
|
556
595
|
@browsing_context.on(:fragment_navigated) do
|
|
557
596
|
page.emit(:framenavigated, self)
|
|
558
597
|
end
|
|
598
|
+
|
|
599
|
+
@browsing_context.on(:request) do |data|
|
|
600
|
+
request = data[:request]
|
|
601
|
+
http_request = HTTPRequest.from(
|
|
602
|
+
request,
|
|
603
|
+
self,
|
|
604
|
+
page.network_interception_enabled?
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
request.once(:success) do
|
|
608
|
+
page.emit(:requestfinished, http_request)
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
request.once(:error) do
|
|
612
|
+
page.emit(:requestfailed, http_request)
|
|
613
|
+
end
|
|
614
|
+
page.request_interception_semaphore.async do
|
|
615
|
+
http_request.finalize_interceptions
|
|
616
|
+
end
|
|
617
|
+
end
|
|
559
618
|
end
|
|
560
619
|
|
|
561
620
|
# Create a Frame for a child browsing context
|
|
@@ -592,6 +651,51 @@ module Puppeteer
|
|
|
592
651
|
raise FrameDetachedError, "Attempted to use detached Frame '#{_id}'." if @browsing_context.closed?
|
|
593
652
|
end
|
|
594
653
|
|
|
654
|
+
def navigation_response_for(navigation)
|
|
655
|
+
return nil unless navigation&.request
|
|
656
|
+
|
|
657
|
+
request = navigation.request
|
|
658
|
+
resolved_request = request.last_redirect || request
|
|
659
|
+
http_request = HTTPRequest.for_core_request(resolved_request)
|
|
660
|
+
return http_request.response if http_request&.response
|
|
661
|
+
|
|
662
|
+
wait_for_request_completion(request)
|
|
663
|
+
|
|
664
|
+
resolved_request = request.last_redirect || request
|
|
665
|
+
http_request = HTTPRequest.for_core_request(resolved_request)
|
|
666
|
+
http_request&.response
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def wait_for_request_completion(request)
|
|
670
|
+
loop do
|
|
671
|
+
return if request.response || request.error
|
|
672
|
+
|
|
673
|
+
promise = Async::Promise.new
|
|
674
|
+
success_listener = proc do
|
|
675
|
+
promise.resolve(:done) unless promise.resolved?
|
|
676
|
+
end
|
|
677
|
+
error_listener = proc do
|
|
678
|
+
promise.resolve(:done) unless promise.resolved?
|
|
679
|
+
end
|
|
680
|
+
redirect_listener = proc do |redirect_request|
|
|
681
|
+
promise.resolve(redirect_request) unless promise.resolved?
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
request.on(:success, &success_listener)
|
|
685
|
+
request.on(:error, &error_listener)
|
|
686
|
+
request.on(:redirect, &redirect_listener)
|
|
687
|
+
|
|
688
|
+
result = promise.wait
|
|
689
|
+
request.off(:success, &success_listener)
|
|
690
|
+
request.off(:error, &error_listener)
|
|
691
|
+
request.off(:redirect, &redirect_listener)
|
|
692
|
+
|
|
693
|
+
return if result == :done
|
|
694
|
+
|
|
695
|
+
request = result
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
|
|
595
699
|
end
|
|
596
700
|
end
|
|
597
701
|
end
|