puppeteer-ruby 0.34.3 → 0.37.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -43
  3. data/README.md +78 -11
  4. data/docs/api_coverage.md +27 -6
  5. data/lib/puppeteer/browser.rb +2 -8
  6. data/lib/puppeteer/browser_context.rb +1 -0
  7. data/lib/puppeteer/concurrent_ruby_utils.rb +2 -2
  8. data/lib/puppeteer/coverage.rb +11 -2
  9. data/lib/puppeteer/define_async_method.rb +1 -1
  10. data/lib/puppeteer/devices.rb +132 -0
  11. data/lib/puppeteer/dom_world.rb +10 -9
  12. data/lib/puppeteer/element_handle/offset.rb +28 -0
  13. data/lib/puppeteer/element_handle/point.rb +11 -0
  14. data/lib/puppeteer/element_handle.rb +68 -7
  15. data/lib/puppeteer/frame.rb +3 -2
  16. data/lib/puppeteer/js_coverage.rb +28 -7
  17. data/lib/puppeteer/launcher/chrome.rb +64 -4
  18. data/lib/puppeteer/launcher/firefox.rb +48 -4
  19. data/lib/puppeteer/launcher/launch_options.rb +2 -1
  20. data/lib/puppeteer/launcher.rb +0 -1
  21. data/lib/puppeteer/mouse.rb +54 -1
  22. data/lib/puppeteer/network_condition.rb +12 -0
  23. data/lib/puppeteer/network_conditions.rb +24 -0
  24. data/lib/puppeteer/network_manager.rb +47 -11
  25. data/lib/puppeteer/page/metrics.rb +49 -0
  26. data/lib/puppeteer/page/screenshot_options.rb +3 -1
  27. data/lib/puppeteer/page.rb +147 -93
  28. data/lib/puppeteer/puppeteer.rb +10 -2
  29. data/lib/puppeteer/timeout_helper.rb +22 -0
  30. data/lib/puppeteer/tracing.rb +6 -1
  31. data/lib/puppeteer/version.rb +1 -1
  32. data/lib/puppeteer/web_socket.rb +1 -0
  33. data/lib/puppeteer-ruby.rb +2 -0
  34. data/lib/puppeteer.rb +15 -12
  35. data/puppeteer-ruby.gemspec +1 -1
  36. metadata +10 -7
  37. data/Dockerfile +0 -9
  38. data/docker-compose.yml +0 -34
  39. data/lib/puppeteer/launcher/base.rb +0 -66
@@ -94,6 +94,8 @@ class Puppeteer::Mouse
94
94
  )
95
95
  end
96
96
 
97
+ define_async_method :async_up
98
+
97
99
  # Dispatches a `mousewheel` event.
98
100
  #
99
101
  # @param delta_x [Integer]
@@ -110,5 +112,56 @@ class Puppeteer::Mouse
110
112
  )
111
113
  end
112
114
 
113
- define_async_method :async_up
115
+ def drag(start, target)
116
+ promise = resolvable_future do |f|
117
+ @client.once('Input.dragIntercepted') do |event|
118
+ f.fulfill(event['data'])
119
+ end
120
+ end
121
+ move(start.x, start.y)
122
+ down
123
+ move(target.x, target.y)
124
+ promise.value!
125
+ end
126
+
127
+ def drag_enter(target, data)
128
+ @client.send_message('Input.dispatchDragEvent',
129
+ type: 'dragEnter',
130
+ x: target.x,
131
+ y: target.y,
132
+ modifiers: @keyboard.modifiers,
133
+ data: data,
134
+ )
135
+ end
136
+
137
+ def drag_over(target, data)
138
+ @client.send_message('Input.dispatchDragEvent',
139
+ type: 'dragOver',
140
+ x: target.x,
141
+ y: target.y,
142
+ modifiers: @keyboard.modifiers,
143
+ data: data,
144
+ )
145
+ end
146
+
147
+ def drop(target, data)
148
+ @client.send_message('Input.dispatchDragEvent',
149
+ type: 'drop',
150
+ x: target.x,
151
+ y: target.y,
152
+ modifiers: @keyboard.modifiers,
153
+ data: data,
154
+ )
155
+ end
156
+
157
+ def drag_and_drop(start, target, delay: nil)
158
+ data = drag(start, target)
159
+ drag_enter(target, data)
160
+ drag_over(target, data)
161
+ if delay
162
+ sleep(delay / 1000.0)
163
+ end
164
+ drop(target, data)
165
+ up
166
+ end
114
167
  end
