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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/docs/api_coverage.md +16 -15
  3. data/lib/puppeteer/accessibility.rb +300 -0
  4. data/lib/puppeteer/browser.rb +107 -8
  5. data/lib/puppeteer/browser_connector.rb +1 -0
  6. data/lib/puppeteer/browser_context.rb +0 -1
  7. data/lib/puppeteer/cdp_session.rb +29 -8
  8. data/lib/puppeteer/chrome_target_manager.rb +145 -34
  9. data/lib/puppeteer/connection.rb +19 -0
  10. data/lib/puppeteer/coverage.rb +5 -0
  11. data/lib/puppeteer/css_coverage.rb +4 -0
  12. data/lib/puppeteer/dialog.rb +5 -0
  13. data/lib/puppeteer/element_handle.rb +2 -3
  14. data/lib/puppeteer/emulation_manager.rb +70 -9
  15. data/lib/puppeteer/events.rb +2 -0
  16. data/lib/puppeteer/execution_context.rb +7 -0
  17. data/lib/puppeteer/extension.rb +20 -6
  18. data/lib/puppeteer/frame.rb +7 -3
  19. data/lib/puppeteer/frame_manager.rb +71 -26
  20. data/lib/puppeteer/http_response.rb +15 -1
  21. data/lib/puppeteer/isolated_world.rb +10 -4
  22. data/lib/puppeteer/js_coverage.rb +4 -0
  23. data/lib/puppeteer/keyboard.rb +6 -0
  24. data/lib/puppeteer/launcher/browser_options.rb +13 -1
  25. data/lib/puppeteer/launcher/chrome.rb +48 -4
  26. data/lib/puppeteer/lifecycle_watcher.rb +1 -6
  27. data/lib/puppeteer/locators.rb +54 -26
  28. data/lib/puppeteer/mouse.rb +12 -24
  29. data/lib/puppeteer/network_manager.rb +16 -4
  30. data/lib/puppeteer/page.rb +259 -33
  31. data/lib/puppeteer/puppeteer.rb +6 -5
  32. data/lib/puppeteer/screen_recorder.rb +269 -0
  33. data/lib/puppeteer/target.rb +7 -3
  34. data/lib/puppeteer/touch_screen.rb +6 -0
  35. data/lib/puppeteer/tracing.rb +4 -0
  36. data/lib/puppeteer/version.rb +2 -2
  37. data/lib/puppeteer/wait_task.rb +1 -2
  38. data/lib/puppeteer/web_mcp.rb +227 -0
  39. data/lib/puppeteer/web_worker.rb +135 -9
  40. data/lib/puppeteer.rb +11 -1
  41. data/puppeteer-ruby.gemspec +1 -0
  42. data/sig/_supplementary.rbs +33 -1
  43. data/sig/puppeteer/accessibility.rbs +73 -0
  44. data/sig/puppeteer/browser.rbs +27 -3
  45. data/sig/puppeteer/cdp_session.rbs +4 -0
  46. data/sig/puppeteer/dialog.rbs +3 -0
  47. data/sig/puppeteer/element_handle.rbs +1 -2
  48. data/sig/puppeteer/execution_context.rbs +5 -0
  49. data/sig/puppeteer/extension.rbs +4 -0
  50. data/sig/puppeteer/frame.rbs +4 -2
  51. data/sig/puppeteer/http_response.rbs +4 -0
  52. data/sig/puppeteer/keyboard.rbs +4 -0
  53. data/sig/puppeteer/locators.rbs +3 -4
  54. data/sig/puppeteer/mouse.rbs +5 -6
  55. data/sig/puppeteer/page.rbs +48 -4
  56. data/sig/puppeteer/puppeteer.rbs +4 -5
  57. data/sig/puppeteer/screen_recorder.rbs +48 -0
  58. data/sig/puppeteer/touch_screen.rbs +4 -0
  59. data/sig/puppeteer/web_mcp.rbs +112 -0
  60. data/sig/puppeteer/web_worker.rbs +39 -0
  61. metadata +22 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70526928098a3ab1bcf1718b6110a6620188a30c77419a04181cb746acf4ed15
