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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -3
  3. data/AGENTS.md +169 -0
  4. data/CLAUDE/README.md +41 -0
  5. data/CLAUDE/architecture.md +253 -0
  6. data/CLAUDE/cdp_protocol.md +230 -0
  7. data/CLAUDE/concurrency.md +216 -0
  8. data/CLAUDE/porting_puppeteer.md +575 -0
  9. data/CLAUDE/rbs_type_checking.md +101 -0
  10. data/CLAUDE/spec_migration_plans.md +1041 -0
  11. data/CLAUDE/testing.md +278 -0
  12. data/CLAUDE.md +242 -0
  13. data/README.md +8 -0
  14. data/Rakefile +7 -0
  15. data/Steepfile +28 -0
  16. data/docs/api_coverage.md +105 -56
  17. data/lib/puppeteer/aria_query_handler.rb +3 -2
  18. data/lib/puppeteer/async_utils.rb +214 -0
  19. data/lib/puppeteer/browser.rb +98 -56
  20. data/lib/puppeteer/browser_connector.rb +18 -3
  21. data/lib/puppeteer/browser_context.rb +196 -3
  22. data/lib/puppeteer/browser_runner.rb +18 -10
  23. data/lib/puppeteer/cdp_session.rb +67 -23
  24. data/lib/puppeteer/chrome_target_manager.rb +65 -40
  25. data/lib/puppeteer/connection.rb +55 -36
  26. data/lib/puppeteer/console_message.rb +9 -1
  27. data/lib/puppeteer/console_patch.rb +47 -0
  28. data/lib/puppeteer/css_coverage.rb +5 -3
  29. data/lib/puppeteer/custom_query_handler.rb +80 -33
  30. data/lib/puppeteer/define_async_method.rb +31 -37
  31. data/lib/puppeteer/dialog.rb +47 -14
  32. data/lib/puppeteer/element_handle.rb +231 -62
  33. data/lib/puppeteer/emulation_manager.rb +1 -1
  34. data/lib/puppeteer/env.rb +1 -1
  35. data/lib/puppeteer/errors.rb +25 -2
  36. data/lib/puppeteer/event_callbackable.rb +15 -0
  37. data/lib/puppeteer/events.rb +4 -0
  38. data/lib/puppeteer/execution_context.rb +148 -3
  39. data/lib/puppeteer/file_chooser.rb +6 -0
  40. data/lib/puppeteer/frame.rb +162 -91
  41. data/lib/puppeteer/frame_manager.rb +69 -48
  42. data/lib/puppeteer/http_request.rb +114 -38
  43. data/lib/puppeteer/http_response.rb +24 -7
  44. data/lib/puppeteer/isolated_world.rb +64 -41
  45. data/lib/puppeteer/js_coverage.rb +5 -3
  46. data/lib/puppeteer/js_handle.rb +58 -16
  47. data/lib/puppeteer/keyboard.rb +30 -17
  48. data/lib/puppeteer/launcher/browser_options.rb +3 -1
  49. data/lib/puppeteer/launcher/chrome.rb +8 -5
  50. data/lib/puppeteer/launcher/launch_options.rb +7 -2
  51. data/lib/puppeteer/launcher.rb +4 -8
  52. data/lib/puppeteer/lifecycle_watcher.rb +38 -22
  53. data/lib/puppeteer/mouse.rb +273 -64
  54. data/lib/puppeteer/network_event_manager.rb +7 -0
  55. data/lib/puppeteer/network_manager.rb +393 -112
  56. data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
  57. data/lib/puppeteer/page.rb +568 -226
  58. data/lib/puppeteer/puppeteer.rb +171 -64
  59. data/lib/puppeteer/query_handler_manager.rb +112 -16
  60. data/lib/puppeteer/reactor_runner.rb +247 -0
  61. data/lib/puppeteer/remote_object.rb +127 -47
  62. data/lib/puppeteer/target.rb +74 -27
  63. data/lib/puppeteer/task_manager.rb +3 -1
  64. data/lib/puppeteer/timeout_helper.rb +6 -10
  65. data/lib/puppeteer/touch_handle.rb +39 -0
  66. data/lib/puppeteer/touch_screen.rb +72 -22
  67. data/lib/puppeteer/tracing.rb +3 -3
  68. data/lib/puppeteer/version.rb +1 -1
  69. data/lib/puppeteer/wait_task.rb +264 -101
  70. data/lib/puppeteer/web_socket.rb +2 -2
  71. data/lib/puppeteer/web_socket_transport.rb +91 -27
  72. data/lib/puppeteer/web_worker.rb +175 -0
  73. data/lib/puppeteer.rb +20 -4
  74. data/puppeteer-ruby.gemspec +15 -11
  75. data/sig/_external.rbs +8 -0
  76. data/sig/_supplementary.rbs +314 -0
  77. data/sig/puppeteer/browser.rbs +166 -0
  78. data/sig/puppeteer/cdp_session.rbs +64 -0
  79. data/sig/puppeteer/dialog.rbs +41 -0
  80. data/sig/puppeteer/element_handle.rbs +305 -0
  81. data/sig/puppeteer/execution_context.rbs +87 -0
  82. data/sig/puppeteer/frame.rbs +226 -0
  83. data/sig/puppeteer/http_request.rbs +214 -0
  84. data/sig/puppeteer/http_response.rbs +89 -0
  85. data/sig/puppeteer/js_handle.rbs +64 -0
  86. data/sig/puppeteer/keyboard.rbs +40 -0
  87. data/sig/puppeteer/mouse.rbs +113 -0
  88. data/sig/puppeteer/page.rbs +515 -0
  89. data/sig/puppeteer/puppeteer.rbs +98 -0
  90. data/sig/puppeteer/remote_object.rbs +78 -0
  91. data/sig/puppeteer/touch_handle.rbs +21 -0
  92. data/sig/puppeteer/touch_screen.rbs +35 -0
  93. data/sig/puppeteer/web_worker.rbs +83 -0
  94. metadata +116 -45
  95. data/CHANGELOG.md +0 -397
  96. data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
  97. data/lib/puppeteer/firefox_target_manager.rb +0 -157
  98. data/lib/puppeteer/launcher/firefox.rb +0 -453
