puppeteer-ruby 0.52.1 → 0.53.0
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/docs/api_coverage.md +16 -15
- data/lib/puppeteer/accessibility.rb +300 -0
- data/lib/puppeteer/browser.rb +107 -8
- data/lib/puppeteer/browser_connector.rb +1 -0
- data/lib/puppeteer/browser_context.rb +0 -1
- data/lib/puppeteer/cdp_session.rb +29 -8
- data/lib/puppeteer/chrome_target_manager.rb +145 -34
- data/lib/puppeteer/connection.rb +19 -0
- data/lib/puppeteer/coverage.rb +5 -0
- data/lib/puppeteer/css_coverage.rb +4 -0
- data/lib/puppeteer/dialog.rb +5 -0
- data/lib/puppeteer/element_handle.rb +2 -3
- data/lib/puppeteer/emulation_manager.rb +70 -9
- data/lib/puppeteer/events.rb +2 -0
- data/lib/puppeteer/execution_context.rb +7 -0
- data/lib/puppeteer/extension.rb +20 -6
- data/lib/puppeteer/frame.rb +7 -3
- data/lib/puppeteer/frame_manager.rb +71 -26
- data/lib/puppeteer/http_response.rb +15 -1
- data/lib/puppeteer/isolated_world.rb +10 -4
- data/lib/puppeteer/js_coverage.rb +4 -0
- data/lib/puppeteer/keyboard.rb +6 -0
- data/lib/puppeteer/launcher/browser_options.rb +13 -1
- data/lib/puppeteer/launcher/chrome.rb +48 -4
- data/lib/puppeteer/lifecycle_watcher.rb +1 -6
- data/lib/puppeteer/locators.rb +54 -26
- data/lib/puppeteer/mouse.rb +12 -24
- data/lib/puppeteer/network_manager.rb +16 -4
- data/lib/puppeteer/page.rb +259 -33
- data/lib/puppeteer/puppeteer.rb +6 -5
- data/lib/puppeteer/screen_recorder.rb +269 -0
- data/lib/puppeteer/target.rb +7 -3
- data/lib/puppeteer/touch_screen.rb +6 -0
- data/lib/puppeteer/tracing.rb +4 -0
- data/lib/puppeteer/version.rb +2 -2
- data/lib/puppeteer/wait_task.rb +1 -2
- data/lib/puppeteer/web_mcp.rb +227 -0
- data/lib/puppeteer/web_worker.rb +135 -9
- data/lib/puppeteer.rb +11 -1
- data/puppeteer-ruby.gemspec +1 -0
- data/sig/_supplementary.rbs +33 -1
- data/sig/puppeteer/accessibility.rbs +73 -0
- data/sig/puppeteer/browser.rbs +27 -3
- data/sig/puppeteer/cdp_session.rbs +4 -0
- data/sig/puppeteer/dialog.rbs +3 -0
- data/sig/puppeteer/element_handle.rbs +1 -2
- data/sig/puppeteer/execution_context.rbs +5 -0
- data/sig/puppeteer/extension.rbs +4 -0
- data/sig/puppeteer/frame.rbs +4 -2
- data/sig/puppeteer/http_response.rbs +4 -0
- data/sig/puppeteer/keyboard.rbs +4 -0
- data/sig/puppeteer/locators.rbs +3 -4
- data/sig/puppeteer/mouse.rbs +5 -6
- data/sig/puppeteer/page.rbs +48 -4
- data/sig/puppeteer/puppeteer.rbs +4 -5
- data/sig/puppeteer/screen_recorder.rbs +48 -0
- data/sig/puppeteer/touch_screen.rbs +4 -0
- data/sig/puppeteer/web_mcp.rbs +112 -0
- data/sig/puppeteer/web_worker.rbs +39 -0
- metadata +22 -2
|
@@ -66,9 +66,14 @@ class Puppeteer::CDPSession
|
|
|
66
66
|
# @rbs return: Async::Promise[Hash[String, untyped]] -- Async CDP response
|
|
67
67
|
def async_send_message(method, params = {})
|
|
68
68
|
if !@connection
|
|
69
|
-
raise
|
|
69
|
+
raise Puppeteer::TargetCloseError.new(
|
|
70
|
+
method: method,
|
|
71
|
+
error_message: "Session closed. Most likely the #{@target_type} has been closed.",
|
|
72
|
+
)
|
|
70
73
|
end
|
|
71
74
|
|
|
75
|
+
@connection.ensure_command_allowed!(method)
|
|
76
|
+
|
|
72
77
|
promise = Async::Promise.new
|
|
73
78
|
|
|
74
79
|
@connection.generate_id do |id|
|
|
@@ -81,6 +86,12 @@ class Puppeteer::CDPSession
|
|
|
81
86
|
promise
|
|
82
87
|
end
|
|
83
88
|
|
|
89
|
+
# @rbs id: Integer -- CDP command id
|
|
90
|
+
# @rbs return: bool -- True when the session owns the callback
|
|
91
|
+
def callback?(id)
|
|
92
|
+
@callbacks_mutex.synchronize { @callbacks.key?(id) }
|
|
93
|
+
end
|
|
94
|
+
|
|
84
95
|
# @rbs message: Hash[String, untyped] -- Raw CDP message
|
|
85
96
|
# @rbs return: void -- No return value
|
|
86
97
|
def handle_message(message)
|
|
@@ -97,11 +108,21 @@ class Puppeteer::CDPSession
|
|
|
97
108
|
|
|
98
109
|
private def callback_with_message(callback, message)
|
|
99
110
|
if message['error']
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
if message['error']['message'].include?('Session with given id not found')
|
|
112
|
+
handle_closed
|
|
113
|
+
callback.reject(
|
|
114
|
+
Puppeteer::TargetCloseError.new(
|
|
115
|
+
method: callback.method,
|
|
116
|
+
error_message: 'Session with given id not found.',
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
else
|
|
120
|
+
callback.reject(
|
|
121
|
+
Puppeteer::Connection::ProtocolError.new(
|
|
122
|
+
method: callback.method,
|
|
123
|
+
error_message: message['error']['message'],
|
|
124
|
+
error_data: message['error']['data']))
|
|
125
|
+
end
|
|
105
126
|
else
|
|
106
127
|
callback.resolve(message['result'])
|
|
107
128
|
end
|
|
@@ -122,9 +143,9 @@ class Puppeteer::CDPSession
|
|
|
122
143
|
end
|
|
123
144
|
callbacks.each do |callback|
|
|
124
145
|
callback.reject(
|
|
125
|
-
Puppeteer::
|
|
146
|
+
Puppeteer::TargetCloseError.new(
|
|
126
147
|
method: callback.method,
|
|
127
|
-
error_message: 'Target
|
|
148
|
+
error_message: 'Target closed'))
|
|
128
149
|
end
|
|
129
150
|
@ready_promise.reject(Error.new("Session closed")) unless @ready_promise.resolved?
|
|
130
151
|
@connection = nil
|
|
@@ -2,7 +2,7 @@ class Puppeteer::ChromeTargetManager
|
|
|
2
2
|
include Puppeteer::DebugPrint
|
|
3
3
|
include Puppeteer::EventCallbackable
|
|
4
4
|
|
|
5
|
-
def initialize(connection:, target_factory:, target_filter_callback:, block_list: nil)
|
|
5
|
+
def initialize(connection:, target_factory:, target_filter_callback:, block_list: nil, allow_list: nil)
|
|
6
6
|
@discovered_targets_by_target_id = {}
|
|
7
7
|
@attached_targets_by_target_id = {}
|
|
8
8
|
@attached_targets_by_session_id = {}
|
|
@@ -13,10 +13,17 @@ class Puppeteer::ChromeTargetManager
|
|
|
13
13
|
@connection = connection
|
|
14
14
|
@target_filter_callback = target_filter_callback
|
|
15
15
|
@target_factory = target_factory
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
if block_list && allow_list
|
|
17
|
+
raise ArgumentError.new('Cannot specify both blocklist and allowlist')
|
|
18
|
+
end
|
|
19
|
+
if block_list && !block_list.is_a?(Array)
|
|
18
20
|
raise ArgumentError.new('block_list must be an Array of URL patterns')
|
|
19
21
|
end
|
|
22
|
+
if allow_list && !allow_list.is_a?(Array)
|
|
23
|
+
raise ArgumentError.new('allow_list must be an Array of URL patterns')
|
|
24
|
+
end
|
|
25
|
+
@block_list = map_patterns(block_list)
|
|
26
|
+
@allow_list = map_patterns(allow_list)
|
|
20
27
|
@target_interceptors = {}
|
|
21
28
|
@initialize_promise = Async::Promise.new
|
|
22
29
|
@initial_attach_done = false
|
|
@@ -44,10 +51,7 @@ class Puppeteer::ChromeTargetManager
|
|
|
44
51
|
Async do
|
|
45
52
|
@connection.async_send_message('Target.setDiscoverTargets', {
|
|
46
53
|
discover: true,
|
|
47
|
-
filter: [
|
|
48
|
-
{ type: 'tab', exclude: true },
|
|
49
|
-
{},
|
|
50
|
-
],
|
|
54
|
+
filter: [{}],
|
|
51
55
|
}).wait
|
|
52
56
|
store_existing_targets_for_init
|
|
53
57
|
rescue => err
|
|
@@ -57,7 +61,7 @@ class Puppeteer::ChromeTargetManager
|
|
|
57
61
|
|
|
58
62
|
private def store_existing_targets_for_init
|
|
59
63
|
@discovered_targets_by_target_id.each do |target_id, target_info|
|
|
60
|
-
if @target_filter_callback.call(target_info) && target_info.type
|
|
64
|
+
if @target_filter_callback.call(target_info) && target_info.type == 'tab' && url_allowed?(target_info.url)
|
|
61
65
|
@target_ids_for_init << target_id
|
|
62
66
|
end
|
|
63
67
|
end
|
|
@@ -68,6 +72,10 @@ class Puppeteer::ChromeTargetManager
|
|
|
68
72
|
waitForDebuggerOnStart: true,
|
|
69
73
|
flatten: true,
|
|
70
74
|
autoAttach: true,
|
|
75
|
+
filter: [
|
|
76
|
+
{ type: 'page', exclude: true },
|
|
77
|
+
{},
|
|
78
|
+
],
|
|
71
79
|
})
|
|
72
80
|
@initial_attach_done = true
|
|
73
81
|
finish_initialization_if_ready
|
|
@@ -83,6 +91,19 @@ class Puppeteer::ChromeTargetManager
|
|
|
83
91
|
@attached_targets_by_target_id
|
|
84
92
|
end
|
|
85
93
|
|
|
94
|
+
def remove_extension_service_workers(extension_id)
|
|
95
|
+
extension_url = "chrome-extension://#{extension_id}/"
|
|
96
|
+
target_ids = @discovered_targets_by_target_id.filter_map do |target_id, target_info|
|
|
97
|
+
if target_info.type == 'service_worker' && target_info.url.start_with?(extension_url)
|
|
98
|
+
target_id
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
target_ids.each do |target_id|
|
|
102
|
+
@ignored_targets << target_id
|
|
103
|
+
handle_target_destroyed('targetId' => target_id)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
86
107
|
def wait_for_service_worker_detach(target_id)
|
|
87
108
|
promise = @service_worker_detach_promises[target_id]
|
|
88
109
|
return unless promise
|
|
@@ -171,6 +192,10 @@ class Puppeteer::ChromeTargetManager
|
|
|
171
192
|
return
|
|
172
193
|
end
|
|
173
194
|
original_target = @attached_targets_by_target_id[target_info.target_id]
|
|
195
|
+
if original_target&.target_info&.subtype && !target_info.subtype
|
|
196
|
+
session = original_target.session
|
|
197
|
+
session&.parent_session&.emit_event(CDPSessionEmittedEvents::Swapped, session)
|
|
198
|
+
end
|
|
174
199
|
emit_event(TargetManagerEmittedEvents::TargetChanged, original_target, target_info) if original_target
|
|
175
200
|
end
|
|
176
201
|
|
|
@@ -189,7 +214,7 @@ class Puppeteer::ChromeTargetManager
|
|
|
189
214
|
begin
|
|
190
215
|
Puppeteer::AsyncUtils.await(session.async_send_message('Runtime.runIfWaitingForDebugger'))
|
|
191
216
|
rescue => err
|
|
192
|
-
|
|
217
|
+
debug_puts(err)
|
|
193
218
|
end
|
|
194
219
|
|
|
195
220
|
# We don't use `session.detach()` because that dispatches all commands on
|
|
@@ -199,7 +224,7 @@ class Puppeteer::ChromeTargetManager
|
|
|
199
224
|
sessionId: session.id,
|
|
200
225
|
}))
|
|
201
226
|
rescue => err
|
|
202
|
-
|
|
227
|
+
debug_puts(err)
|
|
203
228
|
ensure
|
|
204
229
|
if detached_promise && !detached_promise.resolved?
|
|
205
230
|
detached_promise.resolve(true)
|
|
@@ -208,7 +233,16 @@ class Puppeteer::ChromeTargetManager
|
|
|
208
233
|
end
|
|
209
234
|
}
|
|
210
235
|
|
|
211
|
-
|
|
236
|
+
unless @connection.auto_attached?(target_info.target_id)
|
|
237
|
+
Async do
|
|
238
|
+
maybe_setup_network_conditions(session, target_info)
|
|
239
|
+
rescue => err
|
|
240
|
+
debug_puts(err)
|
|
241
|
+
ensure
|
|
242
|
+
session.mark_ready
|
|
243
|
+
end
|
|
244
|
+
return
|
|
245
|
+
end
|
|
212
246
|
|
|
213
247
|
if !@initial_attach_done && !url_allowed?(target_info.url)
|
|
214
248
|
finish_initialization_if_ready(target_info.target_id)
|
|
@@ -225,6 +259,23 @@ class Puppeteer::ChromeTargetManager
|
|
|
225
259
|
# CDP.
|
|
226
260
|
if target_info.type == 'service_worker' && @connection.auto_attached?(target_info.target_id)
|
|
227
261
|
finish_initialization_if_ready(target_info.target_id)
|
|
262
|
+
unless url_allowed?(target_info.url)
|
|
263
|
+
Async do
|
|
264
|
+
setup_task = Async do
|
|
265
|
+
maybe_setup_network_conditions(session, target_info)
|
|
266
|
+
rescue => err
|
|
267
|
+
debug_puts(err)
|
|
268
|
+
end
|
|
269
|
+
run_promise = session.async_send_message('Runtime.runIfWaitingForDebugger')
|
|
270
|
+
Puppeteer::AsyncUtils.await(run_promise)
|
|
271
|
+
Puppeteer::AsyncUtils.await(setup_task)
|
|
272
|
+
rescue => err
|
|
273
|
+
debug_puts(err)
|
|
274
|
+
ensure
|
|
275
|
+
session.mark_ready
|
|
276
|
+
end
|
|
277
|
+
return
|
|
278
|
+
end
|
|
228
279
|
@service_worker_detach_promises[target_info.target_id] ||= Async::Promise.new
|
|
229
280
|
silent_detach.call(@service_worker_detach_promises[target_info.target_id])
|
|
230
281
|
return if @attached_targets_by_target_id.has_key?(target_info.target_id)
|
|
@@ -238,7 +289,8 @@ class Puppeteer::ChromeTargetManager
|
|
|
238
289
|
|
|
239
290
|
unless @target_filter_callback.call(target_info)
|
|
240
291
|
@ignored_targets << target_info.target_id
|
|
241
|
-
|
|
292
|
+
parent_target = parent_session.is_a?(Puppeteer::CDPSession) ? parent_session.target : nil
|
|
293
|
+
finish_initialization_if_ready(parent_target.target_id) if parent_target&.raw_type == 'tab'
|
|
242
294
|
silent_detach.call
|
|
243
295
|
|
|
244
296
|
return
|
|
@@ -267,25 +319,39 @@ class Puppeteer::ChromeTargetManager
|
|
|
267
319
|
end
|
|
268
320
|
end
|
|
269
321
|
|
|
270
|
-
@target_ids_for_init.delete(target.target_id)
|
|
271
322
|
unless is_existing_target
|
|
272
323
|
Async do
|
|
273
324
|
Puppeteer::AsyncUtils.future_with_logging { emit_event(TargetManagerEmittedEvents::TargetAvailable, target) }.call
|
|
274
325
|
end
|
|
275
326
|
end
|
|
327
|
+
parent_target = parent_session.is_a?(Puppeteer::CDPSession) ? parent_session.target : nil
|
|
328
|
+
finish_initialization_if_ready(parent_target.target_id) if parent_target&.raw_type == 'tab'
|
|
276
329
|
finish_initialization_if_ready
|
|
277
330
|
parent_session.emit_event(CDPSessionEmittedEvents::Ready, session)
|
|
278
331
|
|
|
279
332
|
Async do
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
333
|
+
setup_task = Async do
|
|
334
|
+
maybe_setup_network_conditions(session, target_info)
|
|
335
|
+
rescue => err
|
|
336
|
+
debug_puts(err)
|
|
337
|
+
end
|
|
338
|
+
promises = [
|
|
339
|
+
session.async_send_message('Target.setAutoAttach', {
|
|
340
|
+
waitForDebuggerOnStart: true,
|
|
341
|
+
flatten: true,
|
|
342
|
+
autoAttach: true,
|
|
343
|
+
filter: [{}],
|
|
344
|
+
}),
|
|
345
|
+
session.async_send_message('Runtime.runIfWaitingForDebugger'),
|
|
346
|
+
setup_task,
|
|
347
|
+
]
|
|
348
|
+
promises.each do |promise|
|
|
349
|
+
Puppeteer::AsyncUtils.await(promise)
|
|
350
|
+
rescue => err
|
|
351
|
+
debug_puts(err)
|
|
352
|
+
end
|
|
287
353
|
rescue => err
|
|
288
|
-
|
|
354
|
+
debug_puts(err)
|
|
289
355
|
ensure
|
|
290
356
|
session.mark_ready
|
|
291
357
|
end
|
|
@@ -307,32 +373,77 @@ class Puppeteer::ChromeTargetManager
|
|
|
307
373
|
end
|
|
308
374
|
|
|
309
375
|
private def url_allowed?(url)
|
|
310
|
-
|
|
376
|
+
block_list = @block_list || []
|
|
377
|
+
allow_list = @allow_list || []
|
|
378
|
+
return true if block_list.empty? && allow_list.empty?
|
|
311
379
|
return true if url.nil? || url.empty? || url == 'about:blank'
|
|
312
380
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
381
|
+
blocked = block_list.any? { |item| item[:pattern].test?(url) }
|
|
382
|
+
return false if blocked
|
|
383
|
+
|
|
384
|
+
return true if allow_list.empty?
|
|
385
|
+
|
|
386
|
+
allow_list.any? { |item| item[:pattern].test?(url) }
|
|
387
|
+
end
|
|
388
|
+
public :url_allowed?
|
|
389
|
+
|
|
390
|
+
private def map_patterns(rules)
|
|
391
|
+
return nil if rules.nil?
|
|
392
|
+
|
|
393
|
+
rules.map do |rule|
|
|
394
|
+
{
|
|
395
|
+
pattern: URLPattern::URLPattern.new(rule),
|
|
396
|
+
rule: rule,
|
|
397
|
+
}
|
|
317
398
|
end
|
|
318
399
|
end
|
|
319
400
|
|
|
320
|
-
private def maybe_setup_network_conditions(session)
|
|
321
|
-
|
|
401
|
+
private def maybe_setup_network_conditions(session, target_info)
|
|
402
|
+
block_list = @block_list || []
|
|
403
|
+
allow_list = @allow_list || []
|
|
404
|
+
return if block_list.empty? && allow_list.empty?
|
|
322
405
|
|
|
323
|
-
matched_network_conditions =
|
|
406
|
+
matched_network_conditions = block_list.map do |item|
|
|
324
407
|
{
|
|
325
|
-
urlPattern:
|
|
408
|
+
urlPattern: item[:rule],
|
|
409
|
+
offline: true,
|
|
326
410
|
latency: 0,
|
|
327
411
|
downloadThroughput: -1,
|
|
328
412
|
uploadThroughput: -1,
|
|
329
413
|
}
|
|
330
414
|
end
|
|
331
|
-
|
|
332
|
-
|
|
415
|
+
allow_list.each do |item|
|
|
416
|
+
matched_network_conditions << {
|
|
417
|
+
urlPattern: item[:rule],
|
|
418
|
+
offline: false,
|
|
419
|
+
latency: 0,
|
|
420
|
+
downloadThroughput: -1,
|
|
421
|
+
uploadThroughput: -1,
|
|
422
|
+
}
|
|
423
|
+
end
|
|
424
|
+
unless allow_list.empty?
|
|
425
|
+
matched_network_conditions << {
|
|
426
|
+
urlPattern: '',
|
|
427
|
+
offline: true,
|
|
428
|
+
latency: 0,
|
|
429
|
+
downloadThroughput: -1,
|
|
430
|
+
uploadThroughput: -1,
|
|
431
|
+
}
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
promises = []
|
|
435
|
+
worker_types = ['worker', 'service_worker', 'shared_worker']
|
|
436
|
+
promises << session.async_send_message('Network.enable') if worker_types.include?(target_info.type)
|
|
437
|
+
params = {
|
|
438
|
+
offline: block_list.empty? ? nil : true,
|
|
333
439
|
matchedNetworkConditions: matched_network_conditions,
|
|
334
|
-
|
|
335
|
-
|
|
440
|
+
}.compact
|
|
441
|
+
promises << session.async_send_message('Network.emulateNetworkConditionsByRule', params)
|
|
442
|
+
promises.each do |promise|
|
|
443
|
+
Puppeteer::AsyncUtils.await(promise)
|
|
444
|
+
rescue => err
|
|
445
|
+
debug_puts(err)
|
|
446
|
+
end
|
|
336
447
|
rescue => err
|
|
337
448
|
message = err.message.to_s.downcase
|
|
338
449
|
return if message.include?('target closed') || message.include?('session closed') || message.include?('not found')
|
data/lib/puppeteer/connection.rb
CHANGED
|
@@ -44,6 +44,7 @@ class Puppeteer::Connection
|
|
|
44
44
|
@callbacks_mutex = Mutex.new
|
|
45
45
|
@delay = delay
|
|
46
46
|
@protocol_timeout = protocol_timeout
|
|
47
|
+
@reject_emulate_network_conditions_calls = false
|
|
47
48
|
|
|
48
49
|
@network_message_queue = Async::Queue.new
|
|
49
50
|
@network_message_task = nil
|
|
@@ -71,6 +72,16 @@ class Puppeteer::Connection
|
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
attr_reader :protocol_timeout
|
|
75
|
+
attr_writer :reject_emulate_network_conditions_calls
|
|
76
|
+
|
|
77
|
+
def ensure_command_allowed!(method)
|
|
78
|
+
return unless method == 'Network.emulateNetworkConditions'
|
|
79
|
+
return unless @reject_emulate_network_conditions_calls
|
|
80
|
+
|
|
81
|
+
raise Puppeteer::Error.new(
|
|
82
|
+
'Cannot reset network conditions: rule-based emulation is enabled.',
|
|
83
|
+
)
|
|
84
|
+
end
|
|
74
85
|
|
|
75
86
|
# used only in Browser#connected?
|
|
76
87
|
def closed?
|
|
@@ -176,6 +187,7 @@ class Puppeteer::Connection
|
|
|
176
187
|
end
|
|
177
188
|
|
|
178
189
|
def async_send_message(method, params = {})
|
|
190
|
+
ensure_command_allowed!(method)
|
|
179
191
|
promise = Async::Promise.new
|
|
180
192
|
|
|
181
193
|
generate_id do |id|
|
|
@@ -332,6 +344,10 @@ class Puppeteer::Connection
|
|
|
332
344
|
else
|
|
333
345
|
callback.resolve(message['result'])
|
|
334
346
|
end
|
|
347
|
+
else
|
|
348
|
+
sessions = @sessions_mutex.synchronize { @sessions.values }
|
|
349
|
+
session = sessions.find { |candidate| candidate.callback?(message['id']) }
|
|
350
|
+
session&.handle_message(message)
|
|
335
351
|
end
|
|
336
352
|
else
|
|
337
353
|
emit_event(message['method'], message['params'])
|
|
@@ -396,3 +412,6 @@ class Puppeteer::Connection
|
|
|
396
412
|
@sessions_mutex.synchronize { @sessions[session_id] }.tap { |session| session&.mark_ready }
|
|
397
413
|
end
|
|
398
414
|
end
|
|
415
|
+
|
|
416
|
+
# ref: https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/common/Errors.ts
|
|
417
|
+
class Puppeteer::TargetCloseError < Puppeteer::Connection::ProtocolError; end
|
data/lib/puppeteer/coverage.rb
CHANGED
|
@@ -5,6 +5,11 @@ class Puppeteer::Coverage
|
|
|
5
5
|
@css = Puppeteer::CSSCoverage.new(client)
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
+
def update_client(client)
|
|
9
|
+
@js.update_client(client)
|
|
10
|
+
@css.update_client(client)
|
|
11
|
+
end
|
|
12
|
+
|
|
8
13
|
def start_js_coverage(
|
|
9
14
|
reset_on_navigation: nil,
|
|
10
15
|
report_anonymous_scripts: nil,
|
data/lib/puppeteer/dialog.rb
CHANGED
|
@@ -16,6 +16,11 @@ class Puppeteer::Dialog
|
|
|
16
16
|
attr_reader :message #: String
|
|
17
17
|
attr_reader :default_value #: String
|
|
18
18
|
|
|
19
|
+
# @rbs return: bool -- Whether the dialog has already been handled
|
|
20
|
+
def handled?
|
|
21
|
+
@handled
|
|
22
|
+
end
|
|
23
|
+
|
|
19
24
|
# @rbs prompt_text: String? -- Text entered into the prompt
|
|
20
25
|
# @rbs return: void -- No return value
|
|
21
26
|
def accept(prompt_text = nil)
|
|
@@ -374,14 +374,13 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
|
|
|
374
374
|
|
|
375
375
|
# @rbs delay: Numeric? -- Delay between down and up (ms)
|
|
376
376
|
# @rbs button: String? -- Mouse button
|
|
377
|
-
# @rbs click_count: Integer? -- Deprecated: use count (click_count only sets clickCount)
|
|
378
377
|
# @rbs count: Integer? -- Number of clicks to perform
|
|
379
378
|
# @rbs offset: Puppeteer::ElementHandle::Offset | Hash[Symbol, Numeric] | nil -- Click offset
|
|
380
379
|
# @rbs return: void -- No return value
|
|
381
|
-
def click(delay: nil, button: nil,
|
|
380
|
+
def click(delay: nil, button: nil, count: nil, offset: nil)
|
|
382
381
|
scroll_into_view_if_needed
|
|
383
382
|
point = clickable_point(offset)
|
|
384
|
-
@page.mouse.click(point.x, point.y, delay: delay, button: button,
|
|
383
|
+
@page.mouse.click(point.x, point.y, delay: delay, button: button, count: count)
|
|
385
384
|
end
|
|
386
385
|
|
|
387
386
|
define_async_method :async_click
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
class Puppeteer::EmulationManager
|
|
2
|
+
include Puppeteer::DebugPrint
|
|
2
3
|
using Puppeteer::DefineAsyncMethod
|
|
3
4
|
|
|
4
5
|
# @param {!Puppeteer.CDPSession} client
|
|
@@ -6,11 +7,59 @@ class Puppeteer::EmulationManager
|
|
|
6
7
|
@client = client
|
|
7
8
|
@emulating_mobile = false
|
|
8
9
|
@has_touch = false
|
|
10
|
+
@viewport = nil
|
|
11
|
+
@locale = nil
|
|
12
|
+
@locale_configured = false
|
|
13
|
+
@secondary_clients = Set.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update_client(client)
|
|
17
|
+
@client = client
|
|
18
|
+
@secondary_clients.delete(client)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def register_speculative_session(client)
|
|
22
|
+
@secondary_clients << client
|
|
23
|
+
client.once(CDPSessionEmittedEvents::Disconnected) do
|
|
24
|
+
@secondary_clients.delete(client)
|
|
25
|
+
end
|
|
26
|
+
promises = []
|
|
27
|
+
promises.concat(viewport_promises(client, @viewport)) if @viewport
|
|
28
|
+
if @locale_configured
|
|
29
|
+
promises << client.async_send_message(
|
|
30
|
+
'Emulation.setLocaleOverride',
|
|
31
|
+
{ locale: @locale }.compact,
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
return if promises.empty?
|
|
35
|
+
|
|
36
|
+
Async do
|
|
37
|
+
Puppeteer::AsyncUtils.await_promise_all(*promises)
|
|
38
|
+
rescue => err
|
|
39
|
+
debug_puts(err)
|
|
40
|
+
end
|
|
9
41
|
end
|
|
10
42
|
|
|
11
43
|
# @param viewport [Puppeteer::Viewport]
|
|
12
44
|
# @return [true|false]
|
|
13
45
|
def emulate_viewport(viewport)
|
|
46
|
+
mobile = viewport.mobile?
|
|
47
|
+
has_touch = viewport.has_touch?
|
|
48
|
+
|
|
49
|
+
apply_viewport(@client, viewport)
|
|
50
|
+
@viewport = viewport
|
|
51
|
+
|
|
52
|
+
reload_needed = @emulating_mobile != mobile || @has_touch != has_touch
|
|
53
|
+
@emulating_mobile = mobile
|
|
54
|
+
@has_touch = has_touch
|
|
55
|
+
reload_needed
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private def apply_viewport(client, viewport)
|
|
59
|
+
Puppeteer::AsyncUtils.await_promise_all(*viewport_promises(client, viewport))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private def viewport_promises(client, viewport)
|
|
14
63
|
mobile = viewport.mobile?
|
|
15
64
|
width = viewport.width
|
|
16
65
|
height = viewport.height
|
|
@@ -23,24 +72,36 @@ class Puppeteer::EmulationManager
|
|
|
23
72
|
end
|
|
24
73
|
has_touch = viewport.has_touch?
|
|
25
74
|
|
|
26
|
-
|
|
27
|
-
|
|
75
|
+
[
|
|
76
|
+
client.async_send_message('Emulation.setDeviceMetricsOverride',
|
|
28
77
|
mobile: mobile,
|
|
29
78
|
width: width,
|
|
30
79
|
height: height,
|
|
31
80
|
deviceScaleFactor: device_scale_factor,
|
|
32
81
|
screenOrientation: screen_orientation,
|
|
33
82
|
),
|
|
34
|
-
|
|
83
|
+
client.async_send_message('Emulation.setTouchEmulationEnabled',
|
|
35
84
|
enabled: has_touch,
|
|
36
85
|
),
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
reload_needed = @emulating_mobile != mobile || @has_touch != has_touch
|
|
40
|
-
@emulating_mobile = mobile
|
|
41
|
-
@has_touch = has_touch
|
|
42
|
-
reload_needed
|
|
86
|
+
]
|
|
43
87
|
end
|
|
44
88
|
|
|
45
89
|
define_async_method :async_emulate_viewport
|
|
90
|
+
|
|
91
|
+
# @rbs locale: String? -- Locale to emulate, or nil to disable emulation
|
|
92
|
+
# @rbs return: void -- No return value
|
|
93
|
+
def emulate_locale(locale)
|
|
94
|
+
@locale = locale
|
|
95
|
+
@locale_configured = true
|
|
96
|
+
clients = [@client, *@secondary_clients]
|
|
97
|
+
promises = clients.map do |client|
|
|
98
|
+
client.async_send_message(
|
|
99
|
+
'Emulation.setLocaleOverride',
|
|
100
|
+
{ locale: locale }.compact,
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
Puppeteer::AsyncUtils.await_promise_all(*promises)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
define_async_method :async_emulate_locale
|
|
46
107
|
end
|
data/lib/puppeteer/events.rb
CHANGED
|
@@ -35,6 +35,7 @@ module CDPSessionEmittedEvents ; end
|
|
|
35
35
|
{
|
|
36
36
|
Disconnected: EventsDefinitionUtils.symbol('CDPSession.Disconnected'),
|
|
37
37
|
Ready: EventsDefinitionUtils.symbol('CDPSession.Ready'),
|
|
38
|
+
Swapped: EventsDefinitionUtils.symbol('CDPSession.Swapped'),
|
|
38
39
|
}.define_const_into(CDPSessionEmittedEvents)
|
|
39
40
|
|
|
40
41
|
# All the events a Browser may emit.
|
|
@@ -98,6 +99,7 @@ module FrameManagerEmittedEvents ; end
|
|
|
98
99
|
FrameNavigated: EventsDefinitionUtils.symbol('FrameManager.FrameNavigated'),
|
|
99
100
|
FrameDetached: EventsDefinitionUtils.symbol('FrameManager.FrameDetached'),
|
|
100
101
|
FrameSwapped: EventsDefinitionUtils.symbol('FrameManager.FrameSwapped'),
|
|
102
|
+
FrameSwappedByActivation: EventsDefinitionUtils.symbol('FrameManager.FrameSwappedByActivation'),
|
|
101
103
|
LifecycleEvent: EventsDefinitionUtils.symbol('FrameManager.LifecycleEvent'),
|
|
102
104
|
FrameNavigatedWithinDocument: EventsDefinitionUtils.symbol('FrameManager.FrameNavigatedWithinDocument'),
|
|
103
105
|
ExecutionContextCreated: EventsDefinitionUtils.symbol('FrameManager.ExecutionContextCreated'),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
class Puppeteer::ExecutionContext
|
|
4
4
|
include Puppeteer::IfPresent
|
|
5
|
+
include Puppeteer::EventCallbackable
|
|
5
6
|
using Puppeteer::DefineAsyncMethod
|
|
6
7
|
|
|
7
8
|
EVALUATION_SCRIPT_URL = 'pprt://__puppeteer_evaluation_script__'
|
|
@@ -19,6 +20,11 @@ class Puppeteer::ExecutionContext
|
|
|
19
20
|
|
|
20
21
|
attr_reader :client, :world
|
|
21
22
|
|
|
23
|
+
# @rbs return: void -- Notify consumers that this context was destroyed
|
|
24
|
+
def dispose
|
|
25
|
+
emit_event('disposed')
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
# only used in IsolaatedWorld
|
|
23
29
|
private def _context_id
|
|
24
30
|
@context_id
|
|
@@ -353,6 +359,7 @@ class Puppeteer::ExecutionContext
|
|
|
353
359
|
message = error.message
|
|
354
360
|
if message.end_with?('Cannot find context with specified id') ||
|
|
355
361
|
message.end_with?('Inspected target navigated or closed')
|
|
362
|
+
|
|
356
363
|
return Puppeteer::Error.new('Execution context was destroyed, most likely because of a navigation.')
|
|
357
364
|
end
|
|
358
365
|
|
data/lib/puppeteer/extension.rb
CHANGED
|
@@ -38,11 +38,16 @@ class Puppeteer::Extension
|
|
|
38
38
|
extension_targets = @browser.targets.select do |target|
|
|
39
39
|
target.type == 'service_worker' && target.url.start_with?(extension_prefix)
|
|
40
40
|
end
|
|
41
|
-
extension_targets.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
tasks = extension_targets.map do |target|
|
|
42
|
+
proc do
|
|
43
|
+
target.worker
|
|
44
|
+
rescue => error
|
|
45
|
+
raise unless can_ignore_error?(error)
|
|
46
|
+
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
45
49
|
end
|
|
50
|
+
Puppeteer::AsyncUtils.await_promise_all(*tasks).compact
|
|
46
51
|
end
|
|
47
52
|
|
|
48
53
|
# @rbs return: Array[Puppeteer::Page] -- Extension pages
|
|
@@ -54,15 +59,24 @@ class Puppeteer::Extension
|
|
|
54
59
|
end
|
|
55
60
|
extension_targets.filter_map do |target|
|
|
56
61
|
target.as_page
|
|
57
|
-
rescue
|
|
62
|
+
rescue => error
|
|
63
|
+
raise unless can_ignore_error?(error)
|
|
64
|
+
|
|
58
65
|
nil
|
|
59
66
|
end
|
|
60
67
|
end
|
|
61
68
|
|
|
69
|
+
# @rbs error: StandardError -- Error raised while resolving an extension target
|
|
70
|
+
# @rbs return: bool -- Whether the target resolution error can be ignored
|
|
71
|
+
private def can_ignore_error?(error)
|
|
72
|
+
error.is_a?(Puppeteer::TargetCloseError) ||
|
|
73
|
+
error.message.include?('No target with given id found')
|
|
74
|
+
end
|
|
75
|
+
|
|
62
76
|
# @rbs page: Puppeteer::Page -- Target page
|
|
63
77
|
# @rbs return: void -- No return value
|
|
64
78
|
def trigger_action(page)
|
|
65
|
-
|
|
79
|
+
@browser.send(:connection).send_message('Extensions.triggerAction', {
|
|
66
80
|
id: @id,
|
|
67
81
|
targetId: page._tab_id,
|
|
68
82
|
})
|