4
- data.tar.gz: 073e09f7dd759b1b2b2d268ec3021cab96c3e56df93353343fd6ded898556d7b
3
+ metadata.gz: b9b4a8108523041a54e1d2e08af61b9ce13e8416451d321841e31e3ec609161b
4
+ data.tar.gz: 5075602b5392fa17b0d87cb566229d4ef31ef33e336b7d3c030fc71bea370b27
5
5
  SHA512:
6
- metadata.gz: a222036f53e7552d12a8340850369aeed45277f44f1914984ff91168131fbc8ad0a93adf0a3ffe07b98e0f5038a14ee2da6239685bdbf297883bb2702d31df86
7
- data.tar.gz: 57c5a117800a1b814d77abd3ad41d9b25487b08993e1752327dbac49a3e80474c612a52dd3141fd1025dd4fc96e5758c199a82de470abba73e290a5316db0516
6
+ metadata.gz: 2ba3f194f82a267372759a2c0cf25fb4dd3175513c68d6a463fb9b68cf13fed6ae05bb9941cbc7ce290042687e87e45e980ffe71e3c6bf869f5d367078818bd8
7
+ data.tar.gz: 62b6b3e7dd00894cb5789693c3a75b0003b44260a34273b3d24805c9642c2bb00a0e0e87a42571c6111a5ad3ff84cf5ac81e4fe0bccd339758c9d0d4a004be0e
data/docs/api_coverage.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # API coverages
2
- - Puppeteer version: v24.42.0
3
- - puppeteer-ruby version: 0.51.0
2
+ - Puppeteer version: v25.3.0
3
+ - puppeteer-ruby version: 0.53.0
4
4
 
5
5
  ## Puppeteer
6
6
 
@@ -10,9 +10,9 @@
10
10
  * launch
11
11
  * ~~puppeteer~~
12
12
 
13
- ## ~~Accessibility~~
13
+ ## Accessibility
14
14
 
15
- * ~~snapshot~~
15
+ * snapshot
16
16
 
17
17
  ## Browser
18
18
 
@@ -28,7 +28,6 @@
28
28
  * extensions
29
29
  * ~~getWindowBounds~~
30
30
  * installExtension => `#install_extension`
31
- * isConnected => `#connected?`
32
31
  * newPage => `#new_page`
33
32
  * pages
34
33
  * process
@@ -343,6 +342,7 @@
343
342
  * emulateCPUThrottling => `#emulate_cpu_throttling`
344
343
  * ~~emulateFocusedPage~~
345
344
  * emulateIdleState => `#emulate_idle_state`
345
+ * emulateLocale => `#emulate_locale`
346
346
  * emulateMediaFeatures => `#emulate_media_features`
347
347
  * emulateMediaType => `#emulate_media_type`
348
348
  * emulateNetworkConditions => `#emulate_network_conditions`
@@ -360,7 +360,7 @@
360
360
  * goBack => `#go_back`
361
361
  * goForward => `#go_forward`
362
362
  * goto
363
- * ~~hasDevTools~~
363
+ * hasDevTools => `#has_dev_tools`
364
364
  * hover
365
365
  * isClosed => `#closed?`
366
366
  * isDragInterceptionEnabled => `#drag_interception_enabled?`
@@ -369,14 +369,14 @@
369
369
  * locator
370
370
  * mainFrame => `#main_frame`
371
371
  * metrics
372
- * ~~openDevTools~~
372
+ * openDevTools => `#open_dev_tools`
373
373
  * pdf
374
374
  * queryObjects => `#query_objects`
375
375
  * reload
376
376
  * removeExposedFunction => `#remove_exposed_function`
377
377
  * removeScriptToEvaluateOnNewDocument => `#remove_script_to_evaluate_on_new_document`
378
378
  * ~~resize~~
379
- * ~~screencast~~
379
+ * screencast
380
380
  * screenshot
381
381
  * select
382
382
  * setBypassCSP => `#bypass_csp=`