@@ -0,0 +1,12 @@
1
+ class Puppeteer::NetworkCondition
2
+ # @param download [Number] Download speed (bytes/s)
3
+ # @param upload [Number] Upload speed (bytes/s)
4
+ # @param latency [Number] Latency (ms)
5
+ def initialize(download:, upload:, latency:)
6
+ @download = download
7
+ @upload = upload
8
+ @latency = latency
9
+ end
10
+
11
+ attr_reader :download, :upload, :latency
12
+ end
@@ -0,0 +1,24 @@
1
+ require_relative './network_condition'
2
+
3
+ Puppeteer::NETWORK_CONDITIONS = {
4
+ 'Slow 3G' => Puppeteer::NetworkCondition.new(
5
+ download: ((500 * 1000) / 8) * 0.8,
6
+ upload: ((500 * 1000) / 8) * 0.8,
7
+ latency: 400 * 5,
8
+ ),
9
+ 'Fast 3G' => Puppeteer::NetworkCondition.new(
10
+ download: ((1.6 * 1000 * 1000) / 8) * 0.9,
11
+ upload: ((750 * 1000) / 8) * 0.9,
12
+ latency: 150 * 3.75,
13
+ ),
14
+ }
15
+
16
+ module Puppeteer::NetworkConditions
17
+ module_function def slow_3g
18
+ Puppeteer::NETWORK_CONDITIONS['Slow 3G']
19
+ end
20
+
21
+ module_function def fast_3g
22
+ Puppeteer::NETWORK_CONDITIONS['Fast 3G']
23
+ end
24
+ end
@@ -13,6 +13,46 @@ class Puppeteer::NetworkManager
13
13
  attr_reader :username, :password
14
14
  end
15
15
 
16
+ class InternalNetworkCondition
17
+ attr_writer :offline, :upload, :download, :latency
18
+
19
+ def initialize(client)
20
+ @client = client
21
+ @offline = false
22
+ @upload = -1
23
+ @download = -1
24
+ @latency = 0
25
+ end
26
+
27
+ def offline_mode=(value)
28
+ return if @offline == value
29
+ @offline = value
30
+ update_network_conditions
31
+ end
32
+
33
+ def network_condition=(network_condition)
34
+ if network_condition
35
+ @upload = network_condition.upload
36
+ @download = network_condition.download
37
+ @latency = network_condition.latency
38
+ else
39
+ @upload = -1
40
+ @download = -1
41
+ @latency = 0
42
+ end
43
+ update_network_conditions
44
+ end
45
+
46
+ private def update_network_conditions
47
+ @client.send_message('Network.emulateNetworkConditions',
48
+ offline: @offline,
49
+ latency: @latency,
50
+ downloadThroughput: @download,
51
+ uploadThroughput: @upload,
52
+ )
53
+ end
54
+ end
55
+
16
56
  # @param {!Puppeteer.CDPSession} client
17
57
  # @param {boolean} ignoreHTTPSErrors
18
58
  # @param {!Puppeteer.FrameManager} frameManager
@@ -29,13 +69,12 @@ class Puppeteer::NetworkManager
29
69
 
30
70
  @extra_http_headers = {}
31
71
 
32
- @offline = false
33
-
34
72
  @attempted_authentications = Set.new
35
73
  @user_request_interception_enabled = false
36
74
  @protocol_request_interception_enabled = false
37
75
  @user_cache_disabled = false
38
76
  @request_id_to_interception_id = {}
77
+ @internal_network_condition = InternalNetworkCondition.new(@client)
39
78
 
40
79
  @client.on_event('Fetch.requestPaused') do |event|
41
80
  handle_request_paused(event)
@@ -94,15 +133,12 @@ class Puppeteer::NetworkManager
94
133
 
95
134
  # @param value [TrueClass|FalseClass]
96
135
  def offline_mode=(value)
