puppeteer-ruby 0.37.2 → 0.39.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a83ed41b8bd2c8068dc4a1738fa16abe80e88e59fb21c4768994ae640bb5b567
4
- data.tar.gz: 6e67fb0d381644bb376d09d01213320170aa5f9ec429694c9b98888bd2006722
3
+ metadata.gz: e09022ffac5da45a7c40bca6b612c8a67afece662142675625794b15b89a044b
4
+ data.tar.gz: 9af45888cfcfaff3bc368facbd1b5fb28572924ccf0c8d33b05ba310894638bb
5
5
  SHA512:
6
- metadata.gz: b229ccf33f63425298aa0d70051d6d6a1d61d9509bf6030499521b11494fb7b9ece03fa63ab5c31be6b0a552f28b5d7b571b6c930027445cee60bbcadaada24c
7
- data.tar.gz: 6f35fb3ea22f4cd557e062576fdb95939f16e62c611f09469bf8d7647f72ff380000e1b94ac8e1412b03f9107124a5ce21914c4e2b839e893ecd275ed73b9ef3
6
+ metadata.gz: 580410458c1e0f2ac6dd8fb678fce42ed5f12ee6a1ff70b90600b6601b945af4342db53f6b98fe73ba249a9d85e3e8d64efb664534f7b1231265dbfbeb2f8e09
7
+ data.tar.gz: adecd0293ec7b7ade882d9c331b794b4a14e2e52999c164880e7f4b0fa3d723e7bc6b44a238a5ad04c50ad0bfc9c3d5d33e60b50ea40904dc49af1506ce3e72e
data/.rubocop.yml CHANGED
@@ -149,6 +149,8 @@ Style/MethodCallWithArgsParentheses:
149
149
  # utils
150
150
  - debug_print
151
151
  - debug_puts
152
+ - exit
153
+ - puts
152
154
 
153
155
  Style/MethodCallWithoutArgsParentheses:
154
156
  Enabled: true
data/CHANGELOG.md CHANGED
@@ -1,7 +1,37 @@
1
- ### master [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.37.2...master)]
1
+ ### main [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.39.0...main)]
2
2
 
3
3
  - xxx
4
4
 
5
+ ### 0.39.0 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.38.0...0.39.0)]
6
+
7
+ New features:
8
+
9
+ - Puppeteer 12.0 functionalities
10
+
11
+ ### 0.38.0 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.37.4...0.38.0)]
12
+
13
+ New features:
14
+
15
+ - Puppeteer 11.0 functionalities
16
+ * OOP iframe support
17
+ * Customize debugging port
18
+ * HTTPRequest#initiator
19
+ * Customize temp directory for user data
20
+ * Add webp to screenshot quality option allow list
21
+
22
+
23
+ ### 0.37.4 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.37.3...0.37.4)]
24
+
25
+ Bugfix:
26
+
27
+ - Fix ElementHandle#screenshot to work.
28
+
29
+ ### 0.37.3 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.37.2...0.37.3)]
30
+
31
+ Improvement:
32
+
33
+ - Improve the logic of detection of Chrome/Firefox executable path on Linux.
34
+
5
35
  ### 0.37.2 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.37.1...0.37.2)]
6
36
 
7
37
  Bugfix:
data/README.md CHANGED
@@ -41,7 +41,7 @@ NOTE: `require 'puppeteer-ruby'` is not necessary in Rails.
41
41
  ```ruby
42
42
  require 'puppeteer-ruby'
43
43
 
44
- Puppeteer.launch(headless: false, slow_mo: 50, args: ['--guest', '--window-size=1280,800']) do |browser|
44
+ Puppeteer.launch(headless: false, slow_mo: 50, args: ['--window-size=1280,800']) do |browser|
45
45
  page = browser.new_page
46
46
  page.viewport = Puppeteer::Viewport.new(width: 1280, height: 800)
47
47
  page.goto("https://github.com/", wait_until: 'domcontentloaded')
data/docs/api_coverage.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # API coverages
2
- - Puppeteer version: v11.0.0
3
- - puppeteer-ruby version: 0.37.2
2
+ - Puppeteer version: v12.0.0
3
+ - puppeteer-ruby version: 0.39.0
4
4
 
5
5
  ## Puppeteer
6
6
 
@@ -134,7 +134,7 @@
134
134
  * viewport
135
135
  * ~~waitFor~~
136
136
  * waitForFileChooser => `#wait_for_file_chooser`