@@ -423,9 +423,9 @@
423
423
  * ~~extension~~
424
424
  * ~~waitForFunction~~
425
425
 
426
- ## ~~ScreenRecorder~~
426
+ ## ScreenRecorder
427
427
 
428
- * ~~stop~~
428
+ * stop
429
429
 
430
430
  ## ~~SecurityDetails~~
431
431
 
@@ -469,15 +469,15 @@
469
469
  ## ~~UnsupportedOperation~~
470
470
 
471
471
 
472
- ## ~~WebMCP~~
472
+ ## WebMCP
473
473
 
474
- * ~~tools~~
474
+ * tools
475
475
 
476
- ## ~~WebMCPTool~~
476
+ ## WebMCPTool
477
477
 
478
- * ~~execute~~
478
+ * execute
479
479
 
480
- ## ~~WebMCPToolCall~~
480
+ ## WebMCPToolCall
481
481
 
482
482
 
483
483
  ## WebWorker
@@ -486,3 +486,4 @@
486
486
  * evaluate
487
487
  * evaluateHandle => `#evaluate_handle`
488
488
  * url
489
+ * waitForFunction => `#wait_for_function`
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ class Puppeteer::Accessibility
5
+ class SerializedAXNode < Hash #[String, untyped]
6
+ # @rbs values: Hash[String, untyped] -- Serialized node values
7
+ # @rbs element_handle_resolver: Proc -- Resolver for the backing element
8
+ def initialize(values, element_handle_resolver)
9
+ super()
10
+ update(values)
11
+ @element_handle_resolver = element_handle_resolver
12
+ end
13
+
14
+ # @rbs return: Puppeteer::ElementHandle? -- Backing element, if available
15
+ def element_handle
16
+ @element_handle_resolver.call
17
+ end
18
+ end
19
+
20
+ # @rbs frame: Puppeteer::Frame -- Frame whose accessibility tree is inspected
21
+ def initialize(frame)
22
+ @frame = frame
23
+ end
24
+
25
+ # @rbs interesting_only: bool -- Prune uninteresting nodes
26
+ # @rbs include_iframes: bool -- Include descendant iframe trees
27
+ # @rbs root: Puppeteer::ElementHandle? -- Optional root node
28
+ # @rbs return: SerializedAXNode? -- Accessibility snapshot
29
+ def snapshot(interesting_only: true, include_iframes: false, root: nil)
30
+ response = @frame.client.send_message(
31
+ 'Accessibility.getFullAXTree',
32
+ frameId: @frame.id,
33
+ )
34
+ backend_node_id = describe_backend_node(root) if root
35
+ default_root = AXNode.create_tree(@frame, response.fetch('nodes', []))
36
+ return nil unless default_root
37
+
38
+ populate_iframes(default_root, interesting_only: interesting_only) if include_iframes
39
+
40
+ needle = default_root
41
+ if backend_node_id
42
+ needle = default_root.find do |node|
43
+ node.payload['backendDOMNodeId'] == backend_node_id
44
+ end
45
+ end
46
+ return nil unless needle
47
+
48
+ return serialize_tree(needle).first unless interesting_only
49
+
50
+ interesting_nodes = Set.new
51
+ collect_interesting_nodes(interesting_nodes, default_root, false)
52
+ serialize_tree(needle, interesting_nodes).first
53
+ end
54
+
55
+ private def describe_backend_node(root)
56
+ response = @frame.client.send_message(
57
+ 'DOM.describeNode',
58
+ objectId: root.remote_object.object_id_value,
59
+ )
60
+ response.dig('node', 'backendNodeId')
61
+ end
62
+
63
+ private def populate_iframes(root, interesting_only:)
64
+ if root.role == 'Iframe' && root.payload['backendDOMNodeId']
65
+ begin
66
+ handle = @frame.main_world.adopt_backend_node(root.payload['backendDOMNodeId'])
67
+ frame = handle&.content_frame
68
+ root.iframe_snapshot = frame&.accessibility&.snapshot(
69
+ interesting_only: interesting_only,
70
+ include_iframes: true,
71
+ )
72
+ rescue StandardError
73
+ # Frames may detach while their accessibility trees are populated.
74
+ ensure
75
+ handle&.dispose
76
+ end
77
+ end
78
+
79
+ tasks = root.children.map do |child|
80
+ proc { populate_iframes(child, interesting_only: interesting_only) }
81
+ end
82
+ Puppeteer::AsyncUtils.await_promise_all(*tasks) unless tasks.empty?
83
+ end
84
+
85
+ private def serialize_tree(node, interesting_nodes = nil)
86
+ children = node.children.flat_map do |child|
87
+ serialize_tree(child, interesting_nodes)
88
+ end
89
+ return children if interesting_nodes && !interesting_nodes.include?(node)
90
+
91
+ serialized = node.serialize
92
+ serialized['children'] = children unless children.empty?
93
+ if node.iframe_snapshot
94
+ serialized['children'] ||= []
95
+ serialized['children'] << node.iframe_snapshot
96
+ end
97
+ [serialized]
98
+ end
99
+
100
+ private def collect_interesting_nodes(collection, node, inside_control)
101
+ collection << node if node.interesting?(inside_control) || node.iframe_snapshot
102
+ return if node.leaf_node?
103
+
104
+ inside_control ||= node.control?
105
+ node.children.each do |child|
106
+ collect_interesting_nodes(collection, child, inside_control)
107
+ end
108
+ end
109
+
110
+ class AXNode
111
+ CONTROL_ROLES = Set.new(%w[
112
+ button checkbox ColorWell combobox DisclosureTriangle listbox menu
113
+ menubar menuitem menuitemcheckbox menuitemradio radio scrollbar searchbox
114
+ slider spinbutton switch tab textbox tree treeitem
115
+ ]).freeze
116
+ LANDMARK_ROLES = Set.new(%w[
117
+ banner complementary contentinfo form main navigation region search
118
+ ]).freeze
119
+ LEAF_ROLES = Set.new(%w[
120
+ doc-cover graphics-symbol img image Meter scrollbar slider separator progressbar
121
+ ]).freeze
122
+ TEXT_ROLES = Set.new(%w[LineBreak text InlineTextBox StaticText]).freeze
123
+
124
+ # @rbs frame: Puppeteer::Frame -- Owning frame
125
+ # @rbs payload: Hash[String, untyped] -- CDP AX node payload
126
+ def initialize(frame, payload)
127
+ @frame = frame
128
+ @payload = payload
129
+ @children = []
130
+ @iframe_snapshot = nil
131
+ @role = payload.dig('role', 'value') || 'Unknown'
132
+ @ignored = payload['ignored']
133
+ @name = payload.dig('name', 'value') || ''
134
+ @description = payload.dig('description', 'value')
135
+ @richly_editable = false
136
+ @editable = false
137
+ @focusable = false
138
+ @hidden = false
139
+ @busy = false
140
+ @modal = false
141
+ @has_error_message = false
142
+ @has_details = false
143
+ @role_description = nil
144
+ @live = nil
145
+ @cached_has_focusable_child = nil
146
+ read_properties
147
+ end
148
+
149
+ attr_reader :payload, :children, :role
150
+ attr_accessor :iframe_snapshot
151
+
152
+ def find(&predicate)
153
+ return self if predicate.call(self)
154
+
155
+ @children.each do |child|
156
+ result = child.find(&predicate)
157
+ return result if result
158
+ end
159
+ nil
160
+ end
161
+
162
+ def leaf_node?
163
+ return true if @children.empty?
164
+ return true if plain_text_field? || TEXT_ROLES.include?(@role)
165
+ return true if LEAF_ROLES.include?(@role)
166
+ return false if has_focusable_child?
167
+ return true if @role == 'heading' && !@name.empty?
168
+
169
+ false
170
+ end
171
+
172
+ def control?
173
+ CONTROL_ROLES.include?(@role)
174
+ end
175
+
176
+ def landmark?
177
+ LANDMARK_ROLES.include?(@role)
178
+ end
179
+
180
+ def interesting?(inside_control)
181
+ return false if @role == 'Ignored' || @hidden || @ignored
182
+ return true if landmark?
183
+ return true if @focusable || @richly_editable || @busy
184
+ return true if @live && @live != 'off'
185
+ return true if @modal || @has_error_message || @has_details || @role_description
186
+ return true if control?
187
+ return false if inside_control
188
+
189
+ leaf_node? && (!@name.empty? || !@description.to_s.empty?)
190
+ end
191
+
192
+ def serialize
193
+ properties = {}
194
+ @payload.fetch('properties', []).each do |property|
195
+ properties[property['name'].downcase] = property.dig('value', 'value')
196
+ end
197
+ properties['name'] = @payload.dig('name', 'value') if @payload['name']
198
+ properties['value'] = @payload.dig('value', 'value') if @payload['value']
199
+ if @payload['description']
200
+ properties['description'] = @payload.dig('description', 'value')
201
+ end
202
+
203
+ values = {
204
+ 'role' => @role,
205
+ 'backendNodeId' => @payload['backendDOMNodeId'],
206
+ 'loaderId' => @frame.loader_id,
207
+ }
208
+ %w[name value description keyshortcuts roledescription valuetext url].each do |key|
209
+ values[key] = properties[key] if properties.key?(key)
210
+ end
211
+ %w[
212
+ disabled expanded focused modal multiline multiselectable readonly required
213
+ selected busy atomic
214
+ ].each do |key|
215
+ next if key == 'focused' && @role == 'RootWebArea'
216
+ values[key] = !!properties[key] if properties.key?(key)
217
+ end
218
+ %w[checked pressed].each do |key|
219
+ next unless properties.key?(key)
220
+ value = properties[key]
221
+ values[key] = value == 'mixed' ? 'mixed' : value == 'true'
222
+ end
223
+ %w[level valuemax valuemin].each do |key|
224
+ values[key] = properties[key] if properties.key?(key)
225
+ end
226
+ %w[
227
+ autocomplete haspopup invalid orientation live relevant errormessage details
228
+ ].each do |key|
229
+ value = properties[key]
230
+ values[key] = value if value && value != 'false'
231
+ end
232
+ values.delete_if { |_key, value| value.nil? }
233
+
234
+ SerializedAXNode.new(values, method(:element_handle))
235
+ end
236
+
237
+ def self.create_tree(frame, payloads)
238
+ nodes_by_id = {}
239
+ payloads.each do |payload|
240
+ nodes_by_id[payload['nodeId']] = new(frame, payload)
241
+ end
242
+ nodes_by_id.each_value do |node|
243
+ node.payload.fetch('childIds', []).each do |child_id|
244
+ child = nodes_by_id[child_id]
245
+ node.children << child if child
246
+ end
247
+ end
248
+ nodes_by_id.each_value.first
249
+ end
250
+
251
+ private def read_properties
252
+ @payload.fetch('properties', []).each do |property|
253
+ name = property['name']
254
+ value = property.dig('value', 'value')
255
+ case name
256
+ when 'editable'
257
+ @richly_editable = value == 'richtext'
258
+ @editable = true
259
+ when 'focusable' then @focusable = value
260
+ when 'hidden' then @hidden = value
261
+ when 'busy' then @busy = value
262
+ when 'live' then @live = value
263
+ when 'modal' then @modal = value
264
+ when 'roledescription' then @role_description = value
265
+ when 'errormessage' then @has_error_message = true
266
+ when 'details' then @has_details = true
267
+ end
268
+ end
269
+ end
270
+
271
+ private def plain_text_field?
272
+ return false if @richly_editable
273
+ return true if @editable
274
+
275
+ ['textbox', 'searchbox'].include?(@role)
276
+ end
277
+
278
+ private def has_focusable_child?
279
+ if @cached_has_focusable_child.nil?
280
+ @cached_has_focusable_child = @children.any? do |child|
281
+ child.instance_variable_get(:@focusable) || child.send(:has_focusable_child?)
282
+ end
283
+ end
284
+ @cached_has_focusable_child
285
+ end
286
+
287
+ private def element_handle
288
+ backend_node_id = @payload['backendDOMNodeId']
289
+ return nil unless backend_node_id
290
+
291
+ handle = @frame.main_world.adopt_backend_node(backend_node_id)
292
+ element = handle.evaluate_handle(<<~JAVASCRIPT)
293
+ node => node.nodeType === Node.TEXT_NODE ? node.parentElement : node
294
+ JAVASCRIPT
295
+ element.as_element
296
+ ensure
297
+ handle&.dispose
298
+ end
299
+ end
300
+ end
@@ -16,6 +16,7 @@ class Puppeteer::Browser
16
16
  # @rbs network_enabled: bool -- Whether network events are enabled