97
- return if @offline == value
98
- @offline = value
99
- @client.send_message('Network.emulateNetworkConditions',
100
- offline: @offline,
101
- # values of 0 remove any active throttling. crbug.com/456324#c9
102
- latency: 0,
103
- downloadThroughput: -1,
104
- uploadThroughput: -1,
105
- )
136
+ @internal_network_condition.offline_mode=(value)
137
+ end
138
+
139
+ # @param network_condition [Puppeteer::NetworkCondition|nil]
140
+ def emulate_network_conditions(network_condition)
141
+ @internal_network_condition.network_condition = network_condition
106
142
  end
107
143
 
108
144
  # @param user_agent [String]
@@ -0,0 +1,49 @@
1
+ class Puppeteer::Page
2
+ class Metrics
3
+ SUPPORTED_KEYS = Set.new([
4
+ 'Timestamp',
5
+ 'Documents',
6
+ 'Frames',
7
+ 'JSEventListeners',
8
+ 'Nodes',
9
+ 'LayoutCount',
10
+ 'RecalcStyleCount',
11
+ 'LayoutDuration',
12
+ 'RecalcStyleDuration',
13
+ 'ScriptDuration',
14
+ 'TaskDuration',
15
+ 'JSHeapUsedSize',
16
+ 'JSHeapTotalSize',
17
+ ]).freeze
18
+
19
+ SUPPORTED_KEYS.each do |key|
20
+ attr_reader key
21
+ end
22
+
23
+ # @param metrics_result [Hash] response for Performance.getMetrics
24
+ def initialize(metrics_response)
25
+ metrics_response.each do |metric|
26
+ if SUPPORTED_KEYS.include?(metric['name'])
27
+ instance_variable_set(:"@#{metric['name']}", metric['value'])
28
+ end
29
+ end
30
+ end
31
+
32
+ def [](key)
33
+ if SUPPORTED_KEYS.include?(key.to_s)
34
+ instance_variable_get(:"@#{key}")
35
+ else
36
+ raise ArgumentError.new("invalid metric key specified: #{key}")
37
+ end
38
+ end
39
+ end
40
+
41
+ class MetricsEvent
42
+ def initialize(metrics_event)
43
+ @title = metrics_event['title']
44
+ @metrics = Metrics.new(metrics_event['metrics'])
45
+ end
46
+
47
+ attr_reader :title, :metrics
48
+ end
49
+ end
@@ -15,7 +15,7 @@ class Puppeteer::Page
15
15
  # @params options [Hash]
16
16
  def initialize(options)
17
17
  if options[:type]
18
- unless [:png, :jpeg].include?(options[:type].to_sym)
18
+ unless [:png, :jpeg, :webp].include?(options[:type].to_sym)
19
19
  raise ArgumentError.new("Unknown options.type value: #{options[:type]}")
20
20
  end
21
21
  @type = options[:type]
@@ -25,6 +25,8 @@ class Puppeteer::Page
25
25
  @type = 'png'
26
26
  elsif mime_types.include?('image/jpeg')
27
27
  @type = 'jpeg'
28
+ elsif mime_types.include?('image/webp')
29
+ @type = 'webp'
28
30
  else
29
31
  raise ArgumentError.new("Unsupported screenshot mime type resolved: #{mime_types}, path: #{options[:path]}")
30
32
  end
@@ -2,11 +2,13 @@ require 'base64'
2
2
  require 'json'
3
3
  require "stringio"
4
4
 
5
+ require_relative './page/metrics'
5
6
  require_relative './page/pdf_options'
6
7
  require_relative './page/screenshot_options'
7
8
  require_relative './page/screenshot_task_queue'
8
9
 
9
10
  class Puppeteer::Page
11
+ include Puppeteer::DebugPrint
10
12
  include Puppeteer::EventCallbackable
11
13
  include Puppeteer::IfPresent
12
14
  using Puppeteer::DefineAsyncMethod
@@ -46,6 +48,8 @@ class Puppeteer::Page
46
48
  @screenshot_task_queue = ScreenshotTaskQueue.new
47
49
 
48
50
  @workers = {}
51
+ @user_drag_interception_enabled = false
52
+
49
53
  @client.on_event('Target.attachedToTarget') do |event|
50
54
  if event['targetInfo']['type'] != 'worker'
