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.
@@ -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
@@ -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]
@@ -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(event)
179
- return if event['targetInfo']['type'] != 'iframe'
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(event)
191
- frame = @frames[event['targetId']]
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|nil]
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
- frame = Puppeteer::Frame.new(self, parent_frame, frame_id, session)
276
- @frames[frame_id] = frame
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
- frame =
285
- if is_main_frame
286
- @main_frame
287
- else
288
- @frames[frame_payload['id']]
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 ArgumentError.new('We either navigate top level or have old version of the navigated frame')
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 = frame_payload['id']
338
+ frame.id = frame_id
308
339
  else
309
340
  # Initial main frame navigation.
310
- frame = Puppeteer::Frame.new(self, nil, frame_payload['id'], @client)
341
+ frame = Puppeteer::Frame.new(self, nil, frame_id, @client)
311
342
  end
312
- @frames[frame_payload['id']] = frame
343
+ @frames[frame_id] = frame
313
344
  @main_frame = frame
314
345
  end
315
346