17
17
  # @rbs issues_enabled: bool -- Whether issues events are enabled
18
18
  # @rbs block_list: Array[String]? -- URL block list patterns
19
+ # @rbs allow_list: Array[String]? -- URL allow list patterns
19
20
  # @rbs process: Puppeteer::BrowserRunner::BrowserProcess? -- Browser process handle
20
21
  # @rbs close_callback: Proc -- Close callback
21
22
  # @rbs target_filter_callback: Proc? -- Target filter callback
@@ -29,6 +30,7 @@ class Puppeteer::Browser
29
30
  network_enabled: true,
30
31
  issues_enabled: true,
31
32
  block_list: nil,
33
+ allow_list: nil,
32
34
  process:,
33
35
  close_callback:,
34
36
  target_filter_callback:,
@@ -42,11 +44,13 @@ class Puppeteer::Browser
42
44
  network_enabled: network_enabled,
43
45
  issues_enabled: issues_enabled,
44
46
  block_list: block_list,
47
+ allow_list: allow_list,
45
48
  process: process,
46
49
  close_callback: close_callback,
47
50
  target_filter_callback: target_filter_callback,
48
51
  is_page_target_callback: is_page_target_callback,
49
52
  )
53
+ browser.send(:validate_allow_list_version)
50
54
  browser.send(:attach)