51
55
  # If we don't detach from service workers, they will never die.
@@ -102,7 +106,9 @@ class Puppeteer::Page
102
106
  @client.on('Runtime.consoleAPICalled') do |event|
103
107
  handle_console_api(event)
104
108
  end
105
- # client.on('Runtime.bindingCalled', event => this._onBindingCalled(event));
109
+ @client.on('Runtime.bindingCalled') do |event|
110
+ handle_binding_called(event)
111
+ end
106
112
  @client.on_event('Page.javascriptDialogOpening') do |event|
107
113
  handle_dialog_opening(event)
108
114
  end
@@ -112,7 +118,9 @@ class Puppeteer::Page
112
118
  @client.on_event('Inspector.targetCrashed') do |event|
113
119
  handle_target_crashed
114
120
  end
115
- # client.on('Performance.metrics', event => this._emitMetrics(event));
121
+ @client.on_event('Performance.metrics') do |event|
122
+ emit_event(PageEmittedEvents::Metrics, MetricsEvent.new(event))
123
+ end
116
124
  @client.on_event('Log.entryAdded') do |event|
117
125
  handle_log_entry_added(event)
118
126
  end
@@ -134,6 +142,11 @@ class Puppeteer::Page
134
142
  )
135
143
  end
136
144
 
145
+ def drag_interception_enabled?
146
+ @user_drag_interception_enabled
147
+ end
148
+ alias_method :drag_interception_enabled, :drag_interception_enabled?
149
+
137
150
  # @param event_name [Symbol]
138
151
  def on(event_name, &block)
139
152
  unless PageEmittedEvents.values.include?(event_name.to_s)
@@ -266,10 +279,20 @@ class Puppeteer::Page
266
279
  @frame_manager.network_manager.request_interception = value
267
280
  end
268
281
 
282
+ def drag_interception_enabled=(enabled)
283
+ @user_drag_interception_enabled = enabled
284
+ @client.send_message('Input.setInterceptDrags', enabled: enabled)
285
+ end
286
+
269
287
  def offline_mode=(enabled)
270
288
  @frame_manager.network_manager.offline_mode = enabled
271
289
  end
272
290
 
291
+ # @param network_condition [Puppeteer::NetworkCondition|nil]
292
+ def emulate_network_conditions(network_condition)
293
+ @frame_manager.network_manager.emulate_network_conditions(network_condition)
294
+ end
295
+
273
296
  # @param {number} timeout
274
297
  def default_navigation_timeout=(timeout)
275
298
  @timeout_settings.default_navigation_timeout = timeout
@@ -381,8 +404,9 @@ class Puppeteer::Page
381
404
  # @param path [String?]
382
405
  # @param content [String?]
383
406
  # @param type [String?]
384
- def add_script_tag(url: nil, path: nil, content: nil, type: nil)
385
- main_frame.add_script_tag(url: url, path: path, content: content, type: type)
407
+ # @param id [String?]
408
+ def add_script_tag(url: nil, path: nil, content: nil, type: nil, id: nil)
409
+ main_frame.add_script_tag(url: url, path: path, content: content, type: type, id: id)
386
410
  end
387
411
 
388
412
  # @param url [String?]
@@ -392,6 +416,51 @@ class Puppeteer::Page
392
416
  main_frame.add_style_tag(url: url, path: path, content: content)
393
417
  end
394
418
 
