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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b9b4a8108523041a54e1d2e08af61b9ce13e8416451d321841e31e3ec609161b
|
|
4
|
+
data.tar.gz: 5075602b5392fa17b0d87cb566229d4ef31ef33e336b7d3c030fc71bea370b27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
3
|
-
- puppeteer-ruby version: 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
|
-
##
|
|
13
|
+
## Accessibility
|
|
14
14
|
|
|
15
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
##
|
|
426
|
+
## ScreenRecorder
|
|
427
427
|
|
|
428
|
-
*
|
|
428
|
+
* stop
|
|
429
429
|
|
|
430
430
|
## ~~SecurityDetails~~
|
|
431
431
|
|
|
@@ -469,15 +469,15 @@
|
|
|
469
469
|
## ~~UnsupportedOperation~~
|
|
470
470
|
|
|
471
471
|
|
|
472
|
-
##
|
|
472
|
+
## WebMCP
|
|
473
473
|
|
|
474
|
-
*
|
|
474
|
+
* tools
|
|
475
475
|
|
|
476
|
-
##
|
|
476
|
+
## WebMCPTool
|
|
477
477
|
|
|
478
|
-
*
|
|
478
|
+
* execute
|
|
479
479
|
|
|
480
|
-
##
|
|
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
|
data/lib/puppeteer/browser.rb
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
430
|
+
version_info.product
|
|
390
431
|
end
|
|
391
432
|
|
|
392
433
|
# @rbs return: String -- Browser user agent string
|
|
393
434
|
def user_agent
|
|
394
|
-
|
|
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',
|
|
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,
|