51
55
  browser
52
56
  end
@@ -59,6 +63,7 @@ class Puppeteer::Browser
59
63
  # @rbs network_enabled: bool -- Whether network events are enabled
60
64
  # @rbs issues_enabled: bool -- Whether issues events are enabled
61
65
  # @rbs block_list: Array[String]? -- URL block list patterns
66
+ # @rbs allow_list: Array[String]? -- URL allow list patterns
62
67
  # @rbs process: Puppeteer::BrowserRunner::BrowserProcess? -- Browser process handle
63
68
  # @rbs close_callback: Proc -- Close callback
64
69
  # @rbs target_filter_callback: Proc? -- Target filter callback
@@ -72,6 +77,7 @@ class Puppeteer::Browser
72
77
  network_enabled: true,
73
78
  issues_enabled: true,
74
79
  block_list: nil,
80
+ allow_list: nil,
75
81
  process:,
76
82
  close_callback:,
77
83
  target_filter_callback:,
@@ -85,6 +91,7 @@ class Puppeteer::Browser
85
91
  @network_enabled = network_enabled
86
92
  @issues_enabled = issues_enabled
87
93
  @block_list = block_list
94
+ @allow_list = allow_list
88
95
  @process = process
89
96
  @connection = connection
90
97
  @close_callback = close_callback
