puppeteer-ruby 0.0.3 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +30 -0
  3. data/.github/stale.yml +16 -0
  4. data/.rubocop.yml +4 -5
  5. data/README.md +4 -1
  6. data/docs/Puppeteer.html +2020 -0
  7. data/docs/Puppeteer/AsyncAwaitBehavior.html +105 -0
  8. data/docs/Puppeteer/Browser.html +2150 -0
  9. data/docs/Puppeteer/BrowserContext.html +809 -0
  10. data/docs/Puppeteer/BrowserFetcher.html +214 -0
  11. data/docs/Puppeteer/BrowserRunner.html +914 -0
  12. data/docs/Puppeteer/BrowserRunner/BrowserProcess.html +477 -0
  13. data/docs/Puppeteer/CDPSession.html +813 -0
  14. data/docs/Puppeteer/CDPSession/Error.html +124 -0
  15. data/docs/Puppeteer/ConcurrentRubyUtils.html +430 -0
  16. data/docs/Puppeteer/Connection.html +960 -0
  17. data/docs/Puppeteer/Connection/MessageCallback.html +434 -0
  18. data/docs/Puppeteer/Connection/ProtocolError.html +216 -0
  19. data/docs/Puppeteer/Connection/RequestDebugPrinter.html +217 -0
  20. data/docs/Puppeteer/Connection/ResponseDebugPrinter.html +244 -0
  21. data/docs/Puppeteer/ConsoleMessage.html +565 -0
  22. data/docs/Puppeteer/ConsoleMessage/Location.html +433 -0
  23. data/docs/Puppeteer/DOMWorld.html +2219 -0
  24. data/docs/Puppeteer/DOMWorld/DetachedError.html +124 -0
  25. data/docs/Puppeteer/DOMWorld/DocumentEvaluationError.html +124 -0
  26. data/docs/Puppeteer/DebugPrint.html +233 -0
  27. data/docs/Puppeteer/Device.html +470 -0
  28. data/docs/Puppeteer/Devices.html +139 -0
  29. data/docs/Puppeteer/ElementHandle.html +2224 -0
  30. data/docs/Puppeteer/ElementHandle/ElementNotFoundError.html +206 -0
  31. data/docs/Puppeteer/ElementHandle/ElementNotVisibleError.html +206 -0
  32. data/docs/Puppeteer/ElementHandle/Point.html +481 -0
  33. data/docs/Puppeteer/ElementHandle/ScrollIntoViewError.html +124 -0
  34. data/docs/Puppeteer/EmulationManager.html +454 -0
  35. data/docs/Puppeteer/EventCallbackable.html +433 -0
  36. data/docs/Puppeteer/EventCallbackable/EventListeners.html +435 -0
  37. data/docs/Puppeteer/ExecutionContext.html +998 -0
  38. data/docs/Puppeteer/ExecutionContext/EvaluationError.html +124 -0
  39. data/docs/Puppeteer/ExecutionContext/JavaScriptExpression.html +357 -0
  40. data/docs/Puppeteer/ExecutionContext/JavaScriptFunction.html +389 -0
  41. data/docs/Puppeteer/FileChooser.html +455 -0
  42. data/docs/Puppeteer/Frame.html +3677 -0
  43. data/docs/Puppeteer/FrameManager.html +2414 -0
  44. data/docs/Puppeteer/FrameManager/NavigationError.html +124 -0
  45. data/docs/Puppeteer/IfPresent.html +222 -0
  46. data/docs/Puppeteer/JSHandle.html +1352 -0
  47. data/docs/Puppeteer/Keyboard.html +1557 -0
  48. data/docs/Puppeteer/Keyboard/KeyDefinition.html +831 -0
  49. data/docs/Puppeteer/Keyboard/KeyDescription.html +603 -0
  50. data/docs/Puppeteer/Launcher.html +237 -0
  51. data/docs/Puppeteer/Launcher/Base.html +385 -0
  52. data/docs/Puppeteer/Launcher/Base/ExecutablePathNotFound.html +124 -0
  53. data/docs/Puppeteer/Launcher/BrowserOptions.html +441 -0
  54. data/docs/Puppeteer/Launcher/Chrome.html +669 -0
  55. data/docs/Puppeteer/Launcher/Chrome/DefaultArgs.html +382 -0
  56. data/docs/Puppeteer/Launcher/ChromeArgOptions.html +531 -0
  57. data/docs/Puppeteer/Launcher/LaunchOptions.html +893 -0
  58. data/docs/Puppeteer/LifecycleWatcher.html +834 -0
  59. data/docs/Puppeteer/LifecycleWatcher/ExpectedLifecycle.html +363 -0
  60. data/docs/Puppeteer/LifecycleWatcher/FrameDetachedError.html +206 -0
  61. data/docs/Puppeteer/LifecycleWatcher/TerminatedError.html +124 -0
  62. data/docs/Puppeteer/Mouse.html +1105 -0
  63. data/docs/Puppeteer/Mouse/Button.html +136 -0
  64. data/docs/Puppeteer/NetworkManager.html +901 -0
  65. data/docs/Puppeteer/NetworkManager/Credentials.html +385 -0
  66. data/docs/Puppeteer/Page.html +5970 -0
  67. data/docs/Puppeteer/Page/FileChooserTimeoutError.html +206 -0
  68. data/docs/Puppeteer/Page/ScreenshotOptions.html +845 -0
  69. data/docs/Puppeteer/Page/ScriptTag.html +555 -0
  70. data/docs/Puppeteer/Page/StyleTag.html +448 -0
  71. data/docs/Puppeteer/Page/TargetCrashedError.html +124 -0
  72. data/docs/Puppeteer/RemoteObject.html +1016 -0
  73. data/docs/Puppeteer/Target.html +1384 -0
  74. data/docs/Puppeteer/Target/InitializeFailure.html +124 -0
  75. data/docs/Puppeteer/Target/TargetInfo.html +729 -0
  76. data/docs/Puppeteer/TimeoutError.html +135 -0
  77. data/docs/Puppeteer/TimeoutSettings.html +496 -0
  78. data/docs/Puppeteer/TouchScreen.html +464 -0
  79. data/docs/Puppeteer/Viewport.html +757 -0
  80. data/docs/Puppeteer/WaitTask.html +637 -0
  81. data/docs/Puppeteer/WaitTask/TerminatedError.html +124 -0
  82. data/docs/Puppeteer/WaitTask/TimeoutError.html +206 -0
  83. data/docs/Puppeteer/WebSocket.html +673 -0
  84. data/docs/Puppeteer/WebSocket/DriverImpl.html +412 -0
  85. data/docs/Puppeteer/WebSocketTransport.html +600 -0
  86. data/docs/Puppeteer/WebSocktTransportError.html +124 -0
  87. data/docs/_index.html +809 -0
  88. data/docs/class_list.html +51 -0
  89. data/docs/css/common.css +1 -0
  90. data/docs/css/full_list.css +58 -0
  91. data/docs/css/style.css +496 -0
  92. data/docs/file.README.html +123 -0
  93. data/docs/file_list.html +56 -0
  94. data/docs/frames.html +17 -0
  95. data/docs/index.html +123 -0
  96. data/docs/js/app.js +314 -0
  97. data/docs/js/full_list.js +216 -0
  98. data/docs/js/jquery.js +4 -0
  99. data/docs/method_list.html +3979 -0
  100. data/docs/top-level-namespace.html +126 -0
  101. data/lib/puppeteer.rb +16 -8
  102. data/lib/puppeteer/async_await_behavior.rb +6 -0
  103. data/lib/puppeteer/browser.rb +21 -1
  104. data/lib/puppeteer/browser_runner.rb +1 -1
  105. data/lib/puppeteer/cdp_session.rb +33 -11
  106. data/lib/puppeteer/connection.rb +1 -1
  107. data/lib/puppeteer/dom_world.rb +142 -121
  108. data/lib/puppeteer/element_handle.rb +223 -181
  109. data/lib/puppeteer/execution_context.rb +41 -17
  110. data/lib/puppeteer/file_chooser.rb +29 -0
  111. data/lib/puppeteer/frame.rb +23 -15
  112. data/lib/puppeteer/frame_manager.rb +7 -9
  113. data/lib/puppeteer/js_handle.rb +3 -3
  114. data/lib/puppeteer/keyboard.rb +1 -1
  115. data/lib/puppeteer/keyboard/us_keyboard_layout.rb +4 -4
  116. data/lib/puppeteer/launcher.rb +0 -1
  117. data/lib/puppeteer/launcher/chrome.rb +48 -2
  118. data/lib/puppeteer/lifecycle_watcher.rb +9 -4
  119. data/lib/puppeteer/mouse.rb +10 -7
  120. data/lib/puppeteer/page.rb +134 -70
  121. data/lib/puppeteer/remote_object.rb +11 -1
  122. data/lib/puppeteer/version.rb +1 -1
  123. data/lib/puppeteer/wait_task.rb +183 -1
  124. data/puppeteer-ruby.gemspec +4 -1
  125. metadata +143 -4
