puppeteer-ruby 0.0.18 → 0.0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +24 -1
  3. data/.github/workflows/reviewdog.yml +1 -1
  4. data/.rubocop.yml +50 -3
  5. data/Dockerfile +9 -0
  6. data/README.md +38 -0
  7. data/docker-compose.yml +34 -0
  8. data/lib/puppeteer.rb +15 -11
  9. data/lib/puppeteer/browser.rb +19 -26
  10. data/lib/puppeteer/browser_context.rb +48 -49
  11. data/lib/puppeteer/browser_runner.rb +20 -6
  12. data/lib/puppeteer/cdp_session.rb +21 -7
  13. data/lib/puppeteer/concurrent_ruby_utils.rb +18 -5
  14. data/lib/puppeteer/connection.rb +32 -13
  15. data/lib/puppeteer/debug_print.rb +1 -1
  16. data/lib/puppeteer/define_async_method.rb +1 -1
  17. data/lib/puppeteer/devices.rb +998 -849
  18. data/lib/puppeteer/dialog.rb +34 -0
  19. data/lib/puppeteer/dom_world.rb +2 -2
  20. data/lib/puppeteer/element_handle.rb +18 -1
  21. data/lib/puppeteer/env.rb +5 -0
  22. data/lib/puppeteer/event_callbackable.rb +4 -0
  23. data/lib/puppeteer/events.rb +184 -0
  24. data/lib/puppeteer/exception_details.rb +38 -0
  25. data/lib/puppeteer/frame.rb +1 -3
  26. data/lib/puppeteer/frame_manager.rb +20 -16
  27. data/lib/puppeteer/geolocation.rb +24 -0
  28. data/lib/puppeteer/keyboard/us_keyboard_layout.rb +2 -2
  29. data/lib/puppeteer/launcher.rb +11 -2
  30. data/lib/puppeteer/launcher/base.rb +14 -4
  31. data/lib/puppeteer/launcher/browser_options.rb +2 -1
  32. data/lib/puppeteer/launcher/chrome.rb +5 -9
  33. data/lib/puppeteer/launcher/firefox.rb +385 -0
  34. data/lib/puppeteer/lifecycle_watcher.rb +6 -6
  35. data/lib/puppeteer/network_manager.rb +6 -6
  36. data/lib/puppeteer/page.rb +87 -103
  37. data/lib/puppeteer/remote_object.rb +12 -1
  38. data/lib/puppeteer/target.rb +2 -2
  39. data/lib/puppeteer/version.rb +1 -1
  40. data/puppeteer-ruby.gemspec +1 -1
  41. metadata +11 -4
@@ -0,0 +1,34 @@
1
+ class Puppeteer::Dialog
2
+ def initialize(client, type:, message:, default_value:)
3
+ @client = client
4
+ @type = type
5
+ @message = message
6
+ @default_value = default_value || ''
7
+ end
8
+
9
+ attr_reader :type, :message, :default_value
10
+
11
+ # @param prompt_text - optional text that will be entered in the dialog
12
+ # prompt. Has no effect if the dialog's type is not `prompt`.
13
+ #
14
+ # @returns A promise that resolves when the dialog has been accepted.
15
+ def accept(prompt_text = nil)
16
+ if @handled
17
+ raise 'Cannot accept dialog which is already handled!'
18
+ end
19
+ @handled = true
20
+ @client.send_message('Page.handleJavaScriptDialog', {
21
+ accept: true,
22
+ promptText: prompt_text,
23
+ }.compact)
24
+ end
25
+
26
+ # @returns A promise which will resolve once the dialog has been dismissed
27
+ def dismiss
28
+ if @handled
29
+ raise 'Cannot accept dialog which is already handled!'
30
+ end
31
+ @handled = true
32
+ @client.send_message('Page.handleJavaScriptDialog', accept: false)
33
+ end
34
+ end
@@ -134,7 +134,7 @@ class Puppeteer::DOMWorld
134
134
 
135
135
  # @return [String]
136
136
  def content
