puppeteer-ruby 0.35.1 → 0.37.2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -45
  3. data/README.md +69 -0
  4. data/docs/api_coverage.md +55 -45
  5. data/lib/puppeteer/browser.rb +2 -8
  6. data/lib/puppeteer/browser_context.rb +1 -0
  7. data/lib/puppeteer/browser_runner.rb +1 -1
  8. data/lib/puppeteer/concurrent_ruby_utils.rb +2 -2
  9. data/lib/puppeteer/coverage.rb +11 -2
  10. data/lib/puppeteer/define_async_method.rb +1 -1
  11. data/lib/puppeteer/devices.rb +132 -0
  12. data/lib/puppeteer/dom_world.rb +10 -9
  13. data/lib/puppeteer/element_handle/offset.rb +28 -0
  14. data/lib/puppeteer/element_handle/point.rb +11 -0
  15. data/lib/puppeteer/element_handle.rb +68 -7
  16. data/lib/puppeteer/frame.rb +4 -3
  17. data/lib/puppeteer/frame_manager.rb +2 -2
  18. data/lib/puppeteer/{request.rb → http_request.rb} +150 -21
  19. data/lib/puppeteer/{response.rb → http_response.rb} +2 -2
  20. data/lib/puppeteer/js_coverage.rb +28 -7
  21. data/lib/puppeteer/launcher/launch_options.rb +3 -3
  22. data/lib/puppeteer/lifecycle_watcher.rb +2 -2
  23. data/lib/puppeteer/mouse.rb +54 -1
  24. data/lib/puppeteer/network_condition.rb +12 -0
  25. data/lib/puppeteer/network_conditions.rb +24 -0
  26. data/lib/puppeteer/network_manager.rb +64 -18
  27. data/lib/puppeteer/page/metrics.rb +49 -0
  28. data/lib/puppeteer/page/screenshot_options.rb +3 -1
  29. data/lib/puppeteer/page.rb +166 -134
  30. data/lib/puppeteer/puppeteer.rb +5 -0
  31. data/lib/puppeteer/timeout_helper.rb +22 -0
  32. data/lib/puppeteer/tracing.rb +6 -1
  33. data/lib/puppeteer/version.rb +1 -1
  34. data/lib/puppeteer/wait_task.rb +1 -1
  35. data/lib/puppeteer/web_socket.rb +1 -0
  36. data/lib/puppeteer.rb +17 -14
  37. data/puppeteer-ruby.gemspec +1 -1
  38. metadata +11 -6
@@ -12,6 +12,14 @@ class Puppeteer::JSCoverage
12
12
  attr_reader :url, :ranges, :text
13
13
  end
14
14
 
15
+ class ItemWithRawScriptCoverage < Item
16
+ def initialize(url:, ranges:, text:, raw_script_coverage:)
17
+ super(url: url, ranges: ranges, text: text)
18
+ @raw_script_coverage = raw_script_coverage
19
+ end
20
+ attr_reader :raw_script_coverage
21
+ end
22
+
15
23
  # @param client [Puppeteer::CDPSession]
16
24
  def initialize(client)
17
25
  @client = client
@@ -20,7 +28,10 @@ class Puppeteer::JSCoverage
20
28
  @script_sources = {}
21
29
  end
22
30
 
23
- def start(reset_on_navigation: nil, report_anonymous_scripts: nil)
31
+ def start(
32
+ reset_on_navigation: nil,
33
+ report_anonymous_scripts: nil,
34
+ include_raw_script_coverage: nil)
24
35
  raise 'JSCoverage is already enabled' if @enabled
25
36
 
26
37
  @reset_on_navigation =
@@ -30,6 +41,7 @@ class Puppeteer::JSCoverage
30
41
  true
31
42
  end
32
43
  @report_anonymous_scripts = report_anonymous_scripts || false
44
+ @include_raw_script_coverage = include_raw_script_coverage || false
33
45
  @enabled = true
34
46
  @script_urls.clear
35
47
  @script_sources.clear