137
- * ~~waitForFrame~~
137
+ * waitForFrame => `#wait_for_frame`
138
138
  * waitForFunction => `#wait_for_function`
139
139
  * waitForNavigation => `#wait_for_navigation`
140
140
  * ~~waitForNetworkIdle~~
@@ -227,7 +227,7 @@
227
227
  * goto
228
228
  * hover
229
229
  * isDetached => `#detached?`
230
- * ~~isOOPFrame~~
230
+ * isOOPFrame => `#oop_frame?`
231
231
  * name
232
232
  * parentFrame => `#parent_frame`
233
233
  * select
@@ -308,7 +308,7 @@
308
308
  * finalizeInterceptions => `#finalize_interceptions`
309
309
  * frame
310
310
  * headers
311
- * ~~initiator~~
311
+ * initiator
312
312
  * isNavigationRequest => `#navigation_request?`
313
313
  * method
314
314
  * postData => `#post_data`
@@ -360,7 +360,7 @@
360
360
 
361
361
  * connection
362
362
  * detach
363
- * ~~id~~
363
+ * id
364
364
  * send
365
365
 
366
366
  ## Coverage
@@ -7,10 +7,10 @@ class Puppeteer::AriaQueryHandler
7
7
  private def parse_aria_selector(selector)
8
8
  known_attributes = %w(name role)
9
9
  query_options = {}