137
- evaluate <<-JAVASCRIPT
137
+ evaluate(<<-JAVASCRIPT)
138
138
  () => {
139
139
  let retVal = '';
140
140
  if (document.doctype)
@@ -151,7 +151,7 @@ class Puppeteer::DOMWorld
151
151
  # @param wait_until [String|Array<String>]
152
152
  def set_content(html, timeout: nil, wait_until: nil)
153
153
  option_wait_until = [wait_until || 'load'].flatten
154
- option_timeout = @timeout_settings.navigation_timeout
154
+ option_timeout = timeout || @timeout_settings.navigation_timeout
155
155
 
156
156
  # We rely upon the fact that document.open() will reset frame lifecycle with "init"
157
157
  # lifecycle event. @see https://crrev.com/608658
@@ -43,7 +43,24 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
43
43
  if (element.nodeType !== Node.ELEMENT_NODE)
44
44
  return 'Node is not of type HTMLElement';
45
45
 
46
- element.scrollIntoViewIfNeeded({block: 'center', inline: 'center', behavior: 'instant'});
46
+ if (element.scrollIntoViewIfNeeded) {
47
+ element.scrollIntoViewIfNeeded({block: 'center', inline: 'center', behavior: 'instant'});
48
+ } else {
49
+ // force-scroll if page's javascript is disabled.
50
+ if (!pageJavascriptEnabled) {
51
+ element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
52
+ return false;
53
+ }
54
+ const visibleRatio = await new Promise(resolve => {
55
+ const observer = new IntersectionObserver(entries => {
56
+ resolve(entries[0].intersectionRatio);
57
+ observer.disconnect();
58
+ });
59
+ observer.observe(element);
60
+ });
61
+ if (visibleRatio !== 1.0)
62
+ element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
63
+ }
47
64
  return false;
48
65
  }
49
66
  JAVASCRIPT
@@ -9,6 +9,11 @@ class Puppeteer::Env
9
9
  def ci?
10
10
  ['1', 'true'].include?(ENV['CI'].to_s)
11
11
  end
12
+
13
+ # check if running on macOS
14
+ def darwin?
15
+ RUBY_PLATFORM.include?('darwin')
16
+ end
12
17
  end
13
18
 
14
19
  class Puppeteer
@@ -31,6 +31,8 @@ module Puppeteer::EventCallbackable
31
31
  (@event_listeners[event_name] ||= EventListeners.new).add(&block)
32
32
  end
33
33
 
34
+ alias_method :on, :add_event_listener
35
+
34
36
  def remove_event_listener(*id_args)
35
37
  (@event_listeners ||= {}).each do |event_name, listeners|
36
38
  id_args.each do |id|
@@ -50,6 +52,8 @@ module Puppeteer::EventCallbackable
50
52
  end
51
53
  end
52
54
 
55
+ alias_method :once, :observe_first
56
+
53
57
  def on_event(event_name, &block)
54
58
  @event_callbackable_handlers ||= {}
55
59
  @event_callbackable_handlers[event_name] = block