@@ -16,6 +16,11 @@ class Puppeteer::ExecutionContext
16
16
 
17
17
  attr_reader :client, :world
18
18
 
19
+ # only used in DomWorld#delete_context
20
+ def _context_id
21
+ @context_id
22
+ end
23
+
19
24
  # @return [Puppeteer::Frame]
20
25
  def frame
21
26
  if_present(@world) do |world|
@@ -117,7 +122,7 @@ class Puppeteer::ExecutionContext
117
122
  remote_object = Puppeteer::RemoteObject.new(result['result'])
118
123
 
119
124
  if exception_details
120
- raise EvaluationError.new("Evaluation failed: #{exceptionDetails}")
125
+ raise EvaluationError.new("Evaluation failed: #{exception_details}")
121
126
  end
122
127
 
123
128
  if @return_by_value
@@ -211,20 +216,39 @@ class Puppeteer::ExecutionContext
211
216
  # return createJSHandle(this, response.objects);
212
217
  # }
213
218
 
214
- # /**
215
- # * @param {Puppeteer.ElementHandle} elementHandle
216
- # * @return {Promise<Puppeteer.ElementHandle>}
217
- # */
218
- # async _adoptElementHandle(elementHandle) {
219
- # assert(elementHandle.executionContext() !== this, 'Cannot adopt handle that already belongs to this execution context');
220
- # assert(this._world, 'Cannot adopt handle without DOMWorld');
221
- # const nodeInfo = await this._client.send('DOM.describeNode', {
222
- # objectId: elementHandle._remoteObject.objectId,
223
- # });
224
- # const {object} = await this._client.send('DOM.resolveNode', {
225
- # backendNodeId: nodeInfo.node.backendNodeId,
226
- # executionContextId: this._contextId,
227
- # });
228
- # return /** @type {Puppeteer.ElementHandle}*/(createJSHandle(this, object));
229
- # }
219
+ # @param backend_node_id [Integer]
220
+ # @return [Puppeteer::ElementHandle]
221
+ def adopt_backend_node_id(backend_node_id)
222
+ response = @client.send_message('DOM.resolveNode',
223
+ backendNodeId: backend_node_id,
224
+ executionContextId: @context_id,
225
+ )
226
+ Puppeteer::JSHandle.create(
227
+ context: self,
228
+ remote_object: Puppeteer::RemoteObject.new(response["object"]),
229
+ )
230
+ end
231
+
232
+ # @param element_handle [Puppeteer::ElementHandle]
233
+ # @return [Puppeteer::ElementHandle]
234
+ def adopt_element_handle(element_handle)
235
+ if element_handle.execution_context == self
236
+ raise ArgumentError.new('Cannot adopt handle that already belongs to this execution context')
237
+ end
238
+
239
+ unless @world
240
+ raise 'Cannot adopt handle without DOMWorld'
241
+ end
242
+
243
+ node_info = element_handle.remote_object.node_info(@client)
244
+ response = @client.send_message('DOM.resolveNode',
245
+ backendNodeId: node_info["node"]["backendNodeId"],
246
+ executionContextId: @context_id,
247
+ )
248
+
249
+ Puppeteer::JSHandle.create(
250
+ context: self,
251
+ remote_object: Puppeteer::RemoteObject.new(response["object"]),
252
+ )
253
+ end
230
254
  end