@@ -102,14 +109,28 @@ class Puppeteer::Browser
102
109
  target_factory: method(:create_target),
103
110
  target_filter_callback: @target_filter_callback,
104
111
  block_list: block_list,
112
+ allow_list: allow_list,
105
113
  )
114
+ @connection.reject_emulate_network_conditions_calls =
115
+ [block_list, allow_list].any? { |list| list && !list.empty? }
106
116
  @extensions = {}
117
+ @version_promise = nil
107
118
  end
108
119
 
109
120
  private def default_target_filter_callback(target_info)
110
121
  true
111
122
  end
112
123
 
124
+ private def validate_allow_list_version
125
+ return unless @allow_list
126
+
127
+ product = version_info.product
128
+ major_version = product[/\d+/].to_i
129
+ if major_version < 149
130
+ raise Puppeteer::Error.new('The allow_list option requires Chrome 149 or greater.')
131
+ end
132
+ end
133
+
113
134
  private def default_is_page_target_callback(target_info)
114
135
  ['page', 'background_page', 'webview'].include?(target_info.type)
115
136
  end
@@ -261,7 +282,7 @@ class Puppeteer::Browser
261
282
  end
262
283
 
263
284
  private def handle_attached_to_target(target)
264
- if target.initialized_promise.wait
285
+ if target.exposed? && target.initialized_promise.wait
265
286
  emit_event(BrowserEmittedEvents::TargetCreated, target)