@@ -0,0 +1,184 @@
1
+ require 'digest/md5'
2
+
3
+ module EventsDefinitionUtils
4
+ refine Kernel do
5
+ # Symbol is used to prevent external parties listening to these events
6
+ def Symbol(str)
7
+ Digest::MD5.hexdigest(str)
8
+ end
9
+ end
10
+
11
+ refine Hash do
12
+ def define_const_into(target_module)
13
+ each do |key, value|
14
+ target_module.const_set(key, value)
15
+ target_module.define_singleton_method(key) { value }
16
+ end
17
+ keyset = Set.new(keys)
18
+ valueset = Set.new(values)
19
+ target_module.define_singleton_method(:keys) { keyset }
20
+ target_module.define_singleton_method(:values) { valueset }
21
+ end
22
+ end
23
+ end
24
+
25
+ using EventsDefinitionUtils
26
+
27
+ # Internal events that the Connection class emits.
28
+ module ConnectionEmittedEvents ; end
29
+
30
+ {
31
+ Disconnected: Symbol('Connection.Disconnected'),
32
+ }.define_const_into(ConnectionEmittedEvents)
33
+
34
+ # Internal events that the CDPSession class emits.
35
+ module CDPSessionEmittedEvents ; end
36
+
37
+ {
38
+ Disconnected: Symbol('CDPSession.Disconnected'),
39
+ }.define_const_into(CDPSessionEmittedEvents)
40
+
41
+ # All the events a Browser may emit.
42
+ module BrowserEmittedEvents ; end
43
+
44
+ {
45
+ # Emitted when Puppeteer gets disconnected from the Chromium instance. This might happen because of one of the following:
46
+ # - Chromium is closed or crashed
47
+ # - The Browser#disconnect method was called.
48
+ Disconnected: 'disconnected',
49
+
50
+ # Emitted when the url of a target changes. Contains a {@link Target} instance.
51
+ TargetChanged: 'targetchanged',
52
+
53
+ # Emitted when a target is created, for example when a new page is opened by
54
+ # window.open or by Browser#newPage
55
+ # Contains a Target instance.
56
+ TargetCreated: 'targetcreated',
57
+
58
+ # Emitted when a target is destroyed, for example when a page is closed.
59
+ # Contains a Target instance.
60
+ TargetDestroyed: 'targetdestroyed',
61
+ }.define_const_into(BrowserEmittedEvents)
62
+
63
+ module BrowserContextEmittedEvents ; end
64
+
65
+ {
66
+ # Emitted when the url of a target inside the browser context changes.
67
+ # Contains a Target instance.
68
+ TargetChanged: 'targetchanged',
69
+
70
+ # Emitted when a target is created, for example when a new page is opened by
71
+ # window.open or by BrowserContext#newPage
72
+ # Contains a Target instance.
73
+ TargetCreated: 'targetcreated',
74
+
75
+ # Emitted when a target is destroyed within the browser context, for example when a page is closed.
76
+ # Contains a Target instance.
77
+ TargetDestroyed: 'targetdestroyed',
78
+ }.define_const_into(BrowserContextEmittedEvents)
79
+
80
+ # We use symbols to prevent any external parties listening to these events.
81
+ # They are internal to Puppeteer.
82
+ module NetworkManagerEmittedEvents ; end
83
+
84
+ {
85
+ Request: Symbol('NetworkManager.Request'),
86
+ Response: Symbol('NetworkManager.Response'),
87
+ RequestFailed: Symbol('NetworkManager.RequestFailed'),
88
+ RequestFinished: Symbol('NetworkManager.RequestFinished'),
89
+ }.define_const_into(NetworkManagerEmittedEvents)
90
+
91
+
92
+ # We use symbols to prevent external parties listening to these events.
93
+ # They are internal to Puppeteer.
94
+ module FrameManagerEmittedEvents ; end
95
+
96
+ {
97
+ FrameAttached: Symbol('FrameManager.FrameAttached'),
98
+ FrameNavigated: Symbol('FrameManager.FrameNavigated'),
99
+ FrameDetached: Symbol('FrameManager.FrameDetached'),
100
+ LifecycleEvent: Symbol('FrameManager.LifecycleEvent'),
101
+ FrameNavigatedWithinDocument: Symbol('FrameManager.FrameNavigatedWithinDocument'),
102
+ ExecutionContextCreated: Symbol('FrameManager.ExecutionContextCreated'),
103
+ ExecutionContextDestroyed: Symbol('FrameManager.ExecutionContextDestroyed'),
104
+ }.define_const_into(FrameManagerEmittedEvents)
105
+
106
+ # All the events that a page instance may emit.
107
+ module PageEmittedEvents ; end
108
+
109
+ {
110
+ # Emitted when the page closes.
111
+ Close: 'close',
112
+
113
+ # Emitted when JavaScript within the page calls one of console API methods,
114
+ # e.g. `console.log` or `console.dir`. Also emitted if the page throws an
115
+ # error or a warning.
116
+ Console: 'console',
117
+
118
+ # Emitted when a JavaScript dialog appears, such as `alert`, `prompt`,
119
+ # `confirm` or `beforeunload`. Puppeteer can respond to the dialog via
120
+ # Dialog#accept or Dialog#dismiss.
121
+ Dialog: 'dialog',
122
+
123
+ # Emitted when the JavaScript
124
+ # {https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded DOMContentLoaded} event is dispatched.
125
+ DOMContentLoaded: 'domcontentloaded',
126
+
127
+ # Emitted when the page crashes. Will contain an `Error`.
128
+ Error: 'error',
129
+
130
+ # Emitted when a frame is attached. Will contain a Frame.
131
+ FrameAttached: 'frameattached',
132
+ # Emitted when a frame is detached. Will contain a Frame.
133
+ FrameDetached: 'framedetached',
134
+ # Emitted when a frame is navigated to a new URL. Will contain a {@link Frame}.
135
+ FrameNavigated: 'framenavigated',
136
+
137
+ # Emitted when the JavaScript
138
+ # {https://developer.mozilla.org/en-US/docs/Web/Events/load | load} event is dispatched.
139
+ Load: 'load',
140
+
141
+ # Emitted when the JavaScript code makes a call to `console.timeStamp`. For
142
+ # the list of metrics see {@link Page.metrics | page.metrics}.
143
+ #
144
+ # Contains an object with two properties:
145
+ # - `title`: the title passed to `console.timeStamp`
146
+ # - `metrics`: objec containing metrics as key/value pairs. The values will be `number`s.
147
+ Metrics: 'metrics',
148
+
149
+ # Emitted when an uncaught exception happens within the page.
150
+ # Contains an `Error`.
151
+ PageError: 'pageerror',
152
+
153
+ # Emitted when the page opens a new tab or window.
154
+ # Contains a Page corresponding to the popup window.
155
+ Popup: 'popup',
156
+
157
+ # Emitted when a page issues a request and contains a HTTPRequest.
158
+ #
159
+ # The object is readonly. See Page#setRequestInterception for intercepting and mutating requests.
160
+ Request: 'request',
161
+
162
+ # Emitted when a request fails, for example by timing out.
163
+ #
164
+ # Contains a HTTPRequest.
165
+ #
166
+ # NOTE: HTTP Error responses, such as 404 or 503, are still successful
167
+ # responses from HTTP standpoint, so request will complete with
168
+ # `requestfinished` event and not with `requestfailed`.
169
+ RequestFailed: 'requestfailed',
170
+
171
+ # Emitted when a request finishes successfully. Contains a HTTPRequest.
172
+ RequestFinished: 'requestfinished',
173
+
174
+ # Emitted when a response is received. Contains a HTTPResponse.
175
+ Response: 'response',
176
+
177
+ # Emitted when a dedicated
178
+ # {https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API WebWorker} is spawned by the page.
179
+ WorkerCreated: 'workercreated',
180
+
181
+ # Emitted when a dedicated
182
+ # {https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API WebWorker} is destroyed by the page.
183
+ WorkerDestroyed: 'workerdestroyed',
184
+ }.define_const_into(PageEmittedEvents)
@@ -0,0 +1,38 @@
1
+ # Original implementation, helpers.getExceptionMessage
2
+ class Puppeteer::ExceptionDetails
3
+ # @param exception_details [Hash]
4
+ def initialize(exception_details)
5
+ @exception_details = exception_details
6
+ end
7
+
8
+ def message
9
+ # "exceptionDetails"=>{"exceptionId"=>1, "text"=>"Uncaught", "lineNumber"=>12, "columnNumber"=>10, "url"=>"http://127.0.0.1:4567/error.html",
10
+ # "stackTrace"=>{"callFrames"=>[
11
+ # {"functionName"=>"c", "scriptId"=>"6", "url"=>"http://127.0.0.1:4567/error.html", "lineNumber"=>12, "columnNumber"=>10},
12
+ # {"functionName"=>"b", "scriptId"=>"6", "url"=>"http://127.0.0.1:4567/error.html", "lineNumber"=>8, "columnNumber"=>4},
13
+ # {"functionName"=>"a", "scriptId"=>"6", "url"=>"http://127.0.0.1:4567/error.html", "lineNumber"=>4, "columnNumber"=>4},
14
+ # {"functionName"=>"", "scriptId"=>"6", "url"=>"http://127.0.0.1:4567/error.html", "lineNumber"=>1, "columnNumber"=>0}
15
+ # ]},
16
+ # "exception"=>{"type"=>"object", "subtype"=>"error", "className"=>"Error", "description"=>"Error: Fancy error!\n at c (http://127.0.0.1:4567/error.html:13:11)\n at b (http://127.0.0.1:4567/error.html:9:5)\n at a (http://127.0.0.1:4567/error.html:5:5)\n at http://127.0.0.1:4567/error.html:2:1", "objectId"=>"{\"injectedScriptId\":3,\"id\":1}", "preview"=>{"type"=>"object", "subtype"=>"error", "description"=>"Error: Fancy error!\n at c (http://127.0.0.1:4567/error.html:13:11)\n at b (http://127.0.0.1:4567/error.html:9:5)\n at a (http://127.0.0.1:4567/error.html:5:5)\n at http://127.0.0.1:4567/error.html:2:1", "overflow"=>false, "properties"=>[{"name"=>"stack", "type"=>"string", "value"=>"Error: Fancy error!\n at c (http://127.0.0.1:456…:5:5)\n at http://127.0.0.1:4567/error.html:2:1"}, {"name"=>"message", "type"=>"string", "value"=>"Fancy error!"}]}}
17
+ if @exception_details['exception']
18
+ return exception_description_or_value(@exception_details['exception'])
19
+ end
20
+
21
+ messages = []
22
+ messages << @exception_details['text']
23
+
24
+ if @exception_details['stackTrace']
25
+ @exception_details['stackTrace']['callFrames'].each do |call_frame|
26
+ location = "#{call_frame['url']}:#{call_frame['lineNumber']}:#{call_frame['columnNumber']}"
27
+ function_name = call_frame['functionName'] || '<anonymous>'
28
+ messages << "at #{function_name} (#{location})"
29
+ end
30
+ end
31
+
32
+ messages.join("\n ")
33
+ end
34
+
35
+ private def exception_description_or_value(exception)
36
+ exception['description'] || exception['value']
37
+ end
38
+ end
@@ -266,9 +266,7 @@ class Puppeteer::Frame
266
266
  # @param frame_payload [Hash]
