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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b06e1f5f867ef4e7d648e93becc953f660bb707b29019ffe6e93f8e9f02acc4
4
- data.tar.gz: ff082c6ba661e0f2d8b0d8bc0f92f5aeecf036f9edcd7bb6f8937db4fd019947
3
+ metadata.gz: da17d477ad97a3197fa0eacf237e784204439efdca104e2a304d5cfb58c449d3
4
+ data.tar.gz: f56c151a8d7ebd8ac0ff71feafe5a90dcdd14ed997aaa9ea8e198ddd345b93dd
5
5
  SHA512:
6
- metadata.gz: 19b67fb8ef13bac90538e8755905749427a7131bba5088c16f512b51d72833d80ee296ea7537b5122bb531a28d3c95c7924cd199dc6343ab17a1b3d2c391af9b
7
- data.tar.gz: c65cb7780075ddea13c60b6c668b8a9a6d53627a8dbfa263f08694777ff10f1a3f3a98eeb2596a6497e0274a3a6d6aafc24a215ee6873c07485dc400444c0c17
6
+ metadata.gz: 3db33eea6388dd6c743395c9e3cd583d95a3f2147627ae371390e40bf6269f32d689687e8adf0fdaef8b99134b6bad2ee55061ca66c037407ec681ee62b61188
7
+ data.tar.gz: 2b169890fd8c5dde6850fff2439e5d43c03fa1b65e906ce443389c0599cf31d0e88af1ca0203c12cff90b2789c14af7a992d1c6b95caf7e78b9dadbf4d4a0f09
data/CHANGELOG.md CHANGED
@@ -1,7 +1,11 @@
1
- ### main [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.42.0...main)]
1
+ ### main [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.43.0...main)]
2
2
 
3
3
  - xxx
4
4
 
5
+ ### 0.43.0 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.42.0...0.43.0)]
6
+
7
+ - Port Puppeteer v16.0 features. Increasing stability.
8
+
5
9
  ### 0.42.0 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.41.0...0.42.0)]
6
10
 
7
11
  - Port Puppeteer v15.3-v15.5 features, including `Frame#page`
data/docs/api_coverage.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # API coverages
2
- - Puppeteer version: v15.5.0
3
- - puppeteer-ruby version: 0.42.0
2
+ - Puppeteer version: v16.2.0
3
+ - puppeteer-ruby version: 0.43.0
4
4
 
5
5
  ## Puppeteer
6
6
 
@@ -7,13 +7,15 @@ class Puppeteer::Browser
7
7
  include Puppeteer::IfPresent
8
8
  using Puppeteer::DefineAsyncMethod
9
9
 
10
+ # @param product [String|nil] 'chrome' or 'firefox'
10
11
  # @param {!Puppeteer.Connection} connection
11
12
  # @param {!Array<string>} contextIds
12
13
  # @param {boolean} ignoreHTTPSErrors
13
14
  # @param {?Puppeteer.Viewport} defaultViewport
14
15
  # @param process [Puppeteer::BrowserRunner::BrowserProcess|NilClass]
15
16
  # @param {function()=} closeCallback