266
287
  target.browser_context.emit_event(BrowserContextEmittedEvents::TargetCreated, target)
267
288
  end
@@ -270,7 +291,7 @@ class Puppeteer::Browser
270
291
  private def handle_detached_from_target(target)
271
292
  target.ignore_initialize_callback_promise
272
293
  target.closed_callback
273
- if target.initialized_promise.wait
294
+ if target.exposed? && target.initialized_promise.wait
274
295
  emit_event(BrowserEmittedEvents::TargetDestroyed, target)
275
296
  target.browser_context.emit_event(BrowserContextEmittedEvents::TargetDestroyed, target)
276
297
  end
@@ -312,7 +333,7 @@ class Puppeteer::Browser
312
333
  }.compact
313
334
  result = @connection.send_message('Target.createTarget', **create_target_params)
314
335
  target_id = result['targetId']
315
- target = @target_manager.available_targets[target_id]
336
+ target = wait_for_available_target(target_id)
316
337
  unless target
317
338
  raise MissingTargetError.new("Missing target for page (id = #{target_id})")
318
339
  end
@@ -326,11 +347,31 @@ class Puppeteer::Browser
326
347
  page
327
348
  end
328
349
 
350
+ private def wait_for_available_target(target_id)
351
+ target = @target_manager.available_targets[target_id]
352
+ return target if target
353
+
354
+ promise = Async::Promise.new
355
+ listener_id = @target_manager.add_event_listener(TargetManagerEmittedEvents::TargetAvailable) do |available_target|
356
+ if available_target.target_id == target_id && !promise.resolved?
357
+ promise.resolve(available_target)
358
+ end
359
+ end
360
+ target = @target_manager.available_targets[target_id]
361
+ return target if target
362
+
363
+ Puppeteer::AsyncUtils.async_timeout(30_000, promise).wait
364
+ rescue Async::TimeoutError
365
+ nil
366
+ ensure
367
+ @target_manager.remove_event_listener(listener_id) if listener_id
368
+ end
369
+
329
370
  # All active targets inside the Browser. In case of multiple browser contexts, returns
330
371
  # an array with all the targets in all browser contexts.
331
372
  # @rbs return: Array[Puppeteer::Target] -- Active targets
332
373
  def targets
333
- @target_manager.available_targets.values.select { |target| target.initialized? }
374
+ @target_manager.available_targets.values.select { |target| target.exposed? && target.initialized? }
334
375
  end
335
376
 
336
377
 
@@ -386,12 +427,24 @@ class Puppeteer::Browser
386
427
 
387
428
  # @rbs return: String -- Browser version string
388
429
  def version
389
- Version.fetch(@connection).product
430
+ version_info.product
390
431
  end
391
432
 
392
433
  # @rbs return: String -- Browser user agent string
393
434
  def user_agent
394
- Version.fetch(@connection).user_agent
435
+ version_info.user_agent
436
+ end
437
+
438
+ private def version_info
439
+ unless @version_promise
440
+ @version_promise = Async::Promise.new
441
+ begin
442
+ @version_promise.resolve(Version.new(@connection.send_message('Browser.getVersion')))
443
+ rescue => error
444
+ @version_promise.reject(error)
445
+ end
446
+ end
447
+ @version_promise.wait
395
448
  end