419
+ # @param name [String]
420
+ # @param puppeteer_function [Proc]
421
+ def expose_function(name, puppeteer_function)
422
+ if @page_bindings[name]
423
+ raise ArgumentError.new("Failed to add page binding with name `#{name}` already exists!")
424
+ end
425
+ @page_bindings[name] = puppeteer_function
426
+
427
+ add_page_binding = <<~JAVASCRIPT
428
+ function (type, bindingName) {
429
+ /* Cast window to any here as we're about to add properties to it
430
+ * via win[bindingName] which TypeScript doesn't like.
431
+ */
432
+ const win = window;
433
+ const binding = win[bindingName];
434
+
435
+ win[bindingName] = (...args) => {
436
+ const me = window[bindingName];
437
+ let callbacks = me.callbacks;
438
+ if (!callbacks) {
439
+ callbacks = new Map();
440
+ me.callbacks = callbacks;
441
+ }
442
+ const seq = (me.lastSeq || 0) + 1;
443
+ me.lastSeq = seq;
444
+ const promise = new Promise((resolve, reject) =>
445
+ callbacks.set(seq, { resolve, reject })
446
+ );
447
+ binding(JSON.stringify({ type, name: bindingName, seq, args }));
448
+ return promise;
449
+ };
450
+ }
451
+ JAVASCRIPT
452
+
453
+ source = JavaScriptFunction.new(add_page_binding, ['exposedFun', name]).source
454
+ @client.send_message('Runtime.addBinding', name: name)
455
+ @client.send_message('Page.addScriptToEvaluateOnNewDocument', source: source)
456
+
457
+ promises = @frame_manager.frames.map do |frame|
458
+ frame.async_evaluate("() => #{source}")
459
+ end
460
+ await_all(*promises)
461
+
462
+ nil
463
+ end
395
464
  # /**
396
465
  # * @param {string} name
397
466
  # * @param {Function} puppeteerFunction
@@ -440,36 +509,10 @@ class Puppeteer::Page
440
509
  @frame_manager.network_manager.user_agent = user_agent
441
510
  end
442
511
 
443
- # /**
444
- # * @return {!Promise<!Metrics>}
445
- # */
446
- # async metrics() {
447
- # const response = await this._client.send('Performance.getMetrics');
448
- # return this._buildMetricsObject(response.metrics);
449
- # }
450
-
451
- # /**
452
- # * @param {!Protocol.Performance.metricsPayload} event
453
- # */
454
- # _emitMetrics(event) {
455
- # this.emit(PageEmittedEvents::Metrics, {
456
- # title: event.title,
457
- # metrics: this._buildMetricsObject(event.metrics)
458
- # });
459
- # }
460
-
461
- # /**
462
- # * @param {?Array<!Protocol.Performance.Metric>} metrics
463
- # * @return {!Metrics}
464
- # */
465
- # _buildMetricsObject(metrics) {
466
- # const result = {};
467
- # for (const metric of metrics || []) {
468
- # if (supportedMetrics.has(metric.name))
469
- # result[metric.name] = metric.value;
470
- # }
471
- # return result;
472
- # }
512
+ def metrics
513
+ response = @client.send_message('Performance.getMetrics')
514
+ Metrics.new(response['metrics'])
515
+ end
473
516
 
474
517
  class PageError < StandardError ; end
475
518
 
@@ -506,56 +549,51 @@ class Puppeteer::Page
506
549
  add_console_message(event['type'], values, event['stackTrace'])
507
550
  end
508
551
 
509
- # /**
510
- # * @param {!Protocol.Runtime.bindingCalledPayload} event
511
- # */
512
- # async _onBindingCalled(event) {
513
- # const {name, seq, args} = JSON.parse(event.payload);
514
- # let expression = null;
515
- # try {
516
- # const result = await this._pageBindings.get(name)(...args);
517
- # expression = helper.evaluationString(deliverResult, name, seq, result);
518
- # } catch (error) {
519
- # if (error instanceof Error)
520
- # expression = helper.evaluationString(deliverError, name, seq, error.message, error.stack);
521
- # else
522
- # expression = helper.evaluationString(deliverErrorValue, name, seq, error);
523
- # }
524
- # this._client.send('Runtime.evaluate', { expression, contextId: event.executionContextId }).catch(debugError);
525
-
526
- # /**
527
- # * @param {string} name
528
- # * @param {number} seq
529
- # * @param {*} result
530
- # */
531
- # function deliverResult(name, seq, result) {
532
- # window[name]['callbacks'].get(seq).resolve(result);
533
- # window[name]['callbacks'].delete(seq);
534
- # }
552
+ def handle_binding_called(event)
553
+ execution_context_id = event['executionContextId']
554
+ payload =
555
+ begin
556
+ JSON.parse(event['payload'])
557
+ rescue
558
+ # The binding was either called by something in the page or it was
559
+ # called before our wrapper was initialized.
560
+ return
561
+ end
562
+ name = payload['name']
563
+ seq = payload['seq']
564
+ args = payload['args']
535
565
 