267
267
  def navigated(frame_payload)
268
268
  @name = frame_payload['name']
269
- # TODO(lushnikov): remove this once requestInterception has loaderId exposed.
270
- @navigation_url = frame_payload['url']
271
- @url = frame_payload['url']
269
+ @url = "#{frame_payload['url']}#{frame_payload['urlFragment']}"
272
270
 
273
271
  # Ensure loaderId updated.
274
272
  # The order of [Page.lifecycleEvent name="init"] and [Page.frameNavigated] is random... for some reason...
@@ -27,31 +27,31 @@ class Puppeteer::FrameManager
27
27
  # @type {!Set<string>}
28
28
  @isolated_worlds = Set.new
29
29
 
30
- @client.on_event 'Page.frameAttached' do |event|
30
+ @client.on_event('Page.frameAttached') do |event|
31
31
  handle_frame_attached(event['frameId'], event['parentFrameId'])
32
32
  end
33
- @client.on_event 'Page.frameNavigated' do |event|
33
+ @client.on_event('Page.frameNavigated') do |event|
34
34
  handle_frame_navigated(event['frame'])
35
35
  end
36
- @client.on_event 'Page.navigatedWithinDocument' do |event|
36
+ @client.on_event('Page.navigatedWithinDocument') do |event|
37
37
  handle_frame_navigated_within_document(event['frameId'], event['url'])