@@ -0,0 +1,29 @@
1
+ class Puppeteer::FileChooser
2
+ # @param element [Puppeteer::ElementHandle]
3
+ # @param event [Hash]
4
+ def initialize(element, event)
5
+ @element = element
6
+ @multiple = event['mode'] != 'selectSingle'
7
+ @handled = false
8
+ end
9
+
10
+ def multiple?
11
+ @multiple
12
+ end
13
+
14
+ # @param file_paths [Array<String>]
15
+ def accept(file_paths)
16
+ if @handled
17
+ raise 'Cannot accept FileChooser which is already handled!'
18
+ end
19
+ @handled = true
20
+ @element.upload_file(*file_paths)
21
+ end
22
+
23
+ def cancel
24
+ if @handled
25
+ raise 'Cannot cancel FileChooser which is already handled!'
26
+ end
27
+ @handled = true
28
+ end
29
+ end
@@ -143,8 +143,10 @@ class Puppeteer::Frame
143
143
  @main_world.add_style_tag(style_tag)
144
144
  end
145
145
 
146
- # @param {string} selector
147
- # @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
146
+ # @param selector [String]
147
+ # @param delay [Number]
148
+ # @param button [String] "left"|"right"|"middle"
149
+ # @param click_count [Number]
148
150
  def click(selector, delay: nil, button: nil, click_count: nil)
149
151
  @secondary_world.click(selector, delay: delay, button: button, click_count: click_count)
150
152
  end
@@ -171,11 +173,11 @@ class Puppeteer::Frame
171
173
  @secondary_world.tap(selector)
172
174
  end
173
175
 
174
- # @param {string} selector
175
- # @param {string} text
176
- # @param {{delay: (number|undefined)}=} options
177
- def type(selector, text, delay: nil)
178
- @main_world.type(selector, text, delay: delay)
176
+ # @param selector [String]
177
+ # @param text [String]
178
+ # @param delay [Number]
179
+ def type_text(selector, text, delay: nil)
180
+ @main_world.type_text(selector, text, delay: delay)
179
181
  end
180
182
 
181
183
  # /**
@@ -200,9 +202,10 @@ class Puppeteer::Frame
200
202
  # return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
201
203
  # }
202
204
 
203
- # @param {string} selector
204
- # @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
205
- # @return {!Promise<?Puppeteer.ElementHandle>}
205
+ # @param selector [String]
206
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
207
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
208
+ # @param timeout [Integer]
206
209
  def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
207
210
  handle = @secondary_world.wait_for_selector(selector, visible: visible, hidden: hidden, timeout: timeout)
208
211
  if !handle
@@ -214,9 +217,10 @@ class Puppeteer::Frame
214
217
  result
215
218
  end
216
219
 
217
- # @param {string} xpath
218
- # @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
219
- # @return {!Promise<?Puppeteer.ElementHandle>}
220
+ # @param xpath [String]
221
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
222
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
223
+ # @param timeout [Integer]
220
224
  def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
221
225
  handle = @secondary_world.wait_for_xpath(xpath, visible: visible, hidden: hidden, timeout: timeout)
222
226
  if !handle
@@ -246,6 +250,10 @@ class Puppeteer::Frame
246
250
  # TODO(lushnikov): remove this once requestInterception has loaderId exposed.
247
251
  @navigation_url = frame_payload['url']
248
252
  @url = frame_payload['url']
253
+
254
+ # Ensure loaderId updated.
255
+ # The order of [Page.lifecycleEvent name="init"] and [Page.frameNavigated] is random... for some reason...
256
+ @loader_id = frame_payload['loaderId']
249
257
  end
250
258
 
251
259
  # @param url [String]
@@ -268,8 +276,8 @@ class Puppeteer::Frame
268
276
 
269
277
  def detach
270
278
  @detached = true
271
- # this._mainWorld._detach();
272
- # this._secondaryWorld._detach();
279
+ @main_world.detach
280
+ @secondary_world.detach
273
281
  if @parent_frame
274
282
  @parent_frame._child_frames.delete(self)
275
283
  end
@@ -206,12 +206,12 @@ class Puppeteer::FrameManager
206
206
  # @param {string} frameId
207
207
  # @param {?string} parentFrameId
208
208
  def handle_frame_attached(frame_id, parent_frame_id)
209
- return if @frames.has_key?[frame_id]
209
+ return if @frames.has_key?(frame_id)
210
210
  if !parent_frame_id