536
- # /**
537
- # * @param {string} name
538
- # * @param {number} seq
539
- # * @param {string} message
540
- # * @param {string} stack
541
- # */
542
- # function deliverError(name, seq, message, stack) {
543
- # const error = new Error(message);
544
- # error.stack = stack;
545
- # window[name]['callbacks'].get(seq).reject(error);
546
- # window[name]['callbacks'].delete(seq);
547
- # }
566
+ if payload['type'] != 'exposedFun' || !@page_bindings[name]
567
+ return
568
+ end
548
569
 
549
- # /**
550
- # * @param {string} name
551
- # * @param {number} seq
552
- # * @param {*} value
553
- # */
554
- # function deliverErrorValue(name, seq, value) {
555
- # window[name]['callbacks'].get(seq).reject(value);
556
- # window[name]['callbacks'].delete(seq);
557
- # }
558
- # }
570
+ expression =
571
+ begin
572
+ result = @page_bindings[name].call(*args)
573
+
574
+ deliver_result = <<~JAVASCRIPT
575
+ function (name, seq, result) {
576
+ window[name].callbacks.get(seq).resolve(result);
577
+ window[name].callbacks.delete(seq);
578
+ }
579
+ JAVASCRIPT
580
+
581
+ JavaScriptFunction.new(deliver_result, [name, seq, result]).source
582
+ rescue => err
583
+ deliver_error = <<~JAVASCRIPT
584
+ function (name, seq, message) {
585
+ const error = new Error(message);
586
+ window[name].callbacks.get(seq).reject(error);
587
+ window[name].callbacks.delete(seq);
588
+ }
589
+ JAVASCRIPT
590
+ JavaScriptFunction.new(deliver_error, [name, seq, err.message]).source
591
+ end
592
+
593
+ @client.async_send_message('Runtime.evaluate', expression: expression, contextId: execution_context_id).rescue do |error|
594
+ debug_puts(error)
595
+ end
596
+ end
559
597
 
560
598
  private def add_console_message(type, args, stack_trace)
561
599
  text_tokens = args.map { |arg| arg.remote_object.value }
@@ -631,10 +669,9 @@ class Puppeteer::Page
631
669
  # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
632
670
  # @return [Puppeteer::Response]
633
671
  def reload(timeout: nil, wait_until: nil)
634
- await_all(
635
- async_wait_for_navigation(timeout: timeout, wait_until: wait_until),
636
- @client.async_send_message('Page.reload'),
637
- ).first
672
+ wait_for_navigation(timeout: timeout, wait_until: wait_until) do
673
+ @client.send_message('Page.reload')
674
+ end
638
675
  end
639
676
 
640
677
  def wait_for_navigation(timeout: nil, wait_until: nil)
@@ -760,10 +797,9 @@ class Puppeteer::Page
760
797
  entries = history['entries']
761
798
  index = history['currentIndex'] + delta
762
799
  if_present(entries[index]) do |entry|
763
- await_all(
764
- async_wait_for_navigation(timeout: timeout, wait_until: wait_until),
765
- @client.async_send_message('Page.navigateToHistoryEntry', entryId: entry['id']),
766
- )
800
+ wait_for_navigation(timeout: timeout, wait_until: wait_until) do
801
+ @client.send_message('Page.navigateToHistoryEntry', entryId: entry['id'])
802
+ end
767
803
  end
768
804
  end
769
805
 
@@ -799,6 +835,15 @@ class Puppeteer::Page
799
835
  @client.send_message('Emulation.setEmulatedMedia', media: media_type_str)
800
836
  end
801
837
 
838
+ # @param factor [Number|nil] Factor at which the CPU will be throttled (2x, 2.5x. 3x, ...). Passing `nil` disables cpu throttling.
839
+ def emulate_cpu_throttling(factor)
840
+ if factor.nil? || factor >= 1
841
+ @client.send_message('Emulation.setCPUThrottlingRate', rate: factor || 1)
842
+ else
843
+ raise ArgumentError.new('Throttling rate should be greater or equal to 1')
844
+ end
845
+ end
846
+
802
847
  # @param features [Array]
803
848
  def emulate_media_features(features)
804
849
  if features.nil?