@@ -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
- # @param product [String|nil] 'chrome' or 'firefox'
11
- # @param {!Puppeteer.Connection} connection
12
- # @param {!Array<string>} contextIds
13
- # @param {boolean} ignoreHTTPSErrors
14
- # @param {?Puppeteer.Viewport} defaultViewport
15
- # @param process [Puppeteer::BrowserRunner::BrowserProcess|NilClass]
16
- # @param {function()=} closeCallback
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
- # @param product [String|nil] 'chrome' or 'firefox'
42
- # @param {!Puppeteer.Connection} connection
43
- # @param {!Array<string>} contextIds
44
- # @param {boolean} ignoreHTTPSErrors
45
- # @param {?Puppeteer.Viewport} defaultViewport
46
- # @param {?Puppeteer.ChildProcess} process
47
- # @param {(function():Promise)=} closeCallback
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 || 'chrome'
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
- if @product == 'firefox'
73
- @target_manager = Puppeteer::FirefoxTargetManager.new(
74
- connection: connection,
75
- target_factory: method(:create_target),
76
- target_filter_callback: @target_filter_callback,
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
- # @param event_name [Symbol] either of :disconnected, :targetcreated, :targetchanged, :targetdestroyed
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
- # @param event_name [Symbol]
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 [Puppeteer::BrowserRunner::BrowserProcess]
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 [Puppeteer::BrowserContext]
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 [Puppeteer::BrowserContext]
194
+ # @rbs return: Puppeteer::BrowserContext -- Default browser context
166
195
  def default_browser_context
167
196
  @default_context
168
197
  end
169
198
 
170
- # @param context_id [String]
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 < StandardError ; end
207
+ class MissingBrowserContextError < Puppeteer::Error ; end
178
208
 
179
- # @param target_info [Puppeteer::Target::TargetInfo]
180
- # @param session [CDPSession|nil]
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.value!
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.value!
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 [String]
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 < StandardError ; end
246
- class CreatePageError < StandardError ; end
278
+ class MissingTargetError < Puppeteer::Error ; end
279
+ class CreatePageError < Puppeteer::Error ; end
247
280
 
248
- # @param {?string} contextId
249
- # @return {!Promise<!Puppeteer.Page>}
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.value!
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
- # @param predicate [Proc(Puppeteer::Target -> Boolean)]
289
- # @return [Puppeteer::Target]
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 = resolvable_future
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.fulfill(target)
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.fulfill(target)
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.value!
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 {!Promise<!Array<!Puppeteer.Page>>}
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 [String]
362
+ # @rbs return: String -- Browser version string
328
363
  def version
329
364
  Version.fetch(@connection).product
330
365
  end
331
366
 
332
- # @return [String]
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
- product = version.product.downcase.include?('firefox') ? 'firefox' : 'chrome'
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(browser_ws_endpoint, transport, @browser_options.slow_mo)
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('', transport, @browser_options.slow_mo)
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' || (target.type == 'other' && @browser.is_page_target_callback&.call(target.target_info))
57
- }.map(&:page).reject { |page| !page }
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