10
- attribute_regexp = /\[\s*(?<attribute>\w+)\s*=\s*"(?<value>\\.|[^"\\]*)"\s*\]/
10
+ attribute_regexp = /\[\s*(?<attribute>\w+)\s*=\s*(?<quote>"|')(?<value>\\.|.*?(?=\k<quote>))\k<quote>\s*\]/
11
11
  default_name = selector.gsub(attribute_regexp) do
12
12
  attribute = $1.strip
13
- value = $2
13
+ value = $3
14
14
  unless known_attributes.include?(attribute)
15
15
  raise ArgumentError.new("Unkown aria attribute \"#{attribute}\" in selector")
16
16
  end
@@ -39,7 +39,7 @@ class Puppeteer::AriaQueryHandler
39
39
  def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil)
40
40
  binding_function = Puppeteer::DOMWorld::BindingFunction.new(
41
41
  name: 'ariaQuerySelector',
42
- proc: -> (selector) { query_one(dom_world.send(:document), selector) },
42
+ proc: -> (sel) { query_one(dom_world.send(:document), sel) },
43
43
  )
44
44
  dom_world.send(:wait_for_selector_in_page,
45
45
  '(_, selector) => globalThis.ariaQuerySelector(selector)',
@@ -4,13 +4,17 @@ require 'timeout'
4
4
 
5
5
  # https://github.com/puppeteer/puppeteer/blob/master/lib/Launcher.js
6
6
  class Puppeteer::BrowserRunner
7
+ include Puppeteer::DebugPrint
8
+
7
9
  # @param {string} executablePath
8
10
  # @param {!Array<string>} processArguments
9
11
  # @param {string=} tempDirectory
10
- def initialize(executable_path, process_arguments, temp_directory)
12
+ def initialize(for_firefox, executable_path, process_arguments, user_data_dir, using_temp_user_data_dir)
13
+ @for_firefox = for_firefox
11
14
  @executable_path = executable_path
12
15
  @process_arguments = process_arguments
13
- @temp_directory = temp_directory
16
+ @user_data_dir = user_data_dir
17
+ @using_temp_user_data_dir = using_temp_user_data_dir
14
18
  @proc = nil
15
19
  @connection = nil
16
20
  @closed = true
@@ -90,8 +94,8 @@ class Puppeteer::BrowserRunner
90
94
  @process_closing = -> {
91
95
  @proc.dispose
92
96
  @closed = true
93
- if @temp_directory
94
- FileUtils.rm_rf(@temp_directory)
97
+ if @using_temp_user_data_dir
98
+ FileUtils.rm_rf(@user_data_dir)
95
99
  end
96
100
  }
97
101
  at_exit do
@@ -122,7 +126,7 @@ class Puppeteer::BrowserRunner
122
126
  def close
123
127
  return if @closed
124
128
 
125
- if @temp_directory
129
+ if @using_temp_user_data_dir && !@for_firefox
126
130
  kill
127
131
  elsif @connection
128
132
  begin
@@ -137,11 +141,18 @@ class Puppeteer::BrowserRunner
137
141
 
138
142
  # @return {Promise}
139
143
  def kill
140
- if @temp_directory
141
- FileUtils.rm_rf(@temp_directory)
142
- end
143
- unless @closed
144
- @proc.kill
144
+ # If the process failed to launch (for example if the browser executable path
145
+ # is invalid), then the process does not get a pid assigned. A call to
146
+ # `proc.kill` would error, as the `pid` to-be-killed can not be found.
147
+ @proc&.kill
148
+
149
+ # Attempt to remove temporary profile directory to avoid littering.
150
+ begin
151
+ if @using_temp_user_data_dir
152
+ FileUtils.rm_rf(@temp_directory)
153
+ end
154
+ rescue => err
155
+ debug_puts(err)
145
156
  end
146
157
  end
147
158
 
@@ -15,6 +15,11 @@ class Puppeteer::CDPSession
15
15
  @session_id = session_id
16
16
  end
17
17
 
18
+ # @internal
19
+ def id
20
+ @session_id
21
+ end
22
+
18
23
  attr_reader :connection
19
24
 
20
25
  # @param method [String]
@@ -45,10 +45,14 @@ class Puppeteer::DOMWorld
45
45
  end
46
46
  end
47
47
 
48
- # @param {!Puppeteer.FrameManager} frameManager
49
- # @param {!Puppeteer.Frame} frame
50
- # @param {!Puppeteer.TimeoutSettings} timeoutSettings
51
- def initialize(frame_manager, frame, timeout_settings)
48
+ # @param client [Puppeteer::CDPSession]
49
+ # @param frame_manager [Puppeteer::FrameManager]
50
+ # @param frame [Puppeteer::Frame]
51
+ # @param timeout_settings [Puppeteer::TimeoutSettings]
52
+ def initialize(client, frame_manager, frame, timeout_settings)
53
+ # Keep own reference to client because it might differ from the FrameManager's
54
+ # client for OOP iframes.
55
+ @client = client
52
56
  @frame_manager = frame_manager
53
57
  @frame = frame
54
58
  @timeout_settings = timeout_settings
@@ -58,7 +62,7 @@ class Puppeteer::DOMWorld
58
62
  @ctx_bindings = Set.new
59
63
  @detached = false
60
64
 
61
- frame_manager.client.on_event('Runtime.bindingCalled', &method(:handle_binding_called))
65
+ @client.on_event('Runtime.bindingCalled', &method(:handle_binding_called))
62
66
  end
63
67
 
64
68
  attr_reader :frame
@@ -323,7 +323,7 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
323
323
  end
324
324
  end
325
325
 
326
- def screenshot(options = {})
326
+ def screenshot(type: nil, path: nil, full_page: nil, clip: nil, quality: nil, omit_background: nil, encoding: nil)
327
327
  needs_viewport_reset = false
328
328
 
329
329
  box = bounding_box
@@ -358,14 +358,16 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
358
358
  page_x = layout_metrics["layoutViewport"]["pageX"]
359
359
  page_y = layout_metrics["layoutViewport"]["pageY"]
360
360
 
361
- clip = {
362
- x: page_x + box.x,
363
- y: page_y + box.y,
364
- width: box.width,
365
- height: box.height,
366
- }
361
+ if clip.nil?
362
+ clip = {
363
+ x: page_x + box.x,
364
+ y: page_y + box.y,
365
+ width: box.width,
366
+ height: box.height,
367
+ }
368
+ end
367
369
 
368
- @page.screenshot({ clip: clip }.merge(options))
370
+ @page.screenshot(type: type, path: path, full_page: full_page, clip: clip, quality: quality, omit_background: omit_background, encoding: encoding)
369
371
  ensure
370
372
  if needs_viewport_reset
371
373
  @page.viewport = viewport
@@ -0,0 +1,28 @@
1
+ class Puppeteer::ExecutablePathFinder
2
+ # @param executable_names [Array<String>] executable file names to find.
3
+ def initialize(*executable_names)
4
+ @executable_names = executable_names
5
+ end
6
+
7
+ def find_executables_in_path
8
+ Enumerator.new do |result|
9
+ @executable_names.each do |name|
10
+ # Find the first existing path.
11
+ paths.each do |path|
12
+ candidate = File.join(path, name)
13
+ next unless File.exist?(candidate)
14
+ result << candidate
15
+ break
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ def find_first
22
+ find_executables_in_path.first
23
+ end
24
+
25
+ private def paths
26
+ ENV['PATH'].split(File::PATH_SEPARATOR)
27
+ end
28
+ end
@@ -1,25 +1,47 @@
1
1
  class Puppeteer::Frame
2
2
  using Puppeteer::DefineAsyncMethod
3
3
 
4
- # @param {!FrameManager} frameManager
5
- # @param {!Puppeteer.CDPSession} client
6
- # @param {?Frame} parentFrame
7
- # @param {string} frameId
8
- def initialize(frame_manager, client, parent_frame, frame_id)
4
+ # @param frame_manager [Puppeteer::FrameManager]
5
+ # @param parent_frame [Puppeteer::Frame|nil]
6
+ # @param frame_id [String]
7
+ # @param client [Puppeteer::CDPSession]
8
+ def initialize(frame_manager, parent_frame, frame_id, client)
9
9
  @frame_manager = frame_manager
10
- @client = client
11
10
  @parent_frame = parent_frame
12
11
  @id = frame_id
13
12
  @detached = false
14
13
 
15
14
  @loader_id = ''
16
15
  @lifecycle_events = Set.new
17
- @main_world = Puppeteer::DOMWorld.new(frame_manager, self, frame_manager.timeout_settings)
18
- @secondary_world = Puppeteer::DOMWorld.new(frame_manager, self, frame_manager.timeout_settings)
19
16
  @child_frames = Set.new
20
17
  if parent_frame
21
18
  parent_frame._child_frames << self
22
19
  end
20
+
21
+ update_client(client)
22
+ end
23
+
24
+ def inspect
25
+ values = %i[id parent_frame detached loader_id lifecycle_events child_frames].map do |sym|
26
+ value = instance_variable_get(:"@#{sym}")
27
+ "@#{sym}=#{value}"
28
+ end
29
+ "#<Puppeteer::Frame #{values.join(' ')}>"
30
+ end
31
+
32
+ def _client
33
+ @client
34
+ end
35
+
36
+ # @param client [Puppeteer::CDPSession]
37
+ private def update_client(client)
38
+ @client = client
39
+ @main_world = Puppeteer::DOMWorld.new(@client, @frame_manager, self, @frame_manager.timeout_settings)
40
+ @secondary_world = Puppeteer::DOMWorld.new(@client, @frame_manager, self, @frame_manager.timeout_settings)
41
+ end
42
+
43
+ def oop_frame?
44
+ @client != @frame_manager.client
23
45
  end
24
46
 
25
47
  attr_accessor :frame_manager, :id, :loader_id, :lifecycle_events, :main_world, :secondary_world
@@ -27,50 +27,67 @@ class Puppeteer::FrameManager
27
27
  # @type {!Set<string>}
28
28
  @isolated_worlds = Set.new
29
29
 
30
- @client.on_event('Page.frameAttached') do |event|
31
- handle_frame_attached(event['frameId'], event['parentFrameId'])
30
+ setup_listeners(@client)
31
+ end
32
+
33
+ private def setup_listeners(client)
34
+ client.on_event('Page.frameAttached') do |event|
35
+ handle_frame_attached(client, event['frameId'], event['parentFrameId'])
32
36
  end
33
- @client.on_event('Page.frameNavigated') do |event|
37
+ client.on_event('Page.frameNavigated') do |event|
34
38
  handle_frame_navigated(event['frame'])
35
39
  end
36
- @client.on_event('Page.navigatedWithinDocument') do |event|
40
+ client.on_event('Page.navigatedWithinDocument') do |event|
37
41
  handle_frame_navigated_within_document(event['frameId'], event['url'])
38
42
  end
39
- @client.on_event('Page.frameDetached') do |event|
40
- handle_frame_detached(event['frameId'])
43
+ client.on_event('Page.frameDetached') do |event|
44
+ handle_frame_detached(event['frameId'], event['reason'])
41
45
  end
42
- @client.on_event('Page.frameStoppedLoading') do |event|
46
+ client.on_event('Page.frameStoppedLoading') do |event|
43
47
  handle_frame_stopped_loading(event['frameId'])
44
48
  end
45
- @client.on_event('Runtime.executionContextCreated') do |event|
46
- handle_execution_context_created(event['context'])
49
+ client.on_event('Runtime.executionContextCreated') do |event|
50
+ handle_execution_context_created(event['context'], client)
47
51
  end
48
- @client.on_event('Runtime.executionContextDestroyed') do |event|
49
- handle_execution_context_destroyed(event['executionContextId'])
52
+ client.on_event('Runtime.executionContextDestroyed') do |event|
53
+ handle_execution_context_destroyed(event['executionContextId'], client)
50
54
  end
51
- @client.on_event('Runtime.executionContextsCleared') do |event|
52
- handle_execution_contexts_cleared
55
+ client.on_event('Runtime.executionContextsCleared') do |event|
56
+ handle_execution_contexts_cleared(client)
53
57
  end
54
- @client.on_event('Page.lifecycleEvent') do |event|
58
+ client.on_event('Page.lifecycleEvent') do |event|
55
59
  handle_lifecycle_event(event)
56
60
  end
61
+ client.on_event('Target.attachedToTarget') do |event|
62
+ handle_attached_to_target(event)
63
+ end
64
+ client.on_event('Target.detachedFromTarget') do |event|
65
+ handle_detached_from_target(event)
66
+ end
57
67
  end
58
68
 
59
69
  attr_reader :client, :timeout_settings
60
70
 
61
- private def init
71
+ private def init(cdp_session = nil)
72
+ client = cdp_session || @client
73
+
62
74
  results = await_all(
63
- @client.async_send_message('Page.enable'),
64
- @client.async_send_message('Page.getFrameTree'),
75
+ client.async_send_message('Page.enable'),
76
+ client.async_send_message('Page.getFrameTree'),
65
77
  )
66
78
  frame_tree = results.last['frameTree']
67
- handle_frame_tree(frame_tree)
79
+ handle_frame_tree(client, frame_tree)
68
80
  await_all(
69
- @client.async_send_message('Page.setLifecycleEventsEnabled', enabled: true),
70
- @client.async_send_message('Runtime.enable'),
81
+ client.async_send_message('Page.setLifecycleEventsEnabled', enabled: true),
82
+ client.async_send_message('Runtime.enable'),
71
83
  )
72
- ensure_isolated_world(UTILITY_WORLD_NAME)
73
- @network_manager.init
84
+ ensure_isolated_world(client, UTILITY_WORLD_NAME)
85
+ @network_manager.init unless cdp_session
86
+ rescue => err
87
+ # The target might have been closed before the initialization finished.
88
+ return if err.message.include?('Target closed') || err.message.include?('Session closed')
89
+
90
+ raise
74
91
  end
75
92
 
76
93
  define_async_method :async_init
@@ -154,6 +171,28 @@ class Puppeteer::FrameManager
154
171
  watcher.navigation_response
155
172
  end
156
173
 
174
+ # @param event [Hash]
175
+ def handle_attached_to_target(event)
176
+ return if event['targetInfo']['type'] != 'iframe'
177
+
178
+ frame = @frames[event['targetInfo']['targetId']]
179
+ session = Puppeteer::Connection.from_session(@client).session(event['sessionId'])
180
+
181
+ frame.send(:update_client, session)
182
+ setup_listeners(session)
183
+ async_init(session)
184
+ end
185
+
186
+ # @param event [Hash]
187
+ def handle_detached_from_target(event)
188
+ frame = @frames[event['targetId']]
189
+ if frame && frame.oop_frame?
190
+ # When an OOP iframe is removed from the page, it
191
+ # will only get a Target.detachedFromTarget event.
192
+ remove_frame_recursively(frame)
193
+ end
194
+ end
195
+
157
196
  # @param event [Hash]
158
197
  def handle_lifecycle_event(event)
159
198
  frame = @frames[event['frameId']]
@@ -170,16 +209,17 @@ class Puppeteer::FrameManager
170
209
  emit_event(FrameManagerEmittedEvents::LifecycleEvent, frame)
171
210
  end
172
211
 
212
+ # @param session [Puppeteer::CDPSession]
173
213
  # @param frame_tree [Hash]
174
- def handle_frame_tree(frame_tree)
214
+ def handle_frame_tree(session, frame_tree)
175
215
  if frame_tree['frame']['parentId']
176
- handle_frame_attached(frame_tree['frame']['id'], frame_tree['frame']['parentId'])
216
+ handle_frame_attached(session, frame_tree['frame']['id'], frame_tree['frame']['parentId'])
177
217
  end
178
218
  handle_frame_navigated(frame_tree['frame'])
179
219
  return if !frame_tree['childFrames']
180
220
 
181
221
  frame_tree['childFrames'].each do |child|
182
- handle_frame_tree(child)
222
+ handle_frame_tree(session, child)
183
223
  end
184
224
  end
185
225
 
@@ -204,15 +244,25 @@ class Puppeteer::FrameManager
204
244
  @frames[frame_id]
205
245
  end
206
246
 
207
- # @param {string} frameId
208
- # @param {?string} parentFrameId
209
- def handle_frame_attached(frame_id, parent_frame_id)
210
- return if @frames.has_key?(frame_id)
247
+ # @param session [Puppeteer::CDPSession]
248
+ # @param frameId [String]
249
+ # @param parentFrameId [String|nil]
250
+ def handle_frame_attached(session, frame_id, parent_frame_id)
251
+ if @frames.has_key?(frame_id)
252
+ frame = @frames[frame_id]
253
+ if session && frame.oop_frame?
254
+ # If an OOP iframes becomes a normal iframe again
255
+ # it is first attached to the parent page before
256
+ # the target is removed.
257
+ frame.send(:update_client, session)
258
+ end
259
+ return
260
+ end
211
261
  if !parent_frame_id
212
262
  raise ArgymentError.new('parent_frame_id must not be nil')
213
263
  end
214
264
  parent_frame = @frames[parent_frame_id]
215
- frame = Puppeteer::Frame.new(self, @client, parent_frame, frame_id)
265
+ frame = Puppeteer::Frame.new(self, parent_frame, frame_id, session)
216
266
  @frames[frame_id] = frame
217
267
 
218
268
  emit_event(FrameManagerEmittedEvents::FrameAttached, frame)
@@ -247,7 +297,7 @@ class Puppeteer::FrameManager
247
297
  frame.id = frame_payload['id']
248
298
  else
249
299
  # Initial main frame navigation.
250
- frame = Puppeteer::Frame.new(self, @client, nil, frame_payload['id'])
300
+ frame = Puppeteer::Frame.new(self, nil, frame_payload['id'], @client)
251
301
  end
252
302
  @frames[frame_payload['id']] = frame
253
303
  @main_frame = frame
@@ -259,22 +309,26 @@ class Puppeteer::FrameManager
259
309
  emit_event(FrameManagerEmittedEvents::FrameNavigated, frame)
260
310
  end
261
311
 
312
+ # @param session [Puppeteer::CDPSession]
262
313
  # @param name [String]
263
- def ensure_isolated_world(name)
264
- return if @isolated_worlds.include?(name)
265
- @isolated_worlds << name
314
+ private def ensure_isolated_world(session, name)
315
+ key = "#{session.id}:#{name}"
316
+ return if @isolated_worlds.include?(key)
317
+ @isolated_worlds << key
266
318
 
267
- @client.send_message('Page.addScriptToEvaluateOnNewDocument',
319
+ session.send_message('Page.addScriptToEvaluateOnNewDocument',
268
320
  source: "//# sourceURL=#{Puppeteer::ExecutionContext::EVALUATION_SCRIPT_URL}",
269
321
  worldName: name,
270
322
  )
271
- create_isolated_worlds_promises = frames.map do |frame|
272
- @client.async_send_message('Page.createIsolatedWorld',
273
- frameId: frame.id,
274
- grantUniveralAccess: true,
275
- worldName: name,
276
- )
277
- end
323
+ create_isolated_worlds_promises = frames.
324
+ select { |frame| frame._client == session }.
325
+ map do |frame|
326
+ session.async_send_message('Page.createIsolatedWorld',
327
+ frameId: frame.id,
328
+ grantUniveralAccess: true,
329
+ worldName: name,
330
+ )
331
+ end
278
332
  await_all(*create_isolated_worlds_promises)
279
333
  end
280
334
 
@@ -289,19 +343,31 @@ class Puppeteer::FrameManager
289
343
  end
290
344
 
291
345
  # @param frame_id [String]
292
- def handle_frame_detached(frame_id)
346
+ # @param reason [String]
347
+ def handle_frame_detached(frame_id, reason)
293
348
  frame = @frames[frame_id]
294
- if frame
295
- remove_frame_recursively(frame)
349
+ if reason == 'remove'
350
+ # Only remove the frame if the reason for the detached event is
351
+ # an actual removement of the frame.
352
+ # For frames that become OOP iframes, the reason would be 'swap'.
353
+ if frame
354
+ remove_frame_recursively(frame)
355
+ end
296
356
  end
297
357
  end
298
358
 
299
359
  # @param context_payload [Hash]
300
- def handle_execution_context_created(context_payload)
360
+ # @pram session [Puppeteer::CDPSession]
361
+ def handle_execution_context_created(context_payload, session)
301
362
  frame = if_present(context_payload.dig('auxData', 'frameId')) { |frame_id| @frames[frame_id] }
302
363
 
303
364
  world = nil
304
365
  if frame
366
+ # commented out the original implementation for allowing us to use Frame#evaluate on OOP iframe.
367
+ #
368
+ # # Only care about execution contexts created for the current session.
369
+ # return if @client != session
370
+
305
371
  if context_payload.dig('auxData', 'isDefault')
306
372
  world = frame.main_world
307
373
  elsif context_payload['name'] == UTILITY_WORLD_NAME && !frame.secondary_world.has_context?
@@ -316,34 +382,45 @@ class Puppeteer::FrameManager
316
382
  @isolated_worlds << context_payload['name']
317
383
  end
318
384
 
319
- context = Puppeteer::ExecutionContext.new(@client, context_payload, world)
385
+ context = Puppeteer::ExecutionContext.new(frame&._client || @client, context_payload, world)
320
386
  if world
321
387
  world.context = context
322
388
  end
323
- @context_id_to_context[context_payload['id']] = context
389
+ key = "#{session.id}:#{context_payload['id']}"
390
+ @context_id_to_context[key] = context
324
391
  end
325
392
 
326
- # @param {number} executionContextId
327
- def handle_execution_context_destroyed(execution_context_id)
328
- context = @context_id_to_context[execution_context_id]
393
+ # @param execution_context_id [Integer]
394
+ # @param session [Puppeteer::CDPSEssion]
395
+ def handle_execution_context_destroyed(execution_context_id, session)
396
+ key = "#{session.id}:#{execution_context_id}"
397
+ context = @context_id_to_context[key]
329
398
  return unless context
330
- @context_id_to_context.delete(execution_context_id)
399
+ @context_id_to_context.delete(key)
331
400
  if context.world
332
401
  context.world.delete_context(execution_context_id)
333
402
  end
334
403
  end
335
404
 
336
- def handle_execution_contexts_cleared
337
- @context_id_to_context.each do |execution_context_id, context|
338
- if context.world
339
- context.world.delete_context(execution_context_id)
405
+ # @param session [Puppeteer::CDPSession]
406
+ def handle_execution_contexts_cleared(session)
407
+ @context_id_to_context.select! do |execution_context_id, context|
408
+ # Make sure to only clear execution contexts that belong
409
+ # to the current session.
410
+ if context.client != session
411
+ true # keep
412
+ else
413
+ if context.world
414
+ context.world.delete_context(execution_context_id)
415
+ end
416
+ false # remove
340
417
  end
341
418
  end
342
- @context_id_to_context.clear
343
419
  end
344
420
 
345
- def execution_context_by_id(context_id)
346
- @context_id_to_context[context_id] or raise "INTERNAL ERROR: missing context with id = #{context_id}"
421
+ def execution_context_by_id(context_id, session)
422
+ key = "#{session.id}:#{context_id}"
423
+ @context_id_to_context[key] or raise "INTERNAL ERROR: missing context with id = #{context_id}"
347
424
  end
348
425
 
349
426
  # @param {!Frame} frame