@@ -921,7 +966,7 @@ class Puppeteer::Page
921
966
  main_frame.title
922
967
  end
923
968
 
924
- # @param type [String] "png"|"jpeg"
969
+ # @param type [String] "png"|"jpeg"|"webp"
925
970
  # @param path [String]
926
971
  # @param full_page [Boolean]
927
972
  # @param clip [Hash]
@@ -1015,11 +1060,20 @@ class Puppeteer::Page
1015
1060
 
1016
1061
  # @return [Enumerable<String>]
1017
1062
  def create_pdf_stream(options = {})
1063
+ timeout_helper = Puppeteer::TimeoutHelper.new('Page.printToPDF',
1064
+ timeout_ms: options[:timeout],
1065
+ default_timeout_ms: 30000)
1018
1066
  pdf_options = PDFOptions.new(options)
1019
1067
  omit_background = options[:omit_background]
1020
1068
  set_transparent_background_color if omit_background
1021
- result = @client.send_message('Page.printToPDF', pdf_options.page_print_args)
1022
- reset_default_background_color if omit_background
1069
+ result =
1070
+ begin
1071
+ timeout_helper.with_timeout do
1072
+ @client.send_message('Page.printToPDF', pdf_options.page_print_args)
1073
+ end
1074
+ ensure
1075
+ reset_default_background_color if omit_background
1076
+ end
1023
1077
 
1024
1078
  Puppeteer::ProtocolStreamReader.new(
1025
1079
  client: @client,
@@ -11,6 +11,7 @@ class Puppeteer::Puppeteer
11
11
  class NoViewport ; end
12
12
 
13
13
  # @param product [String]
14
+ # @param channel [String|Symbol]
14
15
  # @param executable_path [String]
15
16
  # @param ignore_default_args [Array<String>|nil]
16
17
  # @param handle_SIGINT [Boolean]
@@ -30,6 +31,7 @@ class Puppeteer::Puppeteer
30
31
  # @return [Puppeteer::Browser]
31
32
  def launch(
32
33
  product: nil,
34
+ channel: nil,
33
35
  executable_path: nil,
34
36
  ignore_default_args: nil,
35
37
  handle_SIGINT: nil,
@@ -48,6 +50,7 @@ class Puppeteer::Puppeteer
48
50
  slow_mo: nil
49
51
  )
50
52
  options = {
53
+ channel: channel&.to_s,
51
54
  executable_path: executable_path,
52
55
  ignore_default_args: ignore_default_args,
53
56
  handle_SIGINT: handle_SIGINT,
@@ -118,8 +121,8 @@ class Puppeteer::Puppeteer
118
121
  end
119
122
 
120
123
  # @return [String]
121
- def executable_path
122
- launcher.executable_path
124
+ def executable_path(channel: nil)
125
+ launcher.executable_path(channel: channel)
123
126
  end
124
127
 
125
128
  private def launcher
@@ -146,6 +149,11 @@ class Puppeteer::Puppeteer
146
149
  # # ???
147
150
  # end
148
151
 
152
+ # @return [Puppeteer::NetworkConditions]
153
+ def network_conditions
154
+ Puppeteer::NetworkConditions
155
+ end
156
+
149
157
  # @param args [Array<String>]
150
158
  # @param user_data_dir [String]
151
159
  # @param devtools [Boolean]
@@ -0,0 +1,22 @@
1
+ require 'timeout'
2
+
3
+ class Puppeteer::TimeoutHelper
4
+ # @param timeout_ms [String|Integer|nil]
5
+ # @param default_timeout_ms [Integer]
6
+ def initialize(task_name, timeout_ms:, default_timeout_ms:)
7
+ @task_name = task_name
8
+ @timeout_ms = (timeout_ms || default_timeout_ms).to_i
9
+ end
10
+
11
+ def with_timeout(&block)
12
+ if @timeout_ms > 0
13
+ begin
14
+ Timeout.timeout(@timeout_ms / 1000.0, &block)
15
+ rescue Timeout::Error
16
+ raise Puppeteer::TimeoutError.new("waiting for #{@task_name} failed: timeout #{@timeout_ms}ms exceeded")
17
+ end
18
+ else
19
+ block.call
20
+ end
21
+ end
22
+ end