16
- def self.create(connection:,
17
+ def self.create(product:,
18
+ connection:,
17
19
  context_ids:,
18
20
  ignore_https_errors:,
19
21
  default_viewport:,
@@ -22,6 +24,7 @@ class Puppeteer::Browser
22
24
  target_filter_callback:,
23
25
  is_page_target_callback:)
24
26
  browser = Puppeteer::Browser.new(
27
+ product: product,
25
28
  connection: connection,
26
29
  context_ids: context_ids,
27
30
  ignore_https_errors: ignore_https_errors,
@@ -31,17 +34,19 @@ class Puppeteer::Browser
31
34
  target_filter_callback: target_filter_callback,
32
35
  is_page_target_callback: is_page_target_callback,
33
36
  )
34
- connection.send_message('Target.setDiscoverTargets', discover: true)
37
+ browser.send(:attach)
35
38
  browser
36
39
  end
37
40
 
41
+ # @param product [String|nil] 'chrome' or 'firefox'
38
42
  # @param {!Puppeteer.Connection} connection
39
43
  # @param {!Array<string>} contextIds
40
44
  # @param {boolean} ignoreHTTPSErrors
41
45
  # @param {?Puppeteer.Viewport} defaultViewport
42
46
  # @param {?Puppeteer.ChildProcess} process
43
47
  # @param {(function():Promise)=} closeCallback
44
- def initialize(connection:,
48
+ def initialize(product:,
49
+ connection:,
45
50
  context_ids:,
46
51
  ignore_https_errors:,
47
52
  default_viewport:,
@@ -49,6 +54,7 @@ class Puppeteer::Browser
49
54
  close_callback:,
50
55
  target_filter_callback:,
51
56
  is_page_target_callback:)
57
+ @product = product || 'chrome'
52
58
  @ignore_https_errors = ignore_https_errors
53
59
  @default_viewport = default_viewport
54
60
  @process = process
@@ -56,20 +62,26 @@ class Puppeteer::Browser
56
62
  @close_callback = close_callback
57
63
  @target_filter_callback = target_filter_callback || method(:default_target_filter_callback)
58
64
  @is_page_target_callback = is_page_target_callback || method(:default_is_page_target_callback)
59
-
60
65
  @default_context = Puppeteer::BrowserContext.new(@connection, self, nil)
61
66
  @contexts = {}
67
+
62
68
  context_ids.each do |context_id|
63
69
  @contexts[context_id] = Puppeteer::BrowserContext.new(@connection, self, context_id)
64
70
  end
65
- @targets = {}
66
- @wait_for_creating_targets = {}
67
- @connection.on_event(ConnectionEmittedEvents::Disconnected) do
68
- emit_event(BrowserEmittedEvents::Disconnected)
71
+
72
+ if @product == 'firefox'
73
+ @target_manager = Puppeteer::FirefoxTargetManager.new(
74
+ connection: connection,
75
+ target_factory: method(:create_target),
76
+ target_filter_callback: @target_filter_callback,
77
+ )
78
+ else
79
+ @target_manager = Puppeteer::ChromeTargetManager.new(
80
+ connection: connection,
81
+ target_factory: method(:create_target),
82
+ target_filter_callback: @target_filter_callback,
83
+ )
69
84
  end
70
- @connection.on_event('Target.targetCreated', &method(:handle_target_created))
71
- @connection.on_event('Target.targetDestroyed', &method(:handle_target_destroyed))
72
- @connection.on_event('Target.targetInfoChanged', &method(:handle_target_info_changed))
73
85
  end
74
86
 
75
87
  private def default_target_filter_callback(target_info)
@@ -100,11 +112,45 @@ class Puppeteer::Browser
100
112
  super(event_name.to_s, &block)
101
113
  end
102
114
 
115
+ private def attach
116
+ @connection_event_listeners ||= []
117
+ @connection_event_listeners << @connection.add_event_listener(ConnectionEmittedEvents::Disconnected) do
118
+ emit_event(BrowserEmittedEvents::Disconnected)
119
+ end
120
+ @target_manager_event_listeners ||= []
121
+ @target_manager.add_event_listener(
122
+ TargetManagerEmittedEvents::TargetAvailable,
123
+ &method(:handle_attached_to_target)
124
+ )
125
+ @target_manager.add_event_listener(
126
+ TargetManagerEmittedEvents::TargetGone,
127
+ &method(:handle_detached_from_target)
128
+ )
129
+ @target_manager.add_event_listener(
130
+ TargetManagerEmittedEvents::TargetChanged,
131
+ &method(:handle_target_changed)
132
+ )
133
+ @target_manager.add_event_listener(
134
+ TargetManagerEmittedEvents::TargetDiscovered,
135
+ &method(:handle_target_discovered)
136
+ )
137
+ @target_manager.init
138
+ end
139
+
140
+ private def detach
141
+ @connection.remove_event_listener(*@connection_event_listeners)
142
+ @target_manager.remove_event_listener(*@target_manager_event_listeners)
143
+ end
144
+
103
145
  # @return [Puppeteer::BrowserRunner::BrowserProcess]
104
146
  def process
105
147
  @process
106
148
  end
107
149
 
150
+ private def target_manager
151
+ @target_manager
152
+ end
153
+
108
154
  # @return [Puppeteer::BrowserContext]
109
155
  def create_incognito_browser_context
110
156
  result = @connection.send_message('Target.createBrowserContext')
@@ -123,19 +169,16 @@ class Puppeteer::Browser
123
169
 
124
170
  # @param context_id [String]
125
171
  def dispose_context(context_id)
172
+ return unless context_id
126
173
  @connection.send_message('Target.disposeBrowserContext', browserContextId: context_id)
127
174
  @contexts.delete(context_id)
128
175
  end
129
176
 
130
- class TargetAlreadyExistError < StandardError
131
- def initialize
132
- super('Target should not exist before targetCreated')
133
- end
134
- end
177
+ class MissingBrowserContextError < StandardError ; end
135
178
 
136
- # @param {!Protocol.Target.targetCreatedPayload} event
137
- def handle_target_created(event)
138
- target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
179
+ # @param target_info [Puppeteer::Target::TargetInfo]
180
+ # @param session [CDPSession|nil]
181
+ def create_target(target_info, session)
139
182
  browser_context_id = target_info.browser_context_id
140
183
  context =
141
184
  if browser_context_id && @contexts.has_key?(browser_context_id)
@@ -144,56 +187,39 @@ class Puppeteer::Browser
144
187
  @default_context
145
188
  end
146
189
 
147
- if @targets[target_info.target_id]
148
- raise TargetAlreadyExistError.new
190
+ unless context
191
+ raise MissingBrowserContextError.new('Missing browser context')
149
192
  end
150
193
 
151
- return unless @target_filter_callback.call(target_info)
152
-
153
- target = Puppeteer::Target.new(
194
+ Puppeteer::Target.new(
154
195
  target_info: target_info,
196
+ session: session,
155
197
  browser_context: context,
198
+ target_manager: @target_manager,
156
199
  session_factory: -> { @connection.create_session(target_info) },
157
200
  ignore_https_errors: @ignore_https_errors,
158
201
  default_viewport: @default_viewport,
159
202
  is_page_target_callback: @is_page_target_callback,
160
203
  )
161
- @targets[target_info.target_id] = target
162
- if_present(@wait_for_creating_targets.delete(target_info.target_id)) do |promise|
163
- promise.fulfill(target)
164
- end
165
- if await target.initialized_promise
204
+ end
205
+
206
+ private def handle_attached_to_target(target)
207
+ if target.initialized_promise.value!
166
208
  emit_event(BrowserEmittedEvents::TargetCreated, target)
167
- context.emit_event(BrowserContextEmittedEvents::TargetCreated, target)
209
+ target.browser_context.emit_event(BrowserContextEmittedEvents::TargetCreated, target)
168
210
  end
169
211
  end
170
212
 
171
- # @param {{targetId: string}} event
172
- def handle_target_destroyed(event)
173
- target_id = event['targetId']
174
- target = @targets[target_id]
213
+ private def handle_detached_from_target(target)
175
214
  target.ignore_initialize_callback_promise
176
- @targets.delete(target_id)
177
- if_present(@wait_for_creating_targets.delete(target_id)) do |promise|
178
- promise.reject('target destroyed')
179
- end
180
215
  target.closed_callback
181
- if await target.initialized_promise
216
+ if target.initialized_promise.value!
182
217
  emit_event(BrowserEmittedEvents::TargetDestroyed, target)
183
218
  target.browser_context.emit_event(BrowserContextEmittedEvents::TargetDestroyed, target)
184
219
  end
185
220
  end
186
221
 
187
- class TargetNotExistError < StandardError
188
- def initialize
189
- super('target should exist before targetInfoChanged')
190
- end
191
- end
192
-
193
- # @param {!Protocol.Target.targetInfoChangedPayload} event
194
- def handle_target_info_changed(event)
195
- target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo'])
196
- target = @targets[target_info.target_id] or raise TargetNotExistError.new
222
+ private def handle_target_changed(target, target_info)
197
223
  previous_url = target.url
198
224
  was_initialized = target.initialized?
199
225
  target.handle_target_info_changed(target_info)
@@ -203,6 +229,10 @@ class Puppeteer::Browser
203
229
  end
204
230
  end
205
231
 
232
+ private def handle_target_discovered(target_info)
233
+ emit_event('targetdiscovered', target_info)
234
+ end
235
+
206
236
  # @return [String]
207
237
  def ws_endpoint
208
238
  @connection.url
@@ -212,44 +242,47 @@ class Puppeteer::Browser
212
242
  @default_context.new_page
213
243
  end
214
244
 
245
+ class MissingTargetError < StandardError ; end
246
+ class CreatePageError < StandardError ; end
247
+
215
248
  # @param {?string} contextId
216
249
  # @return {!Promise<!Puppeteer.Page>}
217
250
  def create_page_in_context(context_id)
218
- create_target_params = { url: 'about:blank' }
219
- if context_id
220
- create_target_params[:browserContextId] = context_id
221
- end
251
+ create_target_params = {
252
+ url: 'about:blank',
253
+ browserContextId: context_id,
254
+ }.compact
222
255
  result = @connection.send_message('Target.createTarget', **create_target_params)
223
256
  target_id = result['targetId']
224
- target = @targets[target_id]
257
+ target = @target_manager.available_targets[target_id]
225
258
  unless target
226
- # Target.targetCreated is often notified before the response of Target.createdTarget.
227
- # https://github.com/YusukeIwaki/puppeteer-ruby/issues/91
228
- # D, [2021-04-07T03:00:10.125241 #187] DEBUG -- : SEND >> {"method":"Target.createTarget","params":{"url":"about:blank","browserContextId":"56A86FC3391B50180CF9A6450A0D8C21"},"id":3}
229
- # D, [2021-04-07T03:00:10.142396 #187] DEBUG -- : RECV << {"id"=>3, "result"=>{"targetId"=>"A518447C415A1A3E1A8979454A155632"}}
230
- # D, [2021-04-07T03:00:10.145360 #187] DEBUG -- : RECV << {"method"=>"Target.targetCreated", "params"=>{"targetInfo"=>{"targetId"=>"A518447C415A1A3E1A8979454A155632", "type"=>"page", "title"=>"", "url"=>"", "attached"=>false, "canAccessOpener"=>false, "browserContextId"=>"56A86FC3391B50180CF9A6450A0D8C21"}}}
231
- # This is just a workaround logic...
232
- @wait_for_creating_targets[target_id] = resolvable_future
233
- target = await @wait_for_creating_targets[target_id]
259
+ raise MissingTargetError.new("Missing target for page (id = #{target_id})")
260
+ end
261
+ unless target.initialized_promise.value!
262
+ raise CreatePageError.new("Failed to create target for page (id = #{target_id})")
263
+ end
264
+ page = target.page
265
+ unless page
266
+ raise CreatePageError.new("Failed to create a page for context (id = #{context_id})")
234
267
  end
235
- await target.initialized_promise
236
- await target.page
268
+ page
237
269
  end
238
270
 
239
- # @return {!Array<!Target>}
271
+ # All active targets inside the Browser. In case of multiple browser contexts, returns
272
+ # an array with all the targets in all browser contexts.
240
273
  def targets
241
- @targets.values.select { |target| target.initialized? }
274
+ @target_manager.available_targets.values.select { |target| target.initialized? }
242
275
  end
243
276
 
244
277
 
245
- # @return {!Target}
278
+ # The target associated with the browser.
246
279
  def target
247
- targets.find { |target| target.type == 'browser' }
280
+ targets.find { |target| target.type == 'browser' } or raise 'Browser target is not found'
248
281
  end
249
282
 
250
283
  # used only in Target#opener
251
284
  private def find_target_by_id(target_id)
252
- @targets[target_id]
285
+ @target_manager.available_targets[target_id]
253
286
  end
254
287
 
255
288
  # @param predicate [Proc(Puppeteer::Target -> Boolean)]
@@ -293,12 +326,12 @@ class Puppeteer::Browser
293
326
 
294
327
  # @return [String]
295
328
  def version
296
- get_version.product
329
+ Version.fetch(@connection).product
297
330
  end
298
331
 
299
332
  # @return [String]
300
333
  def user_agent
301
- get_version.user_agent
334
+ Version.fetch(@connection).user_agent
302
335
  end
303
336
 
304
337
  def close
@@ -307,6 +340,7 @@ class Puppeteer::Browser
307
340
  end
308
341
 
309
342
  def disconnect
343
+ @target_manager.dispose
310
344
  @connection.dispose
311
345
  end
312
346
 
@@ -315,6 +349,10 @@ class Puppeteer::Browser
315
349
  end
316
350
 
317
351
  class Version
352
+ def self.fetch(connection)
353
+ new(connection.send_message('Browser.getVersion'))
354
+ end
355
+
318
356
  def initialize(hash)
319
357
  @protocol_version = hash['protocolVersion']
320
358
  @product = hash['product']
@@ -325,8 +363,4 @@ class Puppeteer::Browser
325
363
 
326
364
  attr_reader :protocol_version, :product, :revision, :user_agent, :js_version
327
365
  end
328
-
329
- private def get_version
330
- Version.new(@connection.send_message('Browser.getVersion'))
331
- end
332
366
  end
@@ -0,0 +1,67 @@
1
+ require_relative './browser'
2
+ require_relative './launcher/browser_options'
3
+
4
+ class Puppeteer::BrowserConnector
5
+ def initialize(options)
6
+ @browser_options = Puppeteer::Launcher::BrowserOptions.new(options)
7
+ @browser_ws_endpoint = options[:browser_ws_endpoint]
8
+ @browser_url = options[:browser_url]
9
+ @transport = options[:transport]
10
+ end
11
+
12
+ # @return [Puppeteer::Browser]
13
+ def connect_to_browser
14
+ version = Puppeteer::Browser::Version.fetch(connection)
15
+ product = version.product.downcase.include?('firefox') ? 'firefox' : 'chrome'
16
+
17
+ result = connection.send_message('Target.getBrowserContexts')
18
+ browser_context_ids = result['browserContextIds']
19
+
20
+ Puppeteer::Browser.create(
21
+ product: product,
22
+ connection: connection,
23
+ context_ids: browser_context_ids,
24
+ ignore_https_errors: @browser_options.ignore_https_errors?,
25
+ default_viewport: @browser_options.default_viewport,
26
+ process: nil,
27
+ close_callback: -> { connection.send_message('Browser.close') },
28
+ target_filter_callback: @browser_options.target_filter,
29
+ is_page_target_callback: @browser_options.is_page_target,
30
+ )
31
+ end
32
+
33
+ private def connection
34
+ @connection ||=
35
+ if @browser_ws_endpoint && @browser_url.nil? && @transport.nil?
36
+ connect_with_browser_ws_endpoint(@browser_ws_endpoint)
37
+ elsif @browser_ws_endpoint.nil? && @browser_url && @transport.nil?
38
+ connect_with_browser_url(@browser_url)
39
+ elsif @browser_ws_endpoint.nil? && @browser_url.nil? && @transport
40
+ connect_with_transport(@transport)
41
+ else
42
+ raise ArgumentError.new("Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect")
43
+ end
44
+ end
45
+
46
+ # @return [Puppeteer::Connection]
47
+ private def connect_with_browser_ws_endpoint(browser_ws_endpoint)
48
+ transport = Puppeteer::WebSocketTransport.create(browser_ws_endpoint)
49
+ Puppeteer::Connection.new(browser_ws_endpoint, transport, @browser_options.slow_mo)
50
+ end
51
+
52
+ # @return [Puppeteer::Connection]
53
+ private def connect_with_browser_url(browser_url)
54
+ require 'net/http'
55
+ uri = URI(browser_url)
56
+ uri.path = '/json/version'
57
+ response_body = Net::HTTP.get(uri)
58
+ json = JSON.parse(response_body)
59
+ connection_url = json['webSocketDebuggerUrl']
60
+ connect_with_browser_ws_endpoint(connection_url)
61
+ end
62
+
63
+ # @return [Puppeteer::Connection]
64
+ private def connect_with_transport(transport)
65
+ Puppeteer::Connection.new('', transport, @browser_options.slow_mo)
66
+ end
67
+ end