38
38
  end
39
- @client.on_event 'Page.frameDetached' do |event|
39
+ @client.on_event('Page.frameDetached') do |event|
40
40
  handle_frame_detached(event['frameId'])
41
41
  end
42
- @client.on_event 'Page.frameStoppedLoading' do |event|
42
+ @client.on_event('Page.frameStoppedLoading') do |event|
43
43
  handle_frame_stopped_loading(event['frameId'])
44
44
  end
45
- @client.on_event 'Runtime.executionContextCreated' do |event|
45
+ @client.on_event('Runtime.executionContextCreated') do |event|
46
46
  handle_execution_context_created(event['context'])
47
47
  end
48
- @client.on_event 'Runtime.executionContextDestroyed' do |event|
48
+ @client.on_event('Runtime.executionContextDestroyed') do |event|
49
49
  handle_execution_context_destroyed(event['executionContextId'])
50
50
  end
51
- @client.on_event 'Runtime.executionContextsCleared' do |event|
51
+ @client.on_event('Runtime.executionContextsCleared') do |event|
52
52
  handle_execution_contexts_cleared
53
53
  end
54
- @client.on_event 'Page.lifecycleEvent' do |event|
54
+ @client.on_event('Page.lifecycleEvent') do |event|
55
55
  handle_lifecycle_event(event)