211
211
  raise ArgymentError.new('parent_frame_id must not be nil')
212
212
  end
213
213
  parent_frame = @frames[parent_frame_id]
214
- frame = Frame.new(self, @client, parent_frame, frame_id)
214
+ frame = Puppeteer::Frame.new(self, @client, parent_frame, frame_id)
215
215
  @frames[frame_id] = frame
216
216
 
217
217
  emit_event 'Events.FrameManager.FrameAttached', frame
@@ -219,7 +219,7 @@ class Puppeteer::FrameManager
219
219
 
220
220
  # @param frame_payload [Hash]
221
221
  def handle_frame_navigated(frame_payload)
222
- is_main_frame = !frame_payload['parent_id']
222
+ is_main_frame = !frame_payload['parentId']
223
223
  frame =
224
224
  if is_main_frame
225
225
  @main_frame
@@ -285,8 +285,6 @@ class Puppeteer::FrameManager
285
285
  frame.navigated_within_document(url)
286
286
  emit_event 'Events.FrameManager.FrameNavigatedWithinDocument', frame
287
287
  emit_event 'Events.FrameManager.FrameNavigated', frame
288
- handle_frame_manager_frame_navigated_within_document(frame)
289
- handle_frame_manager_frame_navigated(frame)
290
288
  end
291
289
 
292
290
  # @param frame_id [String]
@@ -332,12 +330,12 @@ class Puppeteer::FrameManager
332
330
  @context_id_to_context.delete(execution_context_id)
333
331
  @context_id_created.delete(execution_context_id)
334
332
  if context.world
335
- context.world.context = nil
333
+ context.world.delete_context(execution_context_id)
336
334
  end
337
335
  end
338
336
 
339
337
  def handle_execution_contexts_cleared
340
- # executionContextCleared is often notified after executionContextCreated.
338
+ # executionContextsCleared is often notified after executionContextCreated.
341
339
  # D, [2020-04-06T01:47:03.101227 #13823] DEBUG -- : RECV << {"method"=>"Runtime.executionContextCreated", "params"=>{"context"=>{"id"=>5, "origin"=>"https://github.com", "name"=>"", "auxData"=>{"isDefault"=>true, "type"=>"default", "frameId"=>"71C347B70848B89DDDEFAA8AB5B0BC92"}}}, "sessionId"=>"53F088EED260C28001D26A019F95D9E3"}
342
340
  # D, [2020-04-06T01:47:03.101439 #13823] DEBUG -- : RECV << {"method"=>"Page.frameNavigated", "params"=>{"frame"=>{"id"=>"71C347B70848B89DDDEFAA8AB5B0BC92", "loaderId"=>"80338225D035AC96BAE8F6D4E81C7D51", "url"=>"https://github.com/search?q=puppeteer", "securityOrigin"=>"https://github.com", "mimeType"=>"text/html"}}, "sessionId"=>"53F088EED260C28001D26A019F95D9E3"}
343
341
  # D, [2020-04-06T01:47:03.101325 #13823] DEBUG -- : RECV << {"method"=>"Target.targetInfoChanged", "params"=>{"targetInfo"=>{"targetId"=>"71C347B70848B89DDDEFAA8AB5B0BC92", "type"=>"page", "title"=>"https://github.com/search?q=puppeteer", "url"=>"https://github.com/search?q=puppeteer", "attached"=>true, "browserContextId"=>"AF37BC660284CE1552B4ECB147BE9305"}}}
@@ -346,9 +344,9 @@ class Puppeteer::FrameManager
346
344
  # To avoid the problem, just skip recent created ids.
347
345
  now = Time.now
348
346
  context_ids_to_skip = @context_id_created.select { |k, v| now - v < 1 }.keys
349
- @context_id_to_context.reject { |k, v| context_ids_to_skip.include?(k) }.each_value do |context|
347
+ @context_id_to_context.reject { |k, v| context_ids_to_skip.include?(k) }.each do |execution_context_id, context|
350
348
  if context.world
351
- context.world.context = nil
349
+ context.world.delete_context(execution_context_id)
352
350
  end
353
351
  end
354
352
  @context_id_to_context.select! { |k, v| context_ids_to_skip.include?(k) }
@@ -89,8 +89,9 @@ class Puppeteer::JSHandle
89
89
  response['result'].each_with_object({}) do |prop, h|
90
90
  next unless prop['enumerable']
91
91
  h[prop['name']] = Puppeteer::JSHandle.create(
92
- context: @context,
93
- remote_object: Puppeteer::RemoteObject.new(prop['value']))
92
+ context: @context,
93
+ remote_object: Puppeteer::RemoteObject.new(prop['value']),
94
+ )
94
95
  end
95
96
  end
96
97
 
@@ -116,7 +117,6 @@ class Puppeteer::JSHandle
116
117
  nil
117
118
  end
118
119
 
119
- # @return [Future]
120
120
  def dispose
121
121
  return if @disposed
122
122
 
@@ -178,6 +178,6 @@ class Puppeteer::Keyboard
178
178
  # @param key [String]
179
179
  # @return [Future]
180
180
  async def async_press(key, delay: nil)
181
- press(key, delay)
181
+ press(key, delay: delay)
182
182
  end
183
183
  end
@@ -41,10 +41,10 @@ class Puppeteer::Keyboard
41
41
  'Backspace': KeyDefinition.new({ 'keyCode': 8, 'code': 'Backspace', 'key': 'Backspace' }),
42
42
  'Tab': KeyDefinition.new({ 'keyCode': 9, 'code': 'Tab', 'key': 'Tab' }),
