puppeteer-ruby 0.42.0 → 0.43.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/CHANGELOG.md +5 -1
- data/docs/api_coverage.md +2 -2
- data/lib/puppeteer/browser.rb +109 -75
- data/lib/puppeteer/browser_connector.rb +67 -0
- data/lib/puppeteer/chrome_target_manager.rb +256 -0
- data/lib/puppeteer/connection.rb +16 -0
- data/lib/puppeteer/events.rb +9 -0
- data/lib/puppeteer/firefox_target_manager.rb +158 -0
- data/lib/puppeteer/frame_manager.rb +69 -38
- data/lib/puppeteer/launcher/chrome.rb +3 -56
- data/lib/puppeteer/launcher/firefox.rb +1 -55
- data/lib/puppeteer/lifecycle_watcher.rb +2 -1
- data/lib/puppeteer/page.rb +38 -29
- data/lib/puppeteer/puppeteer.rb +1 -1
- data/lib/puppeteer/target.rb +14 -1
- data/lib/puppeteer/version.rb +1 -1
- data/lib/puppeteer.rb +3 -0
- data/puppeteer-ruby.gemspec +1 -1
- metadata +7 -4
@@ -0,0 +1,256 @@
|
|
1
|
+
class Puppeteer::ChromeTargetManager
|
2
|
+
include Puppeteer::EventCallbackable
|
3
|
+
|
4
|
+
def initialize(connection:, target_factory:, target_filter_callback:)
|
5
|
+
@discovered_targets_by_target_id = {}
|
6
|
+
@attached_targets_by_target_id = {}
|
7
|
+
@attached_targets_by_session_id = {}
|
8
|
+
@ignored_targets = Set.new
|
9
|
+
@target_ids_for_init = Set.new
|
10
|
+
|
11
|
+
@connection = connection
|
12
|
+
@target_filter_callback = target_filter_callback
|
13
|
+
@target_factory = target_factory
|
14
|
+
@target_interceptors = {}
|
15
|
+
@initialize_promise = resolvable_future
|
16
|
+
|
17
|
+
@connection_event_listeners = []
|
18
|
+
@connection_event_listeners << @connection.add_event_listener(
|
19
|
+
'Target.targetCreated',
|
20
|
+
&method(:handle_target_created)
|
21
|
+
)
|
22
|
+
@connection_event_listeners << @connection.add_event_listener(
|
23
|
+
'Target.targetDestroyed',
|
24
|
+
&method(:handle_target_destroyed)
|
25
|
+
)
|
26
|
+
@connection_event_listeners << @connection.add_event_listener(
|
27
|
+
'Target.targetInfoChanged',
|
28
|
+
&method(:handle_target_info_changed)
|
29
|
+
)
|
30
|
+
@connection_event_listeners << @connection.add_event_listener(
|
31
|
+
'sessiondetached',
|
32
|
+
&method(:handle_session_detached)
|
33
|
+
)
|
34
|
+
|
35
|
+
setup_attachment_listeners(@connection)
|
36
|
+
@connection.async_send_message('Target.setDiscoverTargets', discover: true)
|
37
|
+
end
|
38
|
+
|
39
|
+
def init
|
40
|
+
@discovered_targets_by_target_id.each do |target_id, target_info|
|
41
|
+
if @target_filter_callback.call(target_info)
|
42
|
+
@target_ids_for_init << target_id
|
43
|
+
end
|
44
|
+
end
|
45
|
+
@connection.send_message('Target.setAutoAttach', {
|
46
|
+
waitForDebuggerOnStart: true,
|
47
|
+
flatten: true,
|
48
|
+
autoAttach: true,
|
49
|
+
})
|
50
|
+
@initialize_promise.value!
|
51
|
+
end
|
52
|
+
|
53
|
+
def dispose
|
54
|
+
@connection.remove_event_listener(*@connection_event_listeners)
|
55
|
+
remove_attachment_listeners(@connection)
|
56
|
+
end
|
57
|
+
|
58
|
+
def available_targets
|
59
|
+
@attached_targets_by_target_id
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_target_interceptor(client, interceptor)
|
63
|
+
interceptors = @target_interceptors[client] || []
|
64
|
+
interceptors << interceptor
|
65
|
+
@target_interceptors[client] = interceptors
|
66
|
+
end
|
67
|
+
|
68
|
+
def remove_target_interceptor(client, interceptor)
|
69
|
+
@target_interceptors[client]&.delete_if { |current| current == interceptor }
|
70
|
+
end
|
71
|
+
|
72
|
+
private def setup_attachment_listeners(session)
|
73
|
+
@attachment_listener_ids ||= {}
|
74
|
+
@attachment_listener_ids[session] ||= []
|
75
|
+
|
76
|
+
@attachment_listener_ids[session] << session.add_event_listener('Target.attachedToTarget') do |event|
|
77
|
+
handle_attached_to_target(session, event)
|
78
|
+
end
|
79
|
+
|
80
|
+
@attachment_listener_ids[session] << session.add_event_listener('Target.detachedFromTarget') do |event|
|
81
|
+
handle_detached_from_target(session, event)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private def remove_attachment_listeners(session)
|
86
|
+
return unless @attachment_listener_ids
|
87
|
+
listener_ids = @attachment_listener_ids.delete(session)
|
88
|
+
return if !listener_ids || listener_ids.empty?
|
89
|
+
session.remove_event_listener(*listener_ids)
|
90
|
+
end
|
91
|
+
|
92
|
+
private def handle_session_detached(session)
|
93
|
+
remove_attachment_listeners(session)
|
94
|
+
@target_interceptors.delete(session)
|
95
|
+
end
|
96
|
+
|
97
|
+
private def handle_target_created(event)
|
98
|
+
target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
|
99
|
+
@discovered_targets_by_target_id[target_info.target_id] = target_info
|
100
|
+
|
101
|
+
emit_event(TargetManagerEmittedEvents::TargetDiscovered, target_info)
|
102
|
+
|
103
|
+
# The connection is already attached to the browser target implicitly,
|
104
|
+
# therefore, no new CDPSession is created and we have special handling
|
105
|
+
# here.
|
106
|
+
if target_info.type == 'browser' && target_info.attached
|
107
|
+
return if @attached_targets_by_target_id[target_info.target_id]
|
108
|
+
|
109
|
+
target = @target_factory.call(target_info, nil)
|
110
|
+
@attached_targets_by_target_id[target_info.target_id] = target
|
111
|
+
end
|
112
|
+
|
113
|
+
if target_info.type == 'shared_worker'
|
114
|
+
# Special case (https://crbug.com/1338156): currently, shared_workers
|
115
|
+
# don't get auto-attached. This should be removed once the auto-attach
|
116
|
+
# works.
|
117
|
+
@connection.create_session(target_info)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private def handle_target_destroyed(event)
|
122
|
+
target_id = event['targetId']
|
123
|
+
target_info = @discovered_targets_by_target_id.delete(target_id)
|
124
|
+
finish_initialization_if_ready(target_id)
|
125
|
+
|
126
|
+
if target_info.type == 'service_worker' && @attached_targets_by_target_id.has_key?(target_id)
|
127
|
+
# Special case for service workers: report TargetGone event when
|
128
|
+
# the worker is destroyed.
|
129
|
+
target = @attached_targets_by_target_id.delete(target_id)
|
130
|
+
emit_event(TargetManagerEmittedEvents::TargetGone, target)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private def handle_target_info_changed(event)
|
135
|
+
target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
|
136
|
+
@discovered_targets_by_target_id[target_info.target_id] = target_info
|
137
|
+
|
138
|
+
if @ignored_targets.include?(target_info.target_id) || !@attached_targets_by_target_id.has_key?(target_info.target_id) || !target_info.attached
|
139
|
+
return
|
140
|
+
end
|
141
|
+
original_target = @attached_targets_by_target_id[target_info.target_id]
|
142
|
+
emit_event(TargetManagerEmittedEvents::TargetChanged, original_target, target_info)
|
143
|
+
end
|
144
|
+
|
145
|
+
class SessionNotCreatedError < StandardError ; end
|
146
|
+
|
147
|
+
private def handle_attached_to_target(parent_session, event)
|
148
|
+
target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
|
149
|
+
session_id = event['sessionId']
|
150
|
+
session = @connection.session(session_id)
|
151
|
+
unless session
|
152
|
+
raise SessionNotCreatedError.new("Session #{session_id} was not created.")
|
153
|
+
end
|
154
|
+
|
155
|
+
silent_detach = -> {
|
156
|
+
begin
|
157
|
+
session.send_message('Runtime.runIfWaitingForDebugger')
|
158
|
+
rescue => err
|
159
|
+
Logger.new($stderr).warn(err)
|
160
|
+
end
|
161
|
+
|
162
|
+
# We don't use `session.detach()` because that dispatches all commands on
|
163
|
+
# the connection instead of the parent session.
|
164
|
+
begin
|
165
|
+
parent_session.send_message('Target.detachFromTarget', {
|
166
|
+
sessionId: session.id,
|
167
|
+
})
|
168
|
+
rescue => err
|
169
|
+
Logger.new($stderr).warn(err)
|
170
|
+
end
|
171
|
+
}
|
172
|
+
|
173
|
+
# Special case for service workers: being attached to service workers will
|
174
|
+
# prevent them from ever being destroyed. Therefore, we silently detach
|
175
|
+
# from service workers unless the connection was manually created via
|
176
|
+
# `page.worker()`. To determine this, we use
|
177
|
+
# `this.#connection.isAutoAttached(targetInfo.targetId)`. In the future, we
|
178
|
+
# should determine if a target is auto-attached or not with the help of
|
179
|
+
# CDP.
|
180
|
+
if target_info.type == 'service_worker' && @connection.auto_attached?(target_info.target_id)
|
181
|
+
finish_initialization_if_ready(target_info.target_id)
|
182
|
+
silent_detach.call
|
183
|
+
if parent_session.is_a?(Puppeteer::CDPSession)
|
184
|
+
target = @target_factory.call(target_info, parent_session)
|
185
|
+
@attached_targets_by_target_id[target_info.target_id] = target
|
186
|
+
emit_event(TargetManagerEmittedEvents::TargetAvailable, target)
|
187
|
+
end
|
188
|
+
|
189
|
+
return
|
190
|
+
end
|
191
|
+
|
192
|
+
unless @target_filter_callback.call(target_info)
|
193
|
+
@ignored_targets << target_info.target_id
|
194
|
+
finish_initialization_if_ready(target_info.target_id)
|
195
|
+
silent_detach.call
|
196
|
+
|
197
|
+
return
|
198
|
+
end
|
199
|
+
|
200
|
+
target = @attached_targets_by_target_id[target_info.target_id] || @target_factory.call(target_info, session)
|
201
|
+
setup_attachment_listeners(session)
|
202
|
+
|
203
|
+
@attached_targets_by_target_id[target_info.target_id] ||= target
|
204
|
+
@attached_targets_by_session_id[session.id] = target
|
205
|
+
|
206
|
+
@target_interceptors[parent_session]&.each do |interceptor|
|
207
|
+
if parent_session.is_a?(Puppeteer::Connection)
|
208
|
+
interceptor.call(target, nil)
|
209
|
+
else
|
210
|
+
# Sanity check: if parent session is not a connection, it should be
|
211
|
+
# present in #attachedTargetsBySessionId.
|
212
|
+
attached_target = @attached_targets_by_session_id[parent_session.id]
|
213
|
+
unless attached_target
|
214
|
+
raise "No target found for the parent session: #{parent_session.id}"
|
215
|
+
end
|
216
|
+
interceptor.call(target, attached_target)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
@target_ids_for_init.delete(target.target_id)
|
221
|
+
future { emit_event(TargetManagerEmittedEvents::TargetAvailable, target) }
|
222
|
+
|
223
|
+
if @target_ids_for_init.empty?
|
224
|
+
@initialize_promise.fulfill(nil) unless @initialize_promise.resolved?
|
225
|
+
end
|
226
|
+
|
227
|
+
future do
|
228
|
+
# TODO: the browser might be shutting down here. What do we do with the error?
|
229
|
+
await_all(
|
230
|
+
session.async_send_message('Target.setAutoAttach', {
|
231
|
+
waitForDebuggerOnStart: true,
|
232
|
+
flatten: true,
|
233
|
+
autoAttach: true,
|
234
|
+
}),
|
235
|
+
session.async_send_message('Runtime.runIfWaitingForDebugger'),
|
236
|
+
)
|
237
|
+
rescue => err
|
238
|
+
Logger.new($stderr).warn(err)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
private def finish_initialization_if_ready(target_id)
|
243
|
+
@target_ids_for_init.delete(target_id)
|
244
|
+
if @target_ids_for_init.empty?
|
245
|
+
@initialize_promise.fulfill(nil) unless @initialize_promise.resolved?
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
private def handle_detached_from_target(parent_session, event)
|
250
|
+
session_id = event['sessionId']
|
251
|
+
target = @attached_targets_by_session_id.delete(session_id)
|
252
|
+
return unless target
|
253
|
+
@attached_targets_by_target_id.delete(target.target_id)
|
254
|
+
emit_event(TargetManagerEmittedEvents::TargetGone, target)
|
255
|
+
end
|
256
|
+
end
|
data/lib/puppeteer/connection.rb
CHANGED
@@ -58,6 +58,7 @@ class Puppeteer::Connection
|
|
58
58
|
|
59
59
|
@sessions = Concurrent::Hash.new
|
60
60
|
@closed = false
|
61
|
+
@manually_attached = Set.new
|
61
62
|
end
|
62
63
|
|
63
64
|
# used only in Browser#connected?
|
@@ -243,12 +244,22 @@ class Puppeteer::Connection
|
|
243
244
|
session_id = message['params']['sessionId']
|
244
245
|
session = Puppeteer::CDPSession.new(self, message['params']['targetInfo']['type'], session_id)
|
245
246
|
@sessions[session_id] = session
|
247
|
+
emit_event('sessionattached', session)
|
248
|
+
if message['sessionId']
|
249
|
+
parent_session = @sessions[message['sessionId']]
|
250
|
+
parent_session&.emit_event('sessionattached', session)
|
251
|
+
end
|
246
252
|
when 'Target.detachedFromTarget'
|
247
253
|
session_id = message['params']['sessionId']
|
248
254
|
session = @sessions[session_id]
|
249
255
|
if session
|
250
256
|
session.handle_closed
|
251
257
|
@sessions.delete(session_id)
|
258
|
+
emit_event('sessiondetached', session)
|
259
|
+
if message['sessionId']
|
260
|
+
parent_session = @sessions[message['sessionId']]
|
261
|
+
parent_session&.emit_event('sessiondetached', session)
|
262
|
+
end
|
252
263
|
end
|
253
264
|
end
|
254
265
|
|
@@ -307,9 +318,14 @@ class Puppeteer::Connection
|
|
307
318
|
@transport.close
|
308
319
|
end
|
309
320
|
|
321
|
+
def auto_attached?(target_id)
|
322
|
+
@manually_attached.include?(target_id)
|
323
|
+
end
|
324
|
+
|
310
325
|
# @param {Protocol.Target.TargetInfo} targetInfo
|
311
326
|
# @return [CDPSession]
|
312
327
|
def create_session(target_info)
|
328
|
+
@manually_attached << target_info.target_id
|
313
329
|
result = send_message('Target.attachToTarget', targetId: target_info.target_id, flatten: true)
|
314
330
|
session_id = result['sessionId']
|
315
331
|
@sessions[session_id]
|
data/lib/puppeteer/events.rb
CHANGED
@@ -182,3 +182,12 @@ module PageEmittedEvents ; end
|
|
182
182
|
# {https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API WebWorker} is destroyed by the page.
|
183
183
|
WorkerDestroyed: 'workerdestroyed',
|
184
184
|
}.define_const_into(PageEmittedEvents)
|
185
|
+
|
186
|
+
module TargetManagerEmittedEvents ; end
|
187
|
+
|
188
|
+
{
|
189
|
+
TargetDiscovered: 'targetDiscovered',
|
190
|
+
TargetAvailable: 'targetAvailable',
|
191
|
+
TargetGone: 'targetGone',
|
192
|
+
TargetChanged: 'targetChanged',
|
193
|
+
}.define_const_into(TargetManagerEmittedEvents)
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# FirefoxTargetManager implements target management using
|
2
|
+
# `Target.setDiscoverTargets` without using auto-attach. It, therefore, creates
|
3
|
+
# targets that lazily establish their CDP sessions.
|
4
|
+
#
|
5
|
+
# Although the approach is potentially flaky, there is no other way for Firefox
|
6
|
+
# because Firefox's CDP implementation does not support auto-attach.
|
7
|
+
#
|
8
|
+
# Firefox does not support targetInfoChanged and detachedFromTarget events:
|
9
|
+
# - https://bugzilla.mozilla.org/show_bug.cgi?id=1610855
|
10
|
+
# - https://bugzilla.mozilla.org/show_bug.cgi?id=1636979
|
11
|
+
class Puppeteer::FirefoxTargetManager
|
12
|
+
include Puppeteer::EventCallbackable
|
13
|
+
|
14
|
+
def initialize(connection:, target_factory:, target_filter_callback:)
|
15
|
+
@discovered_targets_by_target_id = {}
|
16
|
+
@available_targets_by_target_id = {}
|
17
|
+
@available_targets_by_session_id = {}
|
18
|
+
@ignored_targets = Set.new
|
19
|
+
@target_ids_for_init = Set.new
|
20
|
+
|
21
|
+
@connection = connection
|
22
|
+
@target_filter_callback = target_filter_callback
|
23
|
+
@target_factory = target_factory
|
24
|
+
@target_interceptors = {}
|
25
|
+
@initialize_promise = resolvable_future
|
26
|
+
|
27
|
+
@connection_event_listeners = []
|
28
|
+
@connection_event_listeners << @connection.add_event_listener(
|
29
|
+
'Target.targetCreated',
|
30
|
+
&method(:handle_target_created)
|
31
|
+
)
|
32
|
+
@connection_event_listeners << @connection.add_event_listener(
|
33
|
+
'Target.targetDestroyed',
|
34
|
+
&method(:handle_target_destroyed)
|
35
|
+
)
|
36
|
+
@connection_event_listeners << @connection.add_event_listener(
|
37
|
+
'sessiondetached',
|
38
|
+
&method(:handle_session_detached)
|
39
|
+
)
|
40
|
+
|
41
|
+
setup_attachment_listeners(@connection)
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_target_interceptor(client, interceptor)
|
45
|
+
interceptors = @target_interceptors[client] || []
|
46
|
+
interceptors << interceptor
|
47
|
+
@target_interceptors[client] = interceptors
|
48
|
+
end
|
49
|
+
|
50
|
+
def remove_target_interceptor(client, interceptor)
|
51
|
+
@target_interceptors[client]&.delete_if { |current| current == interceptor }
|
52
|
+
end
|
53
|
+
|
54
|
+
private def setup_attachment_listeners(session)
|
55
|
+
@attachment_listener_ids ||= {}
|
56
|
+
@attachment_listener_ids[session] ||= []
|
57
|
+
|
58
|
+
@attachment_listener_ids[session] << session.add_event_listener('Target.attachedToTarget') do |event|
|
59
|
+
handle_attached_to_target(session, event)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private def handle_session_detached(session)
|
64
|
+
remove_session_listeners(session)
|
65
|
+
@target_interceptors.delete(session)
|
66
|
+
end
|
67
|
+
|
68
|
+
private def remove_session_listeners(session)
|
69
|
+
return unless @attachment_listener_ids
|
70
|
+
listener_ids = @attachment_listener_ids.delete(session)
|
71
|
+
return if listener_ids.empty?
|
72
|
+
session.remove_event_listener(*listener_ids)
|
73
|
+
end
|
74
|
+
|
75
|
+
def available_targets
|
76
|
+
@available_targets_by_target_id
|
77
|
+
end
|
78
|
+
|
79
|
+
def dispose
|
80
|
+
@connection.remove_event_listener(*@connection_event_listeners)
|
81
|
+
remove_session_listeners(@connection)
|
82
|
+
end
|
83
|
+
|
84
|
+
def init
|
85
|
+
@connection.send_message('Target.setDiscoverTargets', discover: true)
|
86
|
+
@target_ids_for_init.merge(@discovered_targets_by_target_id.keys)
|
87
|
+
@initialize_promise.value!
|
88
|
+
end
|
89
|
+
|
90
|
+
private def handle_target_created(event)
|
91
|
+
target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
|
92
|
+
return if @discovered_targets_by_target_id[target_info.target_id]
|
93
|
+
@discovered_targets_by_target_id[target_info.target_id] = target_info
|
94
|
+
|
95
|
+
if target_info.type == 'browser' && target_info.attached
|
96
|
+
target = @target_factory.call(target_info, nil)
|
97
|
+
@available_targets_by_target_id[target_info.target_id] = target
|
98
|
+
finish_initialization_if_ready(target.target_id)
|
99
|
+
end
|
100
|
+
|
101
|
+
unless @target_filter_callback.call(target_info)
|
102
|
+
@ignored_targets << target_info.target_id
|
103
|
+
finish_initialization_if_ready(target_info.target_id)
|
104
|
+
return
|
105
|
+
end
|
106
|
+
|
107
|
+
target = @target_factory.call(target_info, nil)
|
108
|
+
@available_targets_by_target_id[target_info.target_id] = target
|
109
|
+
emit_event(TargetManagerEmittedEvents::TargetAvailable, target)
|
110
|
+
finish_initialization_if_ready(target.target_id)
|
111
|
+
end
|
112
|
+
|
113
|
+
private def handle_target_destroyed(event)
|
114
|
+
target_id = event['targetId']
|
115
|
+
target_info = @discovered_targets_by_target_id.delete(target_id)
|
116
|
+
finish_initialization_if_ready(target_id)
|
117
|
+
|
118
|
+
target = @available_targets_by_target_id.delete(target_id)
|
119
|
+
emit_event(TargetManagerEmittedEvents::TargetGone, target)
|
120
|
+
end
|
121
|
+
|
122
|
+
class SessionNotCreatedError < StandardError ; end
|
123
|
+
|
124
|
+
private def handle_attached_to_target(parent_session, event)
|
125
|
+
target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
|
126
|
+
session_id = event['sessionId']
|
127
|
+
session = @connection.session(session_id)
|
128
|
+
unless session
|
129
|
+
raise SessionNotCreatedError.new("Session #{session_id} was not created.")
|
130
|
+
end
|
131
|
+
|
132
|
+
target = @available_targets_by_target_id[target_info.target_id] or raise "Target #{target_info.target_id} is missing"
|
133
|
+
setup_attachment_listeners(session)
|
134
|
+
|
135
|
+
@available_targets_by_session_id[session_id] = target
|
136
|
+
|
137
|
+
@target_interceptors[parent_session]&.each do |hook|
|
138
|
+
if parent_session.is_a?(Puppeteer::Connection)
|
139
|
+
hook.call(target, nil)
|
140
|
+
else
|
141
|
+
# Sanity check: if parent session is not a connection, it should be
|
142
|
+
# present in #attachedTargetsBySessionId.
|
143
|
+
available_target = @available_targets_by_session_id[parent_session.id]
|
144
|
+
unless available_target
|
145
|
+
raise "No target found for the parent session: #{parent_session.id}"
|
146
|
+
end
|
147
|
+
hook.call(target, available_target)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
private def finish_initialization_if_ready(target_id)
|
153
|
+
@target_ids_for_init.delete(target_id)
|
154
|
+
if @target_ids_for_init.empty?
|
155
|
+
@initialize_promise.fulfill(nil) unless @initialize_promise.resolved?
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -27,6 +27,13 @@ class Puppeteer::FrameManager
|
|
27
27
|
# @type {!Set<string>}
|
28
28
|
@isolated_worlds = Set.new
|
29
29
|
|
30
|
+
# Keeps track of OOPIF targets/frames (target ID == frame ID for OOPIFs)
|
31
|
+
# that are being initialized.
|
32
|
+
@frames_pending_target_init = {}
|
33
|
+
|
34
|
+
# Keeps track of frames that are in the process of being attached in #onFrameAttached.
|
35
|
+
@frames_pending_attachment = {}
|
36
|
+
|
30
37
|
setup_listeners(@client)
|
31
38
|
end
|
32
39
|
|
@@ -61,27 +68,17 @@ class Puppeteer::FrameManager
|
|
61
68
|
client.on_event('Page.lifecycleEvent') do |event|
|
62
69
|
handle_lifecycle_event(event)
|
63
70
|
end
|
64
|
-
client.on_event('Target.attachedToTarget') do |event|
|
65
|
-
handle_attached_to_target(event)
|
66
|
-
end
|
67
|
-
client.on_event('Target.detachedFromTarget') do |event|
|
68
|
-
handle_detached_from_target(event)
|
69
|
-
end
|
70
71
|
end
|
71
72
|
|
72
73
|
attr_reader :client, :timeout_settings
|
73
74
|
|
74
|
-
private def init(cdp_session = nil)
|
75
|
+
private def init(target_id, cdp_session = nil)
|
76
|
+
@frames_pending_target_init[target_id] ||= resolvable_future
|
75
77
|
client = cdp_session || @client
|
76
78
|
|
77
79
|
promises = [
|
78
80
|
client.async_send_message('Page.enable'),
|
79
81
|
client.async_send_message('Page.getFrameTree'),
|
80
|
-
cdp_session&.async_send_message('Target.setAutoAttach', {
|
81
|
-
autoAttach: true,
|
82
|
-
waitForDebuggerOnStart: false,
|
83
|
-
flatten: true,
|
84
|
-
}),
|
85
82
|
].compact
|
86
83
|
results = await_all(*promises)
|
87
84
|
frame_tree = results[1]['frameTree']
|
@@ -97,6 +94,8 @@ class Puppeteer::FrameManager
|
|
97
94
|
return if err.message.include?('Target closed') || err.message.include?('Session closed')
|
98
95
|
|
99
96
|
raise
|
97
|
+
ensure
|
98
|
+
@frames_pending_target_init.delete(target_id)&.fulfill(nil)
|
100
99
|
end
|
101
100
|
|
102
101
|
define_async_method :async_init
|
@@ -121,12 +120,13 @@ class Puppeteer::FrameManager
|
|
121
120
|
option_timeout = timeout || @timeout_settings.navigation_timeout
|
122
121
|
|
123
122
|
watcher = Puppeteer::LifecycleWatcher.new(self, frame, option_wait_until, option_timeout)
|
123
|
+
ensure_new_document_navigation = false
|
124
124
|
|
125
125
|
begin
|
126
126
|
navigate = future do
|
127
127
|
result = @client.send_message('Page.navigate', navigate_params)
|
128
128
|
loader_id = result['loaderId']
|
129
|
-
|
129
|
+
ensure_new_document_navigation = !!loader_id
|
130
130
|
if result['errorText']
|
131
131
|
raise NavigationError.new("#{result['errorText']} at #{url}")
|
132
132
|
end
|
@@ -137,9 +137,12 @@ class Puppeteer::FrameManager
|
|
137
137
|
)
|
138
138
|
|
139
139
|
await_any(
|
140
|
-
watcher.new_document_navigation_promise,
|
141
|
-
watcher.same_document_navigation_promise,
|
142
140
|
watcher.timeout_or_termination_promise,
|
141
|
+
if ensure_new_document_navigation
|
142
|
+
watcher.new_document_navigation_promise
|
143
|
+
else
|
144
|
+
watcher.same_document_navigation_promise
|
145
|
+
end,
|
143
146
|
)
|
144
147
|
rescue Puppeteer::TimeoutError => err
|
145
148
|
raise NavigationError.new(err)
|
@@ -175,20 +178,19 @@ class Puppeteer::FrameManager
|
|
175
178
|
end
|
176
179
|
|
177
180
|
# @param event [Hash]
|
178
|
-
def handle_attached_to_target(
|
179
|
-
return if
|
180
|
-
|
181
|
-
frame = @frames[event['targetInfo']['targetId']]
|
182
|
-
session = Puppeteer::Connection.from_session(@client).session(event['sessionId'])
|
181
|
+
def handle_attached_to_target(target)
|
182
|
+
return if target.target_info.type != 'iframe'
|
183
183
|
|
184
|
+
frame = @frames[target.target_info.target_id]
|
185
|
+
session = target.session
|
184
186
|
frame&.send(:update_client, session)
|
185
187
|
setup_listeners(session)
|
186
|
-
async_init(session)
|
188
|
+
async_init(target.target_info.target_id, session)
|
187
189
|
end
|
188
190
|
|
189
191
|
# @param event [Hash]
|
190
|
-
def handle_detached_from_target(
|
191
|
-
frame = @frames[
|
192
|
+
def handle_detached_from_target(target)
|
193
|
+
frame = @frames[target.target_id]
|
192
194
|
if frame && frame.oop_frame?
|
193
195
|
# When an OOP iframe is removed from the page, it
|
194
196
|
# will only get a Target.detachedFromTarget event.
|
@@ -256,7 +258,7 @@ class Puppeteer::FrameManager
|
|
256
258
|
|
257
259
|
# @param session [Puppeteer::CDPSession]
|
258
260
|
# @param frameId [String]
|
259
|
-
# @param parentFrameId [String
|
261
|
+
# @param parentFrameId [String]
|
260
262
|
def handle_frame_attached(session, frame_id, parent_frame_id)
|
261
263
|
if @frames.has_key?(frame_id)
|
262
264
|
frame = @frames[frame_id]
|
@@ -268,28 +270,57 @@ class Puppeteer::FrameManager
|
|
268
270
|
end
|
269
271
|
return
|
270
272
|
end
|
271
|
-
if !parent_frame_id
|
272
|
-
raise ArgymentError.new('parent_frame_id must not be nil')
|
273
|
-
end
|
274
273
|
parent_frame = @frames[parent_frame_id]
|
275
|
-
|
276
|
-
|
274
|
+
if parent_frame
|
275
|
+
attach_child_frame(parent_frame, parent_frame_id, frame_id, session)
|
276
|
+
return
|
277
|
+
end
|
277
278
|
|
279
|
+
if @frames_pending_target_init[parent_frame_id]
|
280
|
+
@frames_pending_attachment[frame_id] ||= resolvable_future
|
281
|
+
@frames_pending_target_init[parent_frame_id].then do |_|
|
282
|
+
attach_child_frame(@frames[parent_frame_id], parent_frame_id, frame_id, session)
|
283
|
+
@frames_pending_attachment.delete(frame_id)&.fulfill(nil)
|
284
|
+
end
|
285
|
+
return
|
286
|
+
end
|
287
|
+
|
288
|
+
raise FrameNotFoundError.new("Parent frame #{parent_frame_id} not found.")
|
289
|
+
end
|
290
|
+
|
291
|
+
class FrameNotFoundError < StandardError ; end
|
292
|
+
|
293
|
+
private def attach_child_frame(parent_frame, parent_frame_id, frame_id, session)
|
294
|
+
unless parent_frame
|
295
|
+
raise FrameNotFoundError.new("Parent frame #{parent_frame_id} not found.")
|
296
|
+
end
|
297
|
+
|
298
|
+
frame = Puppeteer::Frame.new(self, parent_frame, frame_id, session)
|
299
|
+
@frames[frame.id] = frame
|
278
300
|
emit_event(FrameManagerEmittedEvents::FrameAttached, frame)
|
301
|
+
frame
|
279
302
|
end
|
280
303
|
|
281
304
|
# @param frame_payload [Hash]
|
282
305
|
def handle_frame_navigated(frame_payload)
|
306
|
+
frame_id = frame_payload['id']
|
283
307
|
is_main_frame = !frame_payload['parentId']
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
@frames[
|
308
|
+
|
309
|
+
|
310
|
+
if @frames_pending_attachment[frame_id]
|
311
|
+
@frames_pending_attachment[frame_id].then do |_|
|
312
|
+
frame = is_main_frame ? @main_frame : @frames[frame_id]
|
313
|
+
reattach_frame(frame, frame_id, is_main_frame, frame_payload)
|
289
314
|
end
|
315
|
+
else
|
316
|
+
frame = is_main_frame ? @main_frame : @frames[frame_id]
|
317
|
+
reattach_frame(frame, frame_id, is_main_frame, frame_payload)
|
318
|
+
end
|
319
|
+
end
|
290
320
|
|
321
|
+
private def reattach_frame(frame, frame_id, is_main_frame, frame_payload)
|
291
322
|
if !is_main_frame && !frame
|
292
|
-
raise
|
323
|
+
raise "Missing frame isMainFrame=#{is_main_frame}, frameId=#{frame_id}"
|
293
324
|
end
|
294
325
|
|
295
326
|
# Detach all child frames first.
|
@@ -304,12 +335,12 @@ class Puppeteer::FrameManager
|
|
304
335
|
if frame
|
305
336
|
# Update frame id to retain frame identity on cross-process navigation.
|
306
337
|
@frames.delete(frame.id)
|
307
|
-
frame.id =
|
338
|
+
frame.id = frame_id
|
308
339
|
else
|
309
340
|
# Initial main frame navigation.
|
310
|
-
frame = Puppeteer::Frame.new(self, nil,
|
341
|
+
frame = Puppeteer::Frame.new(self, nil, frame_id, @client)
|
311
342
|
end
|
312
|
-
@frames[
|
343
|
+
@frames[frame_id] = frame
|
313
344
|
@main_frame = frame
|
314
345
|
end
|
315
346
|
|