396
449
 
397
450
  # @rbs page_target_id: String -- Page target id
@@ -401,10 +454,44 @@ class Puppeteer::Browser
401
454
  result['targetId']
402
455
  end
403
456
 
457
+ # @rbs page_target_id: String -- Inspected page target id
458
+ # @rbs return: Puppeteer::Page -- DevTools page
459
+ def _create_devtools_page(page_target_id)
460
+ result = @connection.send_message('Target.openDevTools', targetId: page_target_id)
461
+ _get_devtools_target_page(result['targetId'])
462
+ end
463
+
464
+ # @rbs devtools_target_id: String -- DevTools target id
465
+ # @rbs return: Puppeteer::Page -- DevTools page
466
+ def _get_devtools_target_page(devtools_target_id)
467
+ target = wait_for_available_target(devtools_target_id)
468
+ unless target
469
+ raise MissingTargetError.new(
470
+ "Missing target for DevTools page (id = #{devtools_target_id})",
471
+ )
472
+ end
473
+ unless target.initialized_promise.wait
474
+ raise CreatePageError.new(
475
+ "Failed to create target for DevTools page (id = #{devtools_target_id})",
476
+ )
477
+ end
478
+ page = target.as_page
479
+ unless page
480
+ raise CreatePageError.new(
481
+ "Failed to create a DevTools Page for target (id = #{devtools_target_id})",
482
+ )
483
+ end
484
+ page
485
+ end
486
+
404
487
  # @rbs path: String -- Extension path
488
+ # @rbs enabled_in_incognito: bool -- Enable in Incognito and OTR profiles
405
489
  # @rbs return: String -- Installed extension id
406
- def install_extension(path)
407
- result = @connection.send_message('Extensions.loadUnpacked', path: path)
490
+ def install_extension(path, enabled_in_incognito: false)
491
+ result = @connection.send_message('Extensions.loadUnpacked', {
492
+ path: path,
493
+ enableInIncognito: enabled_in_incognito,
494
+ })
408
495
  extension_id = result['id']
409
496
  @extensions.delete(extension_id)
410
497
  extension_id
@@ -414,6 +501,7 @@ class Puppeteer::Browser
414
501
  # @rbs return: void -- No return value
415
502
  def uninstall_extension(extension_id)
416
503
  @connection.send_message('Extensions.uninstall', id: extension_id)
504
+ @target_manager.remove_extension_service_workers(extension_id)
417
505
  @extensions.delete(extension_id)
418
506
  end
419
507
 
@@ -451,6 +539,17 @@ class Puppeteer::Browser
451
539
  @block_list
452
540
  end
453
541
 
542
+ # @rbs return: Array[String]? -- URL allow list patterns
543
+ def allow_list
544
+ @allow_list
545
+ end
546
+
547
+ # @rbs url: String -- URL to validate against network restriction rules
548
+ # @rbs return: bool -- Whether the URL is allowed
549
+ def url_allowed?(url)
550
+ @target_manager.url_allowed?(url)
551
+ end
552
+
454
553
  # @rbs return: void -- No return value
455
554
  def close
456
555
  @close_callback.call
@@ -32,6 +32,7 @@ class Puppeteer::BrowserConnector
32
32
  network_enabled: @browser_options.network_enabled,
33
33
  issues_enabled: @browser_options.issues_enabled,
34
34
  block_list: @browser_options.block_list,
35
+ allow_list: @browser_options.allow_list,
35
36
  process: nil,
36
37
  close_callback: -> { connection.send_message('Browser.close') },
37
38
  target_filter_callback: @browser_options.target_filter,
@@ -241,7 +241,6 @@ class Puppeteer::BrowserContext
241
241
  if partition_key
242
242
  normalized['partitionKey'] = convert_partition_key_from_cdp(partition_key)
243
243
  end
244
- normalized['sameParty'] = cookie['sameParty'] || false
245
244
  normalized
246
245
  end
247
246
  end