puppeteer-ruby 0.45.6 → 0.50.0.alpha5
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/.rubocop.yml +1 -3
- data/AGENTS.md +169 -0
- data/CLAUDE/README.md +41 -0
- data/CLAUDE/architecture.md +253 -0
- data/CLAUDE/cdp_protocol.md +230 -0
- data/CLAUDE/concurrency.md +216 -0
- data/CLAUDE/porting_puppeteer.md +575 -0
- data/CLAUDE/rbs_type_checking.md +101 -0
- data/CLAUDE/spec_migration_plans.md +1041 -0
- data/CLAUDE/testing.md +278 -0
- data/CLAUDE.md +242 -0
- data/README.md +8 -0
- data/Rakefile +7 -0
- data/Steepfile +28 -0
- data/docs/api_coverage.md +105 -56
- data/lib/puppeteer/aria_query_handler.rb +3 -2
- data/lib/puppeteer/async_utils.rb +214 -0
- data/lib/puppeteer/browser.rb +98 -56
- data/lib/puppeteer/browser_connector.rb +18 -3
- data/lib/puppeteer/browser_context.rb +196 -3
- data/lib/puppeteer/browser_runner.rb +18 -10
- data/lib/puppeteer/cdp_session.rb +67 -23
- data/lib/puppeteer/chrome_target_manager.rb +65 -40
- data/lib/puppeteer/connection.rb +55 -36
- data/lib/puppeteer/console_message.rb +9 -1
- data/lib/puppeteer/console_patch.rb +47 -0
- data/lib/puppeteer/css_coverage.rb +5 -3
- data/lib/puppeteer/custom_query_handler.rb +80 -33
- data/lib/puppeteer/define_async_method.rb +31 -37
- data/lib/puppeteer/dialog.rb +47 -14
- data/lib/puppeteer/element_handle.rb +231 -62
- data/lib/puppeteer/emulation_manager.rb +1 -1
- data/lib/puppeteer/env.rb +1 -1
- data/lib/puppeteer/errors.rb +25 -2
- data/lib/puppeteer/event_callbackable.rb +15 -0
- data/lib/puppeteer/events.rb +4 -0
- data/lib/puppeteer/execution_context.rb +148 -3
- data/lib/puppeteer/file_chooser.rb +6 -0
- data/lib/puppeteer/frame.rb +162 -91
- data/lib/puppeteer/frame_manager.rb +69 -48
- data/lib/puppeteer/http_request.rb +114 -38
- data/lib/puppeteer/http_response.rb +24 -7
- data/lib/puppeteer/isolated_world.rb +64 -41
- data/lib/puppeteer/js_coverage.rb +5 -3
- data/lib/puppeteer/js_handle.rb +58 -16
- data/lib/puppeteer/keyboard.rb +30 -17
- data/lib/puppeteer/launcher/browser_options.rb +3 -1
- data/lib/puppeteer/launcher/chrome.rb +8 -5
- data/lib/puppeteer/launcher/launch_options.rb +7 -2
- data/lib/puppeteer/launcher.rb +4 -8
- data/lib/puppeteer/lifecycle_watcher.rb +38 -22
- data/lib/puppeteer/mouse.rb +273 -64
- data/lib/puppeteer/network_event_manager.rb +7 -0
- data/lib/puppeteer/network_manager.rb +393 -112
- data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
- data/lib/puppeteer/page.rb +568 -226
- data/lib/puppeteer/puppeteer.rb +171 -64
- data/lib/puppeteer/query_handler_manager.rb +112 -16
- data/lib/puppeteer/reactor_runner.rb +247 -0
- data/lib/puppeteer/remote_object.rb +127 -47
- data/lib/puppeteer/target.rb +74 -27
- data/lib/puppeteer/task_manager.rb +3 -1
- data/lib/puppeteer/timeout_helper.rb +6 -10
- data/lib/puppeteer/touch_handle.rb +39 -0
- data/lib/puppeteer/touch_screen.rb +72 -22
- data/lib/puppeteer/tracing.rb +3 -3
- data/lib/puppeteer/version.rb +1 -1
- data/lib/puppeteer/wait_task.rb +264 -101
- data/lib/puppeteer/web_socket.rb +2 -2
- data/lib/puppeteer/web_socket_transport.rb +91 -27
- data/lib/puppeteer/web_worker.rb +175 -0
- data/lib/puppeteer.rb +20 -4
- data/puppeteer-ruby.gemspec +15 -11
- data/sig/_external.rbs +8 -0
- data/sig/_supplementary.rbs +314 -0
- data/sig/puppeteer/browser.rbs +166 -0
- data/sig/puppeteer/cdp_session.rbs +64 -0
- data/sig/puppeteer/dialog.rbs +41 -0
- data/sig/puppeteer/element_handle.rbs +305 -0
- data/sig/puppeteer/execution_context.rbs +87 -0
- data/sig/puppeteer/frame.rbs +226 -0
- data/sig/puppeteer/http_request.rbs +214 -0
- data/sig/puppeteer/http_response.rbs +89 -0
- data/sig/puppeteer/js_handle.rbs +64 -0
- data/sig/puppeteer/keyboard.rbs +40 -0
- data/sig/puppeteer/mouse.rbs +113 -0
- data/sig/puppeteer/page.rbs +515 -0
- data/sig/puppeteer/puppeteer.rbs +98 -0
- data/sig/puppeteer/remote_object.rbs +78 -0
- data/sig/puppeteer/touch_handle.rbs +21 -0
- data/sig/puppeteer/touch_screen.rbs +35 -0
- data/sig/puppeteer/web_worker.rbs +83 -0
- metadata +116 -45
- data/CHANGELOG.md +0 -397
- data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
- data/lib/puppeteer/firefox_target_manager.rb +0 -157
- data/lib/puppeteer/launcher/firefox.rb +0 -453
data/lib/puppeteer/browser.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
1
3
|
require 'thread'
|
|
2
|
-
require 'timeout'
|
|
3
4
|
|
|
4
5
|
class Puppeteer::Browser
|
|
5
6
|
include Puppeteer::DebugPrint
|
|
@@ -7,18 +8,23 @@ class Puppeteer::Browser
|
|
|
7
8
|
include Puppeteer::IfPresent
|
|
8
9
|
using Puppeteer::DefineAsyncMethod
|
|
9
10
|
|
|
10
|
-
# @
|
|
11
|
-
# @
|
|
12
|
-
# @
|
|
13
|
-
# @
|
|
14
|
-
# @
|
|
15
|
-
# @
|
|
16
|
-
# @
|
|
11
|
+
# @rbs product: String? -- Browser product (chrome only)
|
|
12
|
+
# @rbs connection: Puppeteer::Connection -- CDP connection
|
|
13
|
+
# @rbs context_ids: Array[String] -- Browser context IDs
|
|
14
|
+
# @rbs ignore_https_errors: bool -- Ignore HTTPS errors
|
|
15
|
+
# @rbs default_viewport: Puppeteer::Viewport? -- Default viewport
|
|
16
|
+
# @rbs network_enabled: bool -- Whether network events are enabled
|
|
17
|
+
# @rbs process: Puppeteer::BrowserRunner::BrowserProcess? -- Browser process handle
|
|
18
|
+
# @rbs close_callback: Proc -- Close callback
|
|
19
|
+
# @rbs target_filter_callback: Proc? -- Target filter callback
|
|
20
|
+
# @rbs is_page_target_callback: Proc? -- Page target predicate
|
|
21
|
+
# @rbs return: Puppeteer::Browser -- Browser instance
|
|
17
22
|
def self.create(product:,
|
|
18
23
|
connection:,
|
|
19
24
|
context_ids:,
|
|
20
25
|
ignore_https_errors:,
|
|
21
26
|
default_viewport:,
|
|
27
|
+
network_enabled: true,
|
|
22
28
|
process:,
|
|
23
29
|
close_callback:,
|
|
24
30
|
target_filter_callback:,
|
|
@@ -29,6 +35,7 @@ class Puppeteer::Browser
|
|
|
29
35
|
context_ids: context_ids,
|
|
30
36
|
ignore_https_errors: ignore_https_errors,
|
|
31
37
|
default_viewport: default_viewport,
|
|
38
|
+
network_enabled: network_enabled,
|
|
32
39
|
process: process,
|
|
33
40
|
close_callback: close_callback,
|
|
34
41
|
target_filter_callback: target_filter_callback,
|
|
@@ -38,25 +45,34 @@ class Puppeteer::Browser
|
|
|
38
45
|
browser
|
|
39
46
|
end
|
|
40
47
|
|
|
41
|
-
# @
|
|
42
|
-
# @
|
|
43
|
-
# @
|
|
44
|
-
# @
|
|
45
|
-
# @
|
|
46
|
-
# @
|
|
47
|
-
# @
|
|
48
|
+
# @rbs product: String? -- Browser product (chrome only)
|
|
49
|
+
# @rbs connection: Puppeteer::Connection -- CDP connection
|
|
50
|
+
# @rbs context_ids: Array[String] -- Browser context IDs
|
|
51
|
+
# @rbs ignore_https_errors: bool -- Ignore HTTPS errors
|
|
52
|
+
# @rbs default_viewport: Puppeteer::Viewport? -- Default viewport
|
|
53
|
+
# @rbs network_enabled: bool -- Whether network events are enabled
|
|
54
|
+
# @rbs process: Puppeteer::BrowserRunner::BrowserProcess? -- Browser process handle
|
|
55
|
+
# @rbs close_callback: Proc -- Close callback
|
|
56
|
+
# @rbs target_filter_callback: Proc? -- Target filter callback
|
|
57
|
+
# @rbs is_page_target_callback: Proc? -- Page target predicate
|
|
58
|
+
# @rbs return: void -- No return value
|
|
48
59
|
def initialize(product:,
|
|
49
60
|
connection:,
|
|
50
61
|
context_ids:,
|
|
51
62
|
ignore_https_errors:,
|
|
52
63
|
default_viewport:,
|
|
64
|
+
network_enabled: true,
|
|
53
65
|
process:,
|
|
54
66
|
close_callback:,
|
|
55
67
|
target_filter_callback:,
|
|
56
68
|
is_page_target_callback:)
|
|
57
|
-
@product = product
|
|
69
|
+
@product = product ? product.to_s : 'chrome'
|
|
70
|
+
if @product != 'chrome'
|
|
71
|
+
raise ArgumentError.new("Unsupported product: #{@product}. Only 'chrome' is supported.")
|
|
72
|
+
end
|
|
58
73
|
@ignore_https_errors = ignore_https_errors
|
|
59
74
|
@default_viewport = default_viewport
|
|
75
|
+
@network_enabled = network_enabled
|
|
60
76
|
@process = process
|
|
61
77
|
@connection = connection
|
|
62
78
|
@close_callback = close_callback
|
|
@@ -69,19 +85,11 @@ class Puppeteer::Browser
|
|
|
69
85
|
@contexts[context_id] = Puppeteer::BrowserContext.new(@connection, self, context_id)
|
|
70
86
|
end
|
|
71
87
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
else
|
|
79
|
-
@target_manager = Puppeteer::ChromeTargetManager.new(
|
|
80
|
-
connection: connection,
|
|
81
|
-
target_factory: method(:create_target),
|
|
82
|
-
target_filter_callback: @target_filter_callback,
|
|
83
|
-
)
|
|
84
|
-
end
|
|
88
|
+
@target_manager = Puppeteer::ChromeTargetManager.new(
|
|
89
|
+
connection: connection,
|
|
90
|
+
target_factory: method(:create_target),
|
|
91
|
+
target_filter_callback: @target_filter_callback,
|
|
92
|
+
)
|
|
85
93
|
end
|
|
86
94
|
|
|
87
95
|
private def default_target_filter_callback(target_info)
|
|
@@ -94,7 +102,9 @@ class Puppeteer::Browser
|
|
|
94
102
|
|
|
95
103
|
attr_reader :is_page_target_callback
|
|
96
104
|
|
|
97
|
-
# @
|
|
105
|
+
# @rbs event_name: (String | Symbol) -- Browser event name
|
|
106
|
+
# @rbs &block: ^(untyped) -> void -- Event handler
|
|
107
|
+
# @rbs return: String -- Listener ID
|
|
98
108
|
def on(event_name, &block)
|
|
99
109
|
unless BrowserEmittedEvents.values.include?(event_name.to_s)
|
|
100
110
|
raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{BrowserEmittedEvents.values.to_a.join(", ")}")
|
|
@@ -103,7 +113,9 @@ class Puppeteer::Browser
|
|
|
103
113
|
super(event_name.to_s, &block)
|
|
104
114
|
end
|
|
105
115
|
|
|
106
|
-
# @
|
|
116
|
+
# @rbs event_name: (String | Symbol) -- Browser event name
|
|
117
|
+
# @rbs &block: ^(untyped) -> void -- Event handler
|
|
118
|
+
# @rbs return: String -- Listener ID
|
|
107
119
|
def once(event_name, &block)
|
|
108
120
|
unless BrowserEmittedEvents.values.include?(event_name.to_s)
|
|
109
121
|
raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{BrowserEmittedEvents.values.to_a.join(", ")}")
|
|
@@ -142,7 +154,7 @@ class Puppeteer::Browser
|
|
|
142
154
|
@target_manager.remove_event_listener(*@target_manager_event_listeners)
|
|
143
155
|
end
|
|
144
156
|
|
|
145
|
-
# @return
|
|
157
|
+
# @rbs return: Puppeteer::BrowserRunner::BrowserProcess? -- Browser process handle
|
|
146
158
|
def process
|
|
147
159
|
@process
|
|
148
160
|
end
|
|
@@ -151,33 +163,52 @@ class Puppeteer::Browser
|
|
|
151
163
|
@target_manager
|
|
152
164
|
end
|
|
153
165
|
|
|
154
|
-
# @return
|
|
166
|
+
# @rbs return: Puppeteer::BrowserContext -- New incognito browser context
|
|
155
167
|
def create_incognito_browser_context
|
|
156
168
|
result = @connection.send_message('Target.createBrowserContext')
|
|
157
169
|
browser_context_id = result['browserContextId']
|
|
158
170
|
@contexts[browser_context_id] = Puppeteer::BrowserContext.new(@connection, self, browser_context_id)
|
|
159
171
|
end
|
|
160
172
|
|
|
173
|
+
# @rbs proxy_server: String? -- Proxy server for new context
|
|
174
|
+
# @rbs proxy_bypass_list: Array[String]? -- Proxy bypass list
|
|
175
|
+
# @rbs download_behavior: Hash[Symbol | String, untyped]? -- Download behavior options
|
|
176
|
+
# @rbs return: Puppeteer::BrowserContext -- New browser context
|
|
177
|
+
def create_browser_context(proxy_server: nil, proxy_bypass_list: nil, download_behavior: nil)
|
|
178
|
+
params = {
|
|
179
|
+
proxyServer: proxy_server,
|
|
180
|
+
proxyBypassList: proxy_bypass_list&.join(','),
|
|
181
|
+
}.compact
|
|
182
|
+
result = @connection.send_message('Target.createBrowserContext', params)
|
|
183
|
+
browser_context_id = result['browserContextId']
|
|
184
|
+
context = Puppeteer::BrowserContext.new(@connection, self, browser_context_id)
|
|
185
|
+
context.set_download_behavior(download_behavior) if download_behavior
|
|
186
|
+
@contexts[browser_context_id] = context
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @rbs return: Array[Puppeteer::BrowserContext] -- All browser contexts
|
|
161
190
|
def browser_contexts
|
|
162
191
|
[@default_context].concat(@contexts.values)
|
|
163
192
|
end
|
|
164
193
|
|
|
165
|
-
# @return
|
|
194
|
+
# @rbs return: Puppeteer::BrowserContext -- Default browser context
|
|
166
195
|
def default_browser_context
|
|
167
196
|
@default_context
|
|
168
197
|
end
|
|
169
198
|
|
|
170
|
-
# @
|
|
199
|
+
# @rbs context_id: String? -- Browser context ID
|
|
200
|
+
# @rbs return: void -- No return value
|
|
171
201
|
def dispose_context(context_id)
|
|
172
202
|
return unless context_id
|
|
173
203
|
@connection.send_message('Target.disposeBrowserContext', browserContextId: context_id)
|
|
174
204
|
@contexts.delete(context_id)
|
|
175
205
|
end
|
|
176
206
|
|
|
177
|
-
class MissingBrowserContextError <
|
|
207
|
+
class MissingBrowserContextError < Puppeteer::Error ; end
|
|
178
208
|
|
|
179
|
-
# @
|
|
180
|
-
# @
|
|
209
|
+
# @rbs target_info: Puppeteer::Target::TargetInfo -- Target info
|
|
210
|
+
# @rbs session: Puppeteer::CDPSession? -- Attached session
|
|
211
|
+
# @rbs return: Puppeteer::Target -- Created target
|
|
181
212
|
def create_target(target_info, session)
|
|
182
213
|
browser_context_id = target_info.browser_context_id
|
|
183
214
|
context =
|
|
@@ -199,12 +230,13 @@ class Puppeteer::Browser
|
|
|
199
230
|
session_factory: -> (auto_attach_emulated) { @connection.create_session(target_info, auto_attach_emulated: auto_attach_emulated) },
|
|
200
231
|
ignore_https_errors: @ignore_https_errors,
|
|
201
232
|
default_viewport: @default_viewport,
|
|
233
|
+
network_enabled: @network_enabled,
|
|
202
234
|
is_page_target_callback: @is_page_target_callback,
|
|
203
235
|
)
|
|
204
236
|
end
|
|
205
237
|
|
|
206
238
|
private def handle_attached_to_target(target)
|
|
207
|
-
if target.initialized_promise.
|
|
239
|
+
if target.initialized_promise.wait
|
|
208
240
|
emit_event(BrowserEmittedEvents::TargetCreated, target)
|
|
209
241
|
target.browser_context.emit_event(BrowserContextEmittedEvents::TargetCreated, target)
|
|
210
242
|
end
|
|
@@ -213,7 +245,7 @@ class Puppeteer::Browser
|
|
|
213
245
|
private def handle_detached_from_target(target)
|
|
214
246
|
target.ignore_initialize_callback_promise
|
|
215
247
|
target.closed_callback
|
|
216
|
-
if target.initialized_promise.
|
|
248
|
+
if target.initialized_promise.wait
|
|
217
249
|
emit_event(BrowserEmittedEvents::TargetDestroyed, target)
|
|
218
250
|
target.browser_context.emit_event(BrowserContextEmittedEvents::TargetDestroyed, target)
|
|
219
251
|
end
|
|
@@ -233,20 +265,21 @@ class Puppeteer::Browser
|
|
|
233
265
|
emit_event('targetdiscovered', target_info)
|
|
234
266
|
end
|
|
235
267
|
|
|
236
|
-
# @return
|
|
268
|
+
# @rbs return: String -- WebSocket endpoint URL
|
|
237
269
|
def ws_endpoint
|
|
238
270
|
@connection.url
|
|
239
271
|
end
|
|
240
272
|
|
|
273
|
+
# @rbs return: Puppeteer::Page -- New page in default context
|
|
241
274
|
def new_page
|
|
242
275
|
@default_context.new_page
|
|
243
276
|
end
|
|
244
277
|
|
|
245
|
-
class MissingTargetError <
|
|
246
|
-
class CreatePageError <
|
|
278
|
+
class MissingTargetError < Puppeteer::Error ; end
|
|
279
|
+
class CreatePageError < Puppeteer::Error ; end
|
|
247
280
|
|
|
248
|
-
# @
|
|
249
|
-
# @return
|
|
281
|
+
# @rbs context_id: String? -- Browser context ID
|
|
282
|
+
# @rbs return: Puppeteer::Page -- Created page
|
|
250
283
|
def create_page_in_context(context_id)
|
|
251
284
|
create_target_params = {
|
|
252
285
|
url: 'about:blank',
|
|
@@ -258,7 +291,7 @@ class Puppeteer::Browser
|
|
|
258
291
|
unless target
|
|
259
292
|
raise MissingTargetError.new("Missing target for page (id = #{target_id})")
|
|
260
293
|
end
|
|
261
|
-
unless target.initialized_promise.
|
|
294
|
+
unless target.initialized_promise.wait
|
|
262
295
|
raise CreatePageError.new("Failed to create target for page (id = #{target_id})")
|
|
263
296
|
end
|
|
264
297
|
page = target.page
|
|
@@ -270,12 +303,14 @@ class Puppeteer::Browser
|
|
|
270
303
|
|
|
271
304
|
# All active targets inside the Browser. In case of multiple browser contexts, returns
|
|
272
305
|
# an array with all the targets in all browser contexts.
|
|
306
|
+
# @rbs return: Array[Puppeteer::Target] -- Active targets
|
|
273
307
|
def targets
|
|
274
308
|
@target_manager.available_targets.values.select { |target| target.initialized? }
|
|
275
309
|
end
|
|
276
310
|
|
|
277
311
|
|
|
278
312
|
# The target associated with the browser.
|
|
313
|
+
# @rbs return: Puppeteer::Target -- Browser target
|
|
279
314
|
def target
|
|
280
315
|
targets.find { |target| target.type == 'browser' } or raise 'Browser target is not found'
|
|
281
316
|
end
|
|
@@ -285,29 +320,30 @@ class Puppeteer::Browser
|
|
|
285
320
|
@target_manager.available_targets[target_id]
|
|
286
321
|
end
|
|
287
322
|
|
|
288
|
-
# @
|
|
289
|
-
# @
|
|
323
|
+
# @rbs predicate: Proc -- Predicate for target matching
|
|
324
|
+
# @rbs timeout: Numeric? -- Timeout in milliseconds
|
|
325
|
+
# @rbs return: Puppeteer::Target -- Matching target
|
|
290
326
|
def wait_for_target(predicate:, timeout: nil)
|
|
291
327
|
timeout_helper = Puppeteer::TimeoutHelper.new('target', timeout_ms: timeout, default_timeout_ms: 30000)
|
|
292
328
|
existing_target = targets.find { |target| predicate.call(target) }
|
|
293
329
|
return existing_target if existing_target
|
|
294
330
|
|
|
295
331
|
event_listening_ids = []
|
|
296
|
-
target_promise =
|
|
332
|
+
target_promise = Async::Promise.new
|
|
297
333
|
event_listening_ids << add_event_listener(BrowserEmittedEvents::TargetCreated) do |target|
|
|
298
334
|
if predicate.call(target)
|
|
299
|
-
target_promise.
|
|
335
|
+
target_promise.resolve(target)
|
|
300
336
|
end
|
|
301
337
|
end
|
|
302
338
|
event_listening_ids << add_event_listener(BrowserEmittedEvents::TargetChanged) do |target|
|
|
303
339
|
if predicate.call(target)
|
|
304
|
-
target_promise.
|
|
340
|
+
target_promise.resolve(target)
|
|
305
341
|
end
|
|
306
342
|
end
|
|
307
343
|
|
|
308
344
|
begin
|
|
309
345
|
timeout_helper.with_timeout do
|
|
310
|
-
target_promise.
|
|
346
|
+
target_promise.wait
|
|
311
347
|
end
|
|
312
348
|
ensure
|
|
313
349
|
remove_event_listener(*event_listening_ids)
|
|
@@ -316,43 +352,49 @@ class Puppeteer::Browser
|
|
|
316
352
|
|
|
317
353
|
# @!method async_wait_for_target(predicate:, timeout: nil)
|
|
318
354
|
#
|
|
319
|
-
# @param predicate [Proc(Puppeteer::Target -> Boolean)]
|
|
320
355
|
define_async_method :async_wait_for_target
|
|
321
356
|
|
|
322
|
-
# @return
|
|
357
|
+
# @rbs return: Array[Puppeteer::Page] -- All pages across contexts
|
|
323
358
|
def pages
|
|
324
359
|
browser_contexts.flat_map(&:pages)
|
|
325
360
|
end
|
|
326
361
|
|
|
327
|
-
# @return
|
|
362
|
+
# @rbs return: String -- Browser version string
|
|
328
363
|
def version
|
|
329
364
|
Version.fetch(@connection).product
|
|
330
365
|
end
|
|
331
366
|
|
|
332
|
-
# @return
|
|
367
|
+
# @rbs return: String -- Browser user agent string
|
|
333
368
|
def user_agent
|
|
334
369
|
Version.fetch(@connection).user_agent
|
|
335
370
|
end
|
|
336
371
|
|
|
372
|
+
# @rbs return: void -- No return value
|
|
337
373
|
def close
|
|
338
374
|
@close_callback.call
|
|
339
375
|
disconnect
|
|
340
376
|
end
|
|
341
377
|
|
|
378
|
+
# @rbs return: void -- No return value
|
|
342
379
|
def disconnect
|
|
343
380
|
@target_manager.dispose
|
|
344
381
|
@connection.dispose
|
|
345
382
|
end
|
|
346
383
|
|
|
384
|
+
# @rbs return: bool -- Whether the browser is connected
|
|
347
385
|
def connected?
|
|
348
386
|
!@connection.closed?
|
|
349
387
|
end
|
|
350
388
|
|
|
351
389
|
class Version
|
|
390
|
+
# @rbs connection: Puppeteer::Connection -- CDP connection
|
|
391
|
+
# @rbs return: Puppeteer::Browser::Version -- Browser version info
|
|
352
392
|
def self.fetch(connection)
|
|
353
393
|
new(connection.send_message('Browser.getVersion'))
|
|
354
394
|
end
|
|
355
395
|
|
|
396
|
+
# @rbs hash: Hash[String, String] -- Version payload
|
|
397
|
+
# @rbs return: void -- No return value
|
|
356
398
|
def initialize(hash)
|
|
357
399
|
@protocol_version = hash['protocolVersion']
|
|
358
400
|
@product = hash['product']
|
|
@@ -12,7 +12,11 @@ class Puppeteer::BrowserConnector
|
|
|
12
12
|
# @return [Puppeteer::Browser]
|
|
13
13
|
def connect_to_browser
|
|
14
14
|
version = Puppeteer::Browser::Version.fetch(connection)
|
|
15
|
-
|
|
15
|
+
product_name = version.product.to_s.downcase
|
|
16
|
+
if product_name.include?('firefox')
|
|
17
|
+
raise Puppeteer::Error.new('Firefox CDP support has been removed. Use puppeteer-bidi for Firefox automation.')
|
|
18
|
+
end
|
|
19
|
+
product = 'chrome'
|
|
16
20
|
|
|
17
21
|
result = connection.send_message('Target.getBrowserContexts')
|
|
18
22
|
browser_context_ids = result['browserContextIds']
|
|
@@ -23,6 +27,7 @@ class Puppeteer::BrowserConnector
|
|
|
23
27
|
context_ids: browser_context_ids,
|
|
24
28
|
ignore_https_errors: @browser_options.ignore_https_errors?,
|
|
25
29
|
default_viewport: @browser_options.default_viewport,
|
|
30
|
+
network_enabled: @browser_options.network_enabled,
|
|
26
31
|
process: nil,
|
|
27
32
|
close_callback: -> { connection.send_message('Browser.close') },
|
|
28
33
|
target_filter_callback: @browser_options.target_filter,
|
|
@@ -46,7 +51,12 @@ class Puppeteer::BrowserConnector
|
|
|
46
51
|
# @return [Puppeteer::Connection]
|
|
47
52
|
private def connect_with_browser_ws_endpoint(browser_ws_endpoint)
|
|
48
53
|
transport = Puppeteer::WebSocketTransport.create(browser_ws_endpoint)
|
|
49
|
-
Puppeteer::Connection.new(
|
|
54
|
+
Puppeteer::Connection.new(
|
|
55
|
+
browser_ws_endpoint,
|
|
56
|
+
transport,
|
|
57
|
+
@browser_options.slow_mo,
|
|
58
|
+
protocol_timeout: @browser_options.protocol_timeout,
|
|
59
|
+
)
|
|
50
60
|
end
|
|
51
61
|
|
|
52
62
|
# @return [Puppeteer::Connection]
|
|
@@ -62,6 +72,11 @@ class Puppeteer::BrowserConnector
|
|
|
62
72
|
|
|
63
73
|
# @return [Puppeteer::Connection]
|
|
64
74
|
private def connect_with_transport(transport)
|
|
65
|
-
Puppeteer::Connection.new(
|
|
75
|
+
Puppeteer::Connection.new(
|
|
76
|
+
'',
|
|
77
|
+
transport,
|
|
78
|
+
@browser_options.slow_mo,
|
|
79
|
+
protocol_timeout: @browser_options.protocol_timeout,
|
|
80
|
+
)
|
|
66
81
|
end
|
|
67
82
|
end
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
require 'async/semaphore'
|
|
2
|
+
require 'uri'
|
|
3
|
+
|
|
1
4
|
class Puppeteer::BrowserContext
|
|
2
5
|
include Puppeteer::EventCallbackable
|
|
3
6
|
using Puppeteer::DefineAsyncMethod
|
|
@@ -9,10 +12,39 @@ class Puppeteer::BrowserContext
|
|
|
9
12
|
@connection = connection
|
|
10
13
|
@browser = browser
|
|
11
14
|
@id = context_id
|
|
15
|
+
@closed = false
|
|
16
|
+
@screenshot_semaphore = nil
|
|
17
|
+
@screenshot_operations_count = 0
|
|
12
18
|
end
|
|
13
19
|
|
|
14
20
|
attr_reader :id
|
|
15
21
|
|
|
22
|
+
class ScreenshotGuard
|
|
23
|
+
def initialize(semaphore, on_release: nil)
|
|
24
|
+
@semaphore = semaphore
|
|
25
|
+
@on_release = on_release
|
|
26
|
+
@released = false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def release
|
|
30
|
+
return if @released
|
|
31
|
+
|
|
32
|
+
@released = true
|
|
33
|
+
@semaphore.release
|
|
34
|
+
@on_release&.call
|
|
35
|
+
end
|
|
36
|
+
alias_method :close, :release
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ==(other)
|
|
40
|
+
other = other.__getobj__ if other.is_a?(Puppeteer::ReactorRunner::Proxy)
|
|
41
|
+
return true if equal?(other)
|
|
42
|
+
return false unless other.is_a?(Puppeteer::BrowserContext)
|
|
43
|
+
return false if @id.nil? || other.id.nil?
|
|
44
|
+
|
|
45
|
+
@id == other.id
|
|
46
|
+
end
|
|
47
|
+
|
|
16
48
|
# @param event_name [Symbol] either of :disconnected, :targetcreated, :targetchanged, :targetdestroyed
|
|
17
49
|
def on(event_name, &block)
|
|
18
50
|
unless BrowserContextEmittedEvents.values.include?(event_name.to_s)
|
|
@@ -36,6 +68,25 @@ class Puppeteer::BrowserContext
|
|
|
36
68
|
@browser.targets.select { |target| target.browser_context == self }
|
|
37
69
|
end
|
|
38
70
|
|
|
71
|
+
def start_screenshot
|
|
72
|
+
semaphore = @screenshot_semaphore || Async::Semaphore.new(1)
|
|
73
|
+
@screenshot_semaphore = semaphore
|
|
74
|
+
@screenshot_operations_count += 1
|
|
75
|
+
semaphore.acquire
|
|
76
|
+
ScreenshotGuard.new(semaphore, on_release: lambda {
|
|
77
|
+
@screenshot_operations_count -= 1
|
|
78
|
+
@screenshot_semaphore = nil if @screenshot_operations_count.zero?
|
|
79
|
+
})
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def wait_for_screenshot_operations
|
|
83
|
+
semaphore = @screenshot_semaphore
|
|
84
|
+
return nil unless semaphore
|
|
85
|
+
|
|
86
|
+
semaphore.acquire
|
|
87
|
+
ScreenshotGuard.new(semaphore)
|
|
88
|
+
end
|
|
89
|
+
|
|
39
90
|
# @param predicate [Proc(Puppeteer::Target -> Boolean)]
|
|
40
91
|
# @return [Puppeteer::Target]
|
|
41
92
|
def wait_for_target(predicate:, timeout: nil)
|
|
@@ -51,16 +102,21 @@ class Puppeteer::BrowserContext
|
|
|
51
102
|
define_async_method :async_wait_for_target
|
|
52
103
|
|
|
53
104
|
# @return {!Promise<!Array<!Puppeteer.Page>>}
|
|
54
|
-
def pages
|
|
105
|
+
def pages(include_all: false)
|
|
55
106
|
targets.select { |target|
|
|
56
|
-
target.type == 'page' ||
|
|
57
|
-
|
|
107
|
+
target.type == 'page' ||
|
|
108
|
+
((target.type == 'other' || include_all) && @browser.is_page_target_callback&.call(target.target_info))
|
|
109
|
+
}.map(&:page).compact
|
|
58
110
|
end
|
|
59
111
|
|
|
60
112
|
def incognito?
|
|
61
113
|
!!@id
|
|
62
114
|
end
|
|
63
115
|
|
|
116
|
+
def closed?
|
|
117
|
+
@closed || !@browser.browser_contexts.include?(self)
|
|
118
|
+
end
|
|
119
|
+
|
|
64
120
|
WEB_PERMISSION_TO_PROTOCOL = {
|
|
65
121
|
'geolocation' => 'geolocation',
|
|
66
122
|
'midi' => 'midi',
|
|
@@ -105,9 +161,23 @@ class Puppeteer::BrowserContext
|
|
|
105
161
|
end
|
|
106
162
|
end
|
|
107
163
|
|
|
164
|
+
# @param download_behavior [Hash]
|
|
165
|
+
def set_download_behavior(download_behavior)
|
|
166
|
+
behavior = hash_value(download_behavior, 'policy')
|
|
167
|
+
download_path = hash_value(download_behavior, 'downloadPath', 'download_path')
|
|
168
|
+
@connection.send_message('Browser.setDownloadBehavior', {
|
|
169
|
+
behavior: behavior,
|
|
170
|
+
downloadPath: download_path,
|
|
171
|
+
browserContextId: @id,
|
|
172
|
+
}.compact)
|
|
173
|
+
end
|
|
174
|
+
|
|
108
175
|
# @return [Future<Puppeteer::Page>]
|
|
109
176
|
def new_page
|
|
177
|
+
guard = wait_for_screenshot_operations
|
|
110
178
|
@browser.create_page_in_context(@id)
|
|
179
|
+
ensure
|
|
180
|
+
guard&.release
|
|
111
181
|
end
|
|
112
182
|
|
|
113
183
|
# @return [Browser]
|
|
@@ -120,5 +190,128 @@ class Puppeteer::BrowserContext
|
|
|
120
190
|
raise 'Non-incognito profiles cannot be closed!'
|
|
121
191
|
end
|
|
122
192
|
@browser.dispose_context(@id)
|
|
193
|
+
@closed = true
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# @return [Array<Hash>]
|
|
197
|
+
def cookies
|
|
198
|
+
params = { browserContextId: @id }.compact
|
|
199
|
+
response = @connection.send_message('Storage.getCookies', params)
|
|
200
|
+
response.fetch('cookies', []).map do |cookie|
|
|
201
|
+
normalized = cookie.dup
|
|
202
|
+
partition_key = cookie['partitionKey']
|
|
203
|
+
if partition_key
|
|
204
|
+
normalized['partitionKey'] = convert_partition_key_from_cdp(partition_key)
|
|
205
|
+
end
|
|
206
|
+
normalized['sameParty'] = cookie['sameParty'] || false
|
|
207
|
+
normalized
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# @param cookies [Array<Hash>]
|
|
212
|
+
def set_cookie(*cookies)
|
|
213
|
+
items = cookies.map do |cookie|
|
|
214
|
+
normalized = normalize_cookie_hash(cookie)
|
|
215
|
+
partition_key = normalized.delete('partitionKey') || normalized.delete('partition_key')
|
|
216
|
+
normalized['partitionKey'] = convert_partition_key_for_cdp(partition_key) if partition_key
|
|
217
|
+
normalized
|
|
218
|
+
end
|
|
219
|
+
@connection.send_message('Storage.setCookies', {
|
|
220
|
+
browserContextId: @id,
|
|
221
|
+
cookies: items,
|
|
222
|
+
}.compact)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# @param cookies [Array<Hash>]
|
|
226
|
+
def delete_cookie(*cookies)
|
|
227
|
+
items = cookies.map do |cookie|
|
|
228
|
+
normalized = normalize_cookie_hash(cookie)
|
|
229
|
+
normalized['expires'] = 1
|
|
230
|
+
normalized
|
|
231
|
+
end
|
|
232
|
+
set_cookie(*items)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# @param filters [Array<Hash>]
|
|
236
|
+
def delete_matching_cookies(*filters)
|
|
237
|
+
cookies_to_delete = cookies.select do |cookie|
|
|
238
|
+
filters.any? do |filter|
|
|
239
|
+
filter_name = hash_value(filter, 'name')
|
|
240
|
+
next false unless filter_name == cookie['name']
|
|
241
|
+
|
|
242
|
+
filter_domain = hash_value(filter, 'domain')
|
|
243
|
+
next true if filter_domain && filter_domain == cookie['domain']
|
|
244
|
+
|
|
245
|
+
filter_path = hash_value(filter, 'path')
|
|
246
|
+
next true if filter_path && filter_path == cookie['path']
|
|
247
|
+
|
|
248
|
+
filter_partition_key = hash_value(filter, 'partitionKey', 'partition_key')
|
|
249
|
+
if filter_partition_key && cookie['partitionKey']
|
|
250
|
+
if cookie['partitionKey'].is_a?(String)
|
|
251
|
+
raise Puppeteer::Error.new('Unexpected string partition key')
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
cookie_partition_source_origin = hash_value(cookie['partitionKey'], 'sourceOrigin', 'source_origin')
|
|
255
|
+
filter_partition_source_origin =
|
|
256
|
+
if filter_partition_key.is_a?(String)
|
|
257
|
+
filter_partition_key
|
|
258
|
+
else
|
|
259
|
+
hash_value(filter_partition_key, 'sourceOrigin', 'source_origin')
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
next true if filter_partition_source_origin == cookie_partition_source_origin
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
filter_url = hash_value(filter, 'url')
|
|
266
|
+
if filter_url
|
|
267
|
+
url = URI.parse(filter_url)
|
|
268
|
+
url_path = url.path.to_s.empty? ? '/' : url.path
|
|
269
|
+
next true if url.hostname == cookie['domain'] && url_path == cookie['path']
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
true
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
delete_cookie(*cookies_to_delete)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
private def normalize_cookie_hash(cookie)
|
|
280
|
+
cookie.each_with_object({}) do |(key, value), normalized|
|
|
281
|
+
normalized[key.to_s] = value
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private def hash_value(hash, *keys)
|
|
286
|
+
return nil unless hash
|
|
287
|
+
|
|
288
|
+
keys.each do |key|
|
|
289
|
+
return hash[key] if hash.key?(key)
|
|
290
|
+
return hash[key.to_sym] if key.is_a?(String) && hash.key?(key.to_sym)
|
|
291
|
+
return hash[key.to_s] if key.is_a?(Symbol) && hash.key?(key.to_s)
|
|
292
|
+
end
|
|
293
|
+
nil
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
private def convert_partition_key_for_cdp(partition_key)
|
|
297
|
+
return nil if partition_key.nil?
|
|
298
|
+
return { topLevelSite: partition_key, hasCrossSiteAncestor: false } if partition_key.is_a?(String)
|
|
299
|
+
|
|
300
|
+
source_origin = hash_value(partition_key, 'sourceOrigin', 'source_origin')
|
|
301
|
+
has_cross_site_ancestor = hash_value(partition_key, 'hasCrossSiteAncestor', 'has_cross_site_ancestor')
|
|
302
|
+
{
|
|
303
|
+
topLevelSite: source_origin,
|
|
304
|
+
hasCrossSiteAncestor: has_cross_site_ancestor.nil? ? false : has_cross_site_ancestor,
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
private def convert_partition_key_from_cdp(partition_key)
|
|
309
|
+
return nil if partition_key.nil?
|
|
310
|
+
return partition_key if partition_key.is_a?(String)
|
|
311
|
+
|
|
312
|
+
{
|
|
313
|
+
'sourceOrigin' => hash_value(partition_key, 'topLevelSite', 'top_level_site'),
|
|
314
|
+
'hasCrossSiteAncestor' => hash_value(partition_key, 'hasCrossSiteAncestor', 'has_cross_site_ancestor'),
|
|
315
|
+
}.compact
|
|
123
316
|
end
|
|
124
317
|
end
|