43
43
  'Numpad5': KeyDefinition.new({ 'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3 }),
44
- 'NumpadEnter': KeyDefinition.new({ 'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': '\r', 'location': 3 }),
45
- 'Enter': KeyDefinition.new({ 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r' }),
46
- '\r': KeyDefinition.new({ 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r' }),
47
- '\n': KeyDefinition.new({ 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r' }),
44
+ 'NumpadEnter': KeyDefinition.new({ 'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': "\r", 'location': 3 }),
45
+ 'Enter': KeyDefinition.new({ 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': "\r" }),
46
+ "\r": KeyDefinition.new({ 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': "\r" }),
47
+ "\n": KeyDefinition.new({ 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': "\r" }),
48
48
  'ShiftLeft': KeyDefinition.new({ 'keyCode': 16, 'code': 'ShiftLeft', 'key': 'Shift', 'location': 1 }),
49
49
  'ShiftRight': KeyDefinition.new({ 'keyCode': 16, 'code': 'ShiftRight', 'key': 'Shift', 'location': 2 }),
50
50
  'ControlLeft': KeyDefinition.new({ 'keyCode': 17, 'code': 'ControlLeft', 'key': 'Control', 'location': 1 }),
@@ -6,7 +6,6 @@ require_relative './launcher/launch_options'
6
6
 
7
7
  # https://github.com/puppeteer/puppeteer/blob/master/lib/Launcher.js
8
8
  module Puppeteer::Launcher
9
-
10
9
  # @param {string} projectRoot
11
10
  # @param {string} preferredRevision
12
11
  # @param {boolean} isPuppeteerCore
@@ -149,8 +149,54 @@ module Puppeteer::Launcher
149
149
 
150
150
  # @param {!(Launcher.BrowserOptions & {browserWSEndpoint?: string, browserURL?: string, transport?: !Puppeteer.ConnectionTransport})} options
151
151
  # @return {!Promise<!Browser>}
152
- def connect(options)
153
- raise NotImplementedError.new('Puppeteer.connect is not implemented yet')
152
+ def connect(options = {})
153
+ @browser_options = BrowserOptions.new(options)
154
+ browser_ws_endpoint = options[:browser_ws_endpoint]
155
+ browser_url = options[:browser_url]
156
+ transport = options[:transport]
157
+
158
+ connection =
159
+ if browser_ws_endpoint && browser_url.nil? && transport.nil?
160
+ connect_with_browser_ws_endpoint(browser_ws_endpoint)
161
+ elsif browser_ws_endpoint.nil? && browser_url && transport.nil?
162
+ connect_with_browser_url(browser_url)
163
+ elsif browser_ws_endpoint.nil? && browser_url.nil? && transport
164
+ connect_with_transport(transport)
165
+ else
166
+ raise ArgumentError.new("Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect")
167
+ end
168
+
169
+ result = connection.send_message('Target.getBrowserContexts')
170
+ browser_context_ids = result['browserContextIds']
171
+
172
+ Puppeteer::Browser.create(
173
+ connection: connection,
174
+ context_ids: browser_context_ids,
175
+ ignore_https_errors: @browser_options.ignore_https_errors?,
176
+ default_viewport: @browser_options.default_viewport,
177
+ process: nil,
178
+ close_callback: -> { connection.send_message('Browser.close') },
179
+ )
180
+ end
181
+
182
+ # @return [Puppeteer::Connection]
183
+ private def connect_with_browser_ws_endpoint(browser_ws_endpoint)
184
+ transport = Puppeteer::WebSocketTransport.create(browser_ws_endpoint)
185
+ Puppeteer::Connection.new(browser_ws_endpoint, transport, @browser_options.slow_mo)
186
+ end
187
+
188
+ # @return [Puppeteer::Connection]
189
+ private def connect_with_browser_url(browser_url)
190
+ raise NotImplementedError.new('Puppeteer#connect with browserUrl is not implemented yet.')
191
+ # const connectionURL = await getWSEndpoint(browserURL);
192
+ # const connectionTransport = await WebSocketTransport.create(
193
+ # connectionURL
194
+ # );
195
+ end
196
+
197
+ # @return [Puppeteer::Connection]
198
+ private def connect_with_transport(transport)
199
+ Puppeteer::Connection.new('', transport, @browser_options.slow_mo)
154
200
  end
155
201
 
156
202
  # @return {string}
@@ -50,6 +50,11 @@ class Puppeteer::LifecycleWatcher
50
50
  end
51
51
  end
52
52
 
53
+ class FrameDetachedError < StandardError
54
+ def initialize
55
+ super('Navigating frame was detached')
56
+ end
57
+ end
53
58
  class TerminatedError < StandardError; end
54
59
 
55
60
  # * @param {!Puppeteer.FrameManager} frameManager
@@ -68,7 +73,7 @@ class Puppeteer::LifecycleWatcher
68
73
  terminate(TerminatedError.new('Navigation failed because browser has disconnected!'))
69
74
  end
70
75
  @listener_ids['frame_manager'] = [
71
- @frame_manager.add_event_listener('Events.FrameManager.LifecycleEvent') do |frame|
76
+ @frame_manager.add_event_listener('Events.FrameManager.LifecycleEvent') do |_|
72
77
  check_lifecycle_complete
73
78
  end,
74
79
  @frame_manager.add_event_listener('Events.FrameManager.FrameNavigatedWithinDocument', &method(:navigated_within_document)),
@@ -92,7 +97,7 @@ class Puppeteer::LifecycleWatcher
92
97
  # @param frame [Puppeteer::Frame]
93
98
  def handle_frame_detached(frame)
94
99
  if @frame == frame
95
- # this._terminationCallback.call(null, new Error('Navigating frame was detached'));
100
+ @termination_promise.reject(FrameDetachedError.new)
96
101
  return
97
102
  end
98
103
  check_lifecycle_complete
@@ -144,10 +149,10 @@ class Puppeteer::LifecycleWatcher
144
149
  if @frame.loader_id == @initial_loader_id && !@has_same_document_navigation
145
150
  return
146
151
  end
147
- if @has_same_document_navigation
152
+ if @has_same_document_navigation && @same_document_navigation_promise.pending?
148
153
  @same_document_navigation_promise.fulfill(true)
149
154
  end
150
- if @frame.loader_id != @initial_loader_id
155
+ if @frame.loader_id != @initial_loader_id && @new_document_navigation_promise.pending?
151
156
  @new_document_navigation_promise.fulfill(true)
152
157
  end
153
158
  end
@@ -37,8 +37,8 @@ class Puppeteer::Mouse
37
37
  @client.send_message('Input.dispatchMouseEvent',
38
38
  type: 'mouseMoved',
39
39
  button: @button,
40
- x: from_x + (@x - from_x) * n / steps,
41
- y: from_y + (@y - from_y) * n / steps,
40
+ x: from_x + (@x - from_x) * n / move_steps,
41
+ y: from_y + (@y - from_y) * n / move_steps,
42
42
  modifiers: @keyboard.modifiers,
43
43
  )
44
44
  end
@@ -56,16 +56,19 @@ class Puppeteer::Mouse
56
56
  # @param y [number]
57
57
  # @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
58
58
  def click(x, y, delay: nil, button: nil, click_count: nil)
59
+ # await_all(async_move, async_down, async_up) often breaks the order of CDP commands.
60
+ # D, [2020-04-15T17:09:47.895895 #88683] DEBUG -- : RECV << {"id"=>23, "result"=>{"layoutViewport"=>{"pageX"=>0, "pageY"=>1, "clientWidth"=>375, "clientHeight"=>667}, "visualViewport"=>{"offsetX"=>0, "offsetY"=>0, "pageX"=>0, "pageY"=>1, "clientWidth"=>375, "clientHeight"=>667, "scale"=>1, "zoom"=>1}, "contentSize"=>{"x"=>0, "y"=>0, "width"=>375, "height"=>2007}}, "sessionId"=>"0B09EA5E18DEE403E525B3E7FCD7E225"}
61
+ # D, [2020-04-15T17:09:47.898422 #88683] DEBUG -- : SEND >> {"sessionId":"0B09EA5E18DEE403E525B3E7FCD7E225","method":"Input.dispatchMouseEvent","params":{"type":"mouseReleased","button":"left","x":0,"y":0,"modifiers":0,"clickCount":1},"id":24}
62
+ # D, [2020-04-15T17:09:47.899711 #88683] DEBUG -- : SEND >> {"sessionId":"0B09EA5E18DEE403E525B3E7FCD7E225","method":"Input.dispatchMouseEvent","params":{"type":"mousePressed","button":"left","x":0,"y":0,"modifiers":0,"clickCount":1},"id":25}
63
+ # D, [2020-04-15T17:09:47.900237 #88683] DEBUG -- : SEND >> {"sessionId":"0B09EA5E18DEE403E525B3E7FCD7E225","method":"Input.dispatchMouseEvent","params":{"type":"mouseMoved","button":"left","x":187,"y":283,"modifiers":0},"id":26}
64
+ # So we execute move in advance.
65
+ move(x, y)
59
66
  if delay
60
- await_all(
61
- async_move(x, y),
62
- async_down(button: button, click_count: click_count),
63
- )
67
+ down(button: button, click_count: click_count)
64
68
  sleep(delay / 1000.0)
65
69
  up(button: button, click_count: click_count)
66
70
  else
67
71
  await_all(
68
- async_move(x, y),
69
72
  async_down(button: button, click_count: click_count),
70
73
  async_up(button: button, click_count: click_count),
71
74
  )
@@ -48,10 +48,10 @@ class Puppeteer::Page
48
48
  if event['targetInfo']['type'] != 'worker'
49
49
  # If we don't detach from service workers, they will never die.
50
50
  await @client.send_message('Target.detachFromTarget', sessionId: event['sessionId'])
51
- return
51
+ next
52
52
  end
53
53
 
54
- session = Puppeteer::Connection.from_session(@client).session(event['sessionId'])
54
+ session = Puppeteer::Connection.from_session(@client).session(event['sessionId']) # rubocop:disable Lint/UselessAssignment
55
55
  # const worker = new Worker(session, event.targetInfo.url, this._addConsoleMessage.bind(this), this._handleException.bind(this));
56
56
  # this._workers.set(event.sessionId, worker);
57
57
  # this.emit(Events.Page.WorkerCreated, worker);
@@ -59,7 +59,7 @@ class Puppeteer::Page
59
59
  @client.on_event 'Target.detachedFromTarget' do |event|
60
60
  session_id = event['sessionId']
61
61
  worker = @workers[session_id]
62
- return unless worker
62
+ next unless worker
63
63
 
64
64
  emit_event('Events.Page.WorkerDestroyed', worker)
65
65
  @workers.delete(session_id)
@@ -88,8 +88,8 @@ class Puppeteer::Page
88
88
  network_manager.on_event 'Events.NetworkManager.RequestFinished' do |event|
89
89
  emit_event 'Events.Page.RequestFinished', event
90
90
  end
91
- # this._fileChooserInterceptionIsDisabled = false;
92
- # this._fileChooserInterceptors = new Set();
91
+ @file_chooser_interception_is_disabled = false
92
+ @file_chooser_interceptors = Set.new
93
93
 
94
94
  @client.on_event 'Page.domContentEventFired' do |event|
95
95
  emit_event 'Events.Page.DOMContentLoaded'
@@ -106,7 +106,9 @@ class Puppeteer::Page
106
106
  @client.on_event 'Log.entryAdded' do |event|
107
107
  handle_log_entry_added(event)
108
108
  end
109
- # client.on('Page.fileChooserOpened', event => this._onFileChooser(event));
109
+ @client.on_event 'Page.fileChooserOpened' do |event|
110
+ handle_file_chooser(event)
111
+ end
110
112
  @target.on_close do
111
113
  emit_event 'Events.Page.Close'
112
114
  @closed = true
@@ -122,41 +124,53 @@ class Puppeteer::Page
122
124
  )
123
125
  end
124
126
 
125
- # /**
126
- # * @param {!Protocol.Page.fileChooserOpenedPayload} event
127
- # */
128
- # async _onFileChooser(event) {
129
- # if (!this._fileChooserInterceptors.size)
130
- # return;
131
- # const frame = this._frameManager.frame(event.frameId);
132
- # const context = await frame.executionContext();
133
- # const element = await context._adoptBackendNodeId(event.backendNodeId);
134
- # const interceptors = Array.from(this._fileChooserInterceptors);
135
- # this._fileChooserInterceptors.clear();
136
- # const fileChooser = new FileChooser(this._client, element, event);
137
- # for (const interceptor of interceptors)
138
- # interceptor.call(null, fileChooser);
139
- # }
127
+ def handle_file_chooser(event)
128
+ return if @file_chooser_interceptors.empty?
129
+
130
+ frame = @frame_manager.frame(event['frameId'])
131
+ context = frame.execution_context
132
+ element = context.adopt_backend_node_id(event['backendNodeId'])
133
+ interceptors = @file_chooser_interceptors.to_a
134
+ @file_chooser_interceptors.clear
135
+ file_chooser = Puppeteer::FileChooser.new(element, event)
136
+ interceptors.each do |promise|
137
+ promise.fulfill(file_chooser)
138
+ end
139
+ end
140
140
 
141
- # /**
142
- # * @param {!{timeout?: number}=} options
143
- # * @return !Promise<!FileChooser>}
144
- # */
145
- # async waitForFileChooser(options = {}) {
146
- # if (!this._fileChooserInterceptors.size)
147
- # await this._client.send('Page.setInterceptFileChooserDialog', {enabled: true});
141
+ class FileChooserTimeoutError < StandardError
142
+ def initialize(timeout:)
143
+ super("waiting for filechooser failed: timeout #{timeout}ms exceeded")
144
+ end
145
+ end
148
146
 
149
- # const {
150
- # timeout = this._timeoutSettings.timeout(),
151
- # } = options;
152
- # let callback;
153
- # const promise = new Promise(x => callback = x);
154
- # this._fileChooserInterceptors.add(callback);
155
- # return helper.waitWithTimeout(promise, 'waiting for file chooser', timeout).catch(e => {
156
- # this._fileChooserInterceptors.delete(callback);
157
- # throw e;
158
- # });
159
- # }
147
+ # @param timeout [Integer]
148
+ # @return [Puppeteer::FileChooser]
149
+ def wait_for_file_chooser(timeout: nil)
150
+ if @file_chooser_interceptors.empty?
151
+ @client.send_message('Page.setInterceptFileChooserDialog', enabled: true)
152
+ end
153
+
154
+ option_timeout = timeout || @timeout_settings.timeout
155
+ promise = resolvable_future
156
+ @file_chooser_interceptors << promise
157
+
158
+ begin
159
+ Timeout.timeout(option_timeout / 1000.0) do
160
+ promise.value!
161
+ end
162
+ rescue Timeout::Error
163
+ raise FileChooserTimeoutError.new(timeout: option_timeout)
164
+ ensure
165
+ @file_chooser_interceptors.delete(promise)
166
+ end
167
+ end
168
+
169
+ # @param timeout [Integer]
170
+ # @return [Future<Puppeteer::FileChooser>]
171
+ async def async_wait_for_file_chooser(timeout: nil)
172
+ wait_for_file_chooser(timeout: timeout)
173
+ end
160
174
 
161
175
 
162
176
  # /**
@@ -173,7 +187,7 @@ class Puppeteer::Page
173
187
  # await this._client.send('Emulation.setGeolocationOverride', {longitude, latitude, accuracy});
174
188
  # }
175
189
 
176
- attr_reader :target
190
+ attr_reader :javascript_enabled, :target
177
191
 
178
192
  def browser
179
193
  @target.browser
@@ -204,7 +218,9 @@ class Puppeteer::Page
204
218
  end
205
219
  if source != 'worker'
206
220
  console_message_location = Puppeteer::ConsoleMessage::Location.new(
207
- url: url, line_number: line_number)
221
+ url: url,
222
+ line_number: line_number,
223
+ )
208
224
  emit_event('Events.Page.Console',
209
225
  Puppeteer::ConsoleMessage.new(level, text, [], console_message_location))
210
226
  end
@@ -273,23 +289,37 @@ class Puppeteer::Page
273
289
  end
274
290
 
275
291
  # `$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
276
- # @param {string} selector
277
- # @param {Function|string} pageFunction
278
- # @param {!Array<*>} args
279
- # @return {!Promise<(!Object|undefined)>}
292
+ # @param selector [String]
293
+ # @param page_function [String]
294
+ # @return [Object]
280
295
  def Seval(selector, page_function, *args)
281
296
  main_frame.Seval(selector, page_function, *args)
282
297
  end
283
298
 
299
+ # `$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
300
+ # @param selector [String]
301
+ # @param page_function [String]
302
+ # @return [Future]
303
+ async def async_Seval(selector, page_function, *args)
304
+ Seval(selector, page_function, *args)
305
+ end
306
+
284
307
  # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
285
- # @param {string} selector
286
- # @param {Function|string} pageFunction
287
- # @param {!Array<*>} args
288
- # @return {!Promise<(!Object|undefined)>}
308
+ # @param selector [String]
309
+ # @param page_function [String]
310
+ # @return [Object]
289
311
  def SSeval(selector, page_function, *args)
290
312
  main_frame.SSeval(selector, page_function, *args)
291
313
  end
292
314
 
315
+ # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
316
+ # @param selector [String]
317
+ # @param page_function [String]
318
+ # @return [Future]
319
+ async def async_SSeval(selector, page_function, *args)
320
+ SSeval(selector, page_function, *args)
321
+ end
322
+
293
323
  # `$x()` in JavaScript. $ is not allowed to use as a method name in Ruby.
294
324
  # @param {string} expression
295
325
  # @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
@@ -608,14 +638,14 @@ class Puppeteer::Page
608
638
  main_frame.goto(url, referer: referer, timeout: timeout, wait_until: wait_until)
609
639
  end
610
640
 
611
- # @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
612
- # @return {!Promise<?Puppeteer.Response>}
641
+ # @param timeout [number|nil]
642
+ # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
643
+ # @return [Puppeteer::Response]
613
644
  def reload(timeout: nil, wait_until: nil)
614
- # const [response] = await Promise.all([
615
- # this.waitForNavigation(options),
616
- # this._client.send('Page.reload')
617
- # ]);
618
- # return response;
645
+ await_all(
646
+ async_wait_for_navigation(timeout: timeout, wait_until: wait_until),
647
+ @client.async_send_message('Page.reload'),
648
+ ).first
619
649
  end
620
650
 
621
651
  # @param timeout [number|nil]
@@ -932,12 +962,23 @@ class Puppeteer::Page
932
962
 
933
963
  attr_reader :mouse
934
964
 
935
- # @param {string} selector
936
- # @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
965
+ # @param selector [String]
966
+ # @param delay [Number]
967
+ # @param button [String] "left"|"right"|"middle"
968
+ # @param click_count [Number]
937
969
  def click(selector, delay: nil, button: nil, click_count: nil)
938
970
  main_frame.click(selector, delay: delay, button: button, click_count: click_count)
939
971
  end
940
972
 
973
+ # @param selector [String]
974
+ # @param delay [Number]
975
+ # @param button [String] "left"|"right"|"middle"
976
+ # @param click_count [Number]
977
+ # @return [Future]
978
+ async def async_click(selector, delay: nil, button: nil, click_count: nil)
979
+ click(selector, delay: delay, button: button, click_count: click_count)
980
+ end
981
+
941
982
  # @param {string} selector
942
983
  def focus(selector)
943
984
  main_frame.focus(selector)
@@ -955,16 +996,21 @@ class Puppeteer::Page
955
996
  main_frame.select(selector, *values)
956
997
  end
957
998
 
958
- # @param {string} selector
999
+ # @param selector [String]
959
1000
  def tap(selector)
960
1001
  main_frame.tap(selector)
961
1002
  end
962
1003
 
963
- # @param {string} selector
964
- # @param {string} text
965
- # @param {{delay: (number|undefined)}=} options
966
- def type(selector, text, delay: nil)
967
- main_frame.type(selector, text, delay: delay)
1004
+ # @param selector [String]
1005
+ async def async_tap(selector)
1006
+ tap(selector)
1007
+ end
1008
+
1009
+ # @param selector [String]
1010
+ # @param text [String]
1011
+ # @param delay [Number]
1012
+ def type_text(selector, text, delay: nil)
1013
+ main_frame.type_text(selector, text, delay: delay)
968
1014
  end
969
1015
 
970
1016
  # /**
@@ -977,20 +1023,38 @@ class Puppeteer::Page
977
1023
  # return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
978
1024
  # }
979
1025
 
980
- # @param {string} selector
981
- # @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
982
- # @return {!Promise<?Puppeteer.ElementHandle>}
1026
+ # @param selector [String]
1027
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1028
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
1029
+ # @param timeout [Integer]
983
1030
  def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
984
1031
  main_frame.wait_for_selector(selector, visible: visible, hidden: hidden, timeout: timeout)
985
1032
  end
986
1033
 
987
- # @param {string} xpath
988
- # @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
989
- # @return {!Promise<?Puppeteer.ElementHandle>}
1034
+ # @param selector [String]
1035
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1036
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
1037
+ # @param timeout [Integer]
1038
+ async def async_wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
1039
+ wait_for_selector(selector, visible: visible, hidden: hidden, timeout: timeout)
1040
+ end
1041
+
1042
+ # @param xpath [String]
1043
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1044
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
1045
+ # @param timeout [Integer]
990
1046
  def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
991
1047
  main_frame.wait_for_xpath(xpath, visible: visible, hidden: hidden, timeout: timeout)
992
1048
  end
993
1049
 
1050
+ # @param xpath [String]
1051
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1052
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
1053
+ # @param timeout [Integer]
1054
+ async def async_wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
1055
+ wait_for_xpath(xpath, visible: visible, hidden: hidden, timeout: timeout)
1056
+ end
1057
+
994
1058
  # @param {Function|string} pageFunction
995
1059
  # @param {!{polling?: string|number, timeout?: number}=} options
996
1060
  # @param {!Array<*>} args