@@ -43,7 +55,7 @@ class Puppeteer::JSCoverage
43
55
  await_all(
44
56
  @client.async_send_message('Profiler.enable'),
45
57
  @client.async_send_message('Profiler.startPreciseCoverage',
46
- callCount: false,
58
+ callCount: @include_raw_script_coverage,
47
59
  detailed: true,
48
60
  ),
49
61
  @client.async_send_message('Debugger.enable'),
@@ -107,11 +119,20 @@ class Puppeteer::JSCoverage
107
119
  end
108
120
  end
109
121
 
110
- coverage << Item.new(
111
- url: url,
112
- ranges: convert_to_disjoint_ranges(flatten_ranges),
113
- text: text,
114
- )
122
+ if @include_raw_script_coverage
123
+ coverage << ItemWithRawScriptCoverage.new(
124
+ url: url,
125
+ ranges: convert_to_disjoint_ranges(flatten_ranges),
126
+ text: text,
127
+ raw_script_coverage: entry,
128
+ )
129
+ else
130
+ coverage << Item.new(
131
+ url: url,
132
+ ranges: convert_to_disjoint_ranges(flatten_ranges),
133
+ text: text,
134
+ )
135
+ end
115
136
  end
116
137
 
117
138
  coverage
@@ -35,9 +35,9 @@ module Puppeteer::Launcher
35
35
  @channel = options[:channel]
36
36
  @executable_path = options[:executable_path]
37
37
  @ignore_default_args = options[:ignore_default_args] || false
38
- @handle_SIGINT = options[:handle_SIGINT] || true
39
- @handle_SIGTERM = options[:handle_SIGTERM] || true
40
- @handle_SIGHUP = options[:handle_SIGHUP] || true
38
+ @handle_SIGINT = options.fetch(:handle_SIGINT, true)
39
+ @handle_SIGTERM = options.fetch(:handle_SIGTERM, true)
40
+ @handle_SIGHUP = options.fetch(:handle_SIGHUP, true)
41
41
  @timeout = options[:timeout] || 30000
42
42
  @dumpio = options[:dumpio] || false
43
43
  @env = options[:env] || ENV
@@ -88,7 +88,7 @@ class Puppeteer::LifecycleWatcher
88
88
  check_lifecycle_complete
89
89
  end
90
90
 
91
- # @param [Puppeteer::Request] request
91
+ # @param [Puppeteer::HTTPRequest] request
92
92
  def handle_request(request)
93
93
  return if request.frame != @frame || !request.navigation_request?
94
94
  @navigation_request = request
@@ -103,7 +103,7 @@ class Puppeteer::LifecycleWatcher
103
103
  check_lifecycle_complete
104
104
  end
105
105
 
106
- # @return [Puppeteer::Response]
106
+ # @return [Puppeteer::HTTPResponse]
107
107
  def navigation_response
108
108
  if_present(@navigation_request) do |request|
109
109
  request.response
@@ -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,21 +133,23 @@ 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]
109
- def user_agent=(user_agent)
110
- @client.send_message('Network.setUserAgentOverride', userAgent: user_agent)
145
+ # @param user_agent_metadata [Hash]
146
+ def set_user_agent(user_agent, user_agent_metadata = nil)
147
+ @client.send_message('Network.setUserAgentOverride', {
148
+ userAgent: user_agent,
149
+ userAgentMetadata: user_agent_metadata,
150
+ }.compact)
111
151
  end
152
+ alias_method :user_agent=, :set_user_agent
112
153
 
113
154
  def cache_enabled=(enabled)
114
155
  @user_cache_disabled = !enabled
@@ -210,9 +251,14 @@ class Puppeteer::NetworkManager
210
251
  end
211
252
  end
212
253
  frame = if_present(event['frameId']) { |frame_id| @frame_manager.frame(frame_id) }
213
- request = Puppeteer::Request.new(@client, frame, interception_id, @user_request_interception_enabled, event, redirect_chain)
254
+ request = Puppeteer::HTTPRequest.new(@client, frame, interception_id, @user_request_interception_enabled, event, redirect_chain)
214
255
  @request_id_to_request[event['requestId']] = request
215
256
  emit_event(NetworkManagerEmittedEvents::Request, request)
257
+ begin
258
+ request.finalize_interceptions
259
+ rescue => err
260
+ debug_puts(err)
261
+ end
216
262
  end
217
263
 
218
264
  private def handle_request_served_from_cache(event)
@@ -221,13 +267,13 @@ class Puppeteer::NetworkManager
221
267
  end
222
268
  end
223
269
 
224
- # @param request [Puppeteer::Request]
270
+ # @param request [Puppeteer::HTTPRequest]
225
271
  # @param response_payload [Hash]
226
272
  private def handle_request_redirect(request, response_payload)
227
- response = Puppeteer::Response.new(@client, request, response_payload)
273
+ response = Puppeteer::HTTPResponse.new(@client, request, response_payload)
228
274
  request.internal.response = response
229
275
  request.internal.redirect_chain << request
230
- response.internal.body_loaded_promise.reject(Puppeteer::Response::Redirected.new)
276
+ response.internal.body_loaded_promise.reject(Puppeteer::HTTPResponse::Redirected.new)
231
277
  @request_id_to_request.delete(request.internal.request_id)
232
278
  @attempted_authentications.delete(request.internal.interception_id)
233
279
  emit_event(NetworkManagerEmittedEvents::Response, response)
@@ -240,7 +286,7 @@ class Puppeteer::NetworkManager
240
286
  # FileUpload sends a response without a matching request.
241
287
  return unless request
242
288
 
243
- response = Puppeteer::Response.new(@client, request, event['response'])
289
+ response = Puppeteer::HTTPResponse.new(@client, request, event['response'])
244
290
  request.internal.response = response
245
291
  emit_event(NetworkManagerEmittedEvents::Response, response)
246
292
  end
@@ -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