puppeteer-ruby 0.42.0 → 0.43.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|