56
56
  end
57
57
  end
@@ -121,6 +121,8 @@ class Puppeteer::FrameManager
121
121
  document_navigation_promise,
122
122
  watcher.timeout_or_termination_promise,
123
123
  )
124
+ rescue Puppeteer::TimeoutError => err
125
+ raise NavigationError.new(err)
124
126
  ensure
125
127
  watcher.dispose
126
128
  end
@@ -143,6 +145,8 @@ class Puppeteer::FrameManager
143
145
  watcher.same_document_navigation_promise,
144
146
  watcher.new_document_navigation_promise,
145
147
  )
148
+ rescue Puppeteer::TimeoutError => err
149
+ raise NavigationError.new(err)
146
150
  ensure
147
151
  watcher.dispose
148
152
  end
@@ -155,7 +159,7 @@ class Puppeteer::FrameManager
155
159
  frame = @frames[event['frameId']]
156
160
  return if !frame
157
161
  frame.handle_lifecycle_event(event['loaderId'], event['name'])
158
- emit_event 'Events.FrameManager.LifecycleEvent', frame
162
+ emit_event(FrameManagerEmittedEvents::LifecycleEvent, frame)
159
163
  end
160
164
 
161
165
  # @param {string} frameId
@@ -163,7 +167,7 @@ class Puppeteer::FrameManager
163
167
  frame = @frames[frame_id]
164
168
  return if !frame
165
169
  frame.handle_loading_stopped
166
- emit_event 'Events.FrameManager.LifecycleEvent', frame
170
+ emit_event(FrameManagerEmittedEvents::LifecycleEvent, frame)
167
171
  end
168
172
 
169
173
  # @param frame_tree [Hash]
@@ -211,7 +215,7 @@ class Puppeteer::FrameManager
211
215
  frame = Puppeteer::Frame.new(self, @client, parent_frame, frame_id)
212
216
  @frames[frame_id] = frame
213
217
 
214
- emit_event 'Events.FrameManager.FrameAttached', frame
218
+ emit_event(FrameManagerEmittedEvents::FrameAttached, frame)
215
219
  end
216
220
 
217
221
  # @param frame_payload [Hash]
@@ -252,7 +256,7 @@ class Puppeteer::FrameManager
252
256
  # Update frame payload.
253
257
  frame.navigated(frame_payload)
254
258
 
255
- emit_event 'Events.FrameManager.FrameNavigated', frame
259
+ emit_event(FrameManagerEmittedEvents::FrameNavigated, frame)
256
260
  end
257
261
 
258
262
  # @param name [String]
@@ -280,8 +284,8 @@ class Puppeteer::FrameManager
280
284
  frame = @frames[frame_id]
281
285
  return unless frame
282
286
  frame.navigated_within_document(url)
283
- emit_event 'Events.FrameManager.FrameNavigatedWithinDocument', frame
284
- emit_event 'Events.FrameManager.FrameNavigated', frame
287
+ emit_event(FrameManagerEmittedEvents::FrameNavigatedWithinDocument, frame)
288
+ emit_event(FrameManagerEmittedEvents::FrameNavigated, frame)
285
289
  end
286
290
 
287
291
  # @param frame_id [String]
@@ -349,7 +353,7 @@ class Puppeteer::FrameManager
349
353
  end
350
354
  frame.detach
351
355
  @frames.delete(frame.id)
352
- emit_event 'Events.FrameManager.FrameDetached', frame
356
+ emit_event(FrameManagerEmittedEvents::FrameDetached, frame)
353
357
  end
354
358
 
355
359
  private def assert_no_legacy_navigation_options(wait_until:)