puppeteer-ruby 0.0.15 → 0.0.20

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.
@@ -156,6 +156,6 @@ class Puppeteer::BrowserRunner
156
156
  end
157
157
  end
158
158
  rescue Timeout::Error
159
- raise Puppeteer::TimeoutError.new("Timed out after #{timeout} ms while trying to connect to the browser! Only Chrome at revision r#{preferredRevision} is guaranteed to work.")
159
+ raise Puppeteer::TimeoutError.new("Timed out after #{timeout} ms while trying to connect to the browser! Only Chrome at revision r#{preferred_revision} is guaranteed to work.")
160
160
  end
161
161
  end
@@ -81,4 +81,14 @@ class Puppeteer::CDPSession
81
81
  @connection = nil
82
82
  emit_event 'Events.CDPSession.Disconnected'
83
83
  end
84
+
85
+ # @param event_name [String]
86
+ def on(event_name, &block)
87
+ add_event_listener(event_name, &block)
88
+ end
89
+
90
+ # @param event_name [String]
91
+ def once(event_name, &block)
92
+ observe_first(event_name, &block)
93
+ end
84
94
  end
@@ -49,13 +49,18 @@ class Puppeteer::Connection
49
49
  async_handle_message(message)
50
50
  end
51
51
  @transport.on_close do |reason, code|
52
- handle_close(reason, code)
52
+ handle_close
53
53
  end
54
54
 
55
55
  @sessions = {}
56
56
  @closed = false
57
57
  end
58
58
 
59
+ # used only in Browser#connected?
60
+ def closed?
61
+ @closed
62
+ end
63
+
59
64
  private def sleep_before_handling_message(message)
60
65
  # Puppeteer doesn't handle any Network monitoring responses.
61
66
  # So we don't have to sleep.
@@ -199,8 +204,8 @@ class Puppeteer::Connection
199
204
  callback.reject(
200
205
  ProtocolError.new(
201
206
  method: callback.method,
202
- error_message: response['error']['message'],
203
- error_data: response['error']['data']))
207
+ error_message: message['error']['message'],
208
+ error_data: message['error']['data']))
204
209
  else
205
210
  callback.resolve(message['result'])
206
211
  end
@@ -1,9 +1,9 @@
1
1
  require 'logger'
2
2
 
3
3
  module Puppeteer::DebugPrint
4
- if ['1', 'true'].include?(ENV['DEBUG'])
4
+ if Puppeteer.env.debug?
5
5
  def debug_puts(*args, **kwargs)
6
- @__debug_logger ||= Logger.new(STDOUT)
6
+ @__debug_logger ||= Logger.new($stdout)
7
7
  @__debug_logger.debug(*args, **kwargs)
8
8
  end
9
9
 
@@ -14,7 +14,7 @@ module Puppeteer::DefineAsyncMethod
14
14
  Concurrent::Promises.future do
15
15
  original_method.bind(self).call(*args)
16
16
  rescue => err
17
- Logger.new(STDERR).warn(err)
17
+ Logger.new($stderr).warn(err)
18
18
  raise err
19
19
  end
20
20
  end
@@ -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
@@ -314,25 +314,28 @@ class Puppeteer::DOMWorld
314
314
  # }
315
315
  # }
316
316
 
317
+ class ElementNotFoundError < StandardError
318
+ def initialize(selector)
319
+ super("No node found for selector: #{selector}")
320
+ end
321
+ end
322
+
317
323
  # @param selector [String]
318
324
  # @param delay [Number]
319
325
  # @param button [String] "left"|"right"|"middle"
320
326
  # @param click_count [Number]
321
327
  def click(selector, delay: nil, button: nil, click_count: nil)
322
- handle = S(selector)
328
+ handle = S(selector) or raise ElementNotFoundError.new(selector)
323
329
  handle.click(delay: delay, button: button, click_count: click_count)
324
330
  handle.dispose
325
331
  end
326
332
 
327
- # /**
328
- # * @param {string} selector
329
- # */
330
- # async focus(selector) {
331
- # const handle = await this.$(selector);
332
- # assert(handle, 'No node found for selector: ' + selector);
333
- # await handle.focus();
334
- # await handle.dispose();
335
- # }
333
+ # @param selector [String]
334
+ def focus(selector)
335
+ handle = S(selector) or raise ElementNotFoundError.new(selector)
336
+ handle.focus
337
+ handle.dispose
338
+ end
336
339
 
337
340
  # /**
338
341
  # * @param {string} selector
@@ -347,7 +350,7 @@ class Puppeteer::DOMWorld
347
350
  # @param selector [String]
348
351
  # @return [Array<String>]
349
352
  def select(selector, *values)
350
- handle = S(selector)
353
+ handle = S(selector) or raise ElementNotFoundError.new(selector)
351
354
  result = handle.select(*values)
352
355
  handle.dispose
353
356
 
@@ -356,7 +359,7 @@ class Puppeteer::DOMWorld
356
359
 
357
360
  # @param selector [String]
358
361
  def tap(selector)
359
- handle = S(selector)
362
+ handle = S(selector) or raise ElementNotFoundError.new(selector)
360
363
  handle.tap
361
364
  handle.dispose
362
365
  end
@@ -365,7 +368,7 @@ class Puppeteer::DOMWorld
365
368
  # @param text [String]
366
369
  # @param delay [Number]
367
370
  def type_text(selector, text, delay: nil)
368
- handle = S(selector)
371
+ handle = S(selector) or raise ElementNotFoundError.new(selector)
369
372
  handle.type_text(text, delay: delay)
370
373
  handle.dispose
371
374
  end
@@ -399,6 +402,26 @@ class Puppeteer::DOMWorld
399
402
  # return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
400
403
  # }
401
404
 
405
+ # @param page_function [String]
406
+ # @param args [Array]
407
+ # @param polling [Integer|String]
408
+ # @param timeout [Integer]
409
+ # @return [Puppeteer::JSHandle]
410
+ def wait_for_function(page_function, args: [], polling: nil, timeout: nil)
411
+ option_polling = polling || 'raf'
412
+ option_timeout = timeout || @timeout_settings.timeout
413
+
414
+ Puppeteer::WaitTask.new(
415
+ dom_world: self,
416
+ predicate_body: page_function,
417
+ title: 'function',
418
+ polling: option_polling,
419
+ timeout: option_timeout,
420
+ args: args,
421
+ ).await_promise
422
+ end
423
+
424
+
402
425
  # @return [String]
403
426
  def title
404
427
  evaluate('() => document.title')
@@ -424,7 +447,7 @@ class Puppeteer::DOMWorld
424
447
 
425
448
  wait_task = Puppeteer::WaitTask.new(
426
449
  dom_world: self,
427
- predicate_body: "return (#{PREDICATE})(...args)",
450
+ predicate_body: PREDICATE,
428
451
  title: title,
429
452
  polling: polling,
430
453
  timeout: option_timeout,
@@ -3,6 +3,7 @@ require_relative './element_handle/box_model'
3
3
  require_relative './element_handle/point'
4
4
 
5
5
  class Puppeteer::ElementHandle < Puppeteer::JSHandle
6
+ include Puppeteer::DebugPrint
6
7
  include Puppeteer::IfPresent
7
8
  using Puppeteer::DefineAsyncMethod
8
9
 
@@ -23,7 +24,7 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
23
24
  end
24
25
 
25
26
  def content_frame
26
- node_info = @remote_object.node_info
27
+ node_info = @remote_object.node_info(@client)
27
28
  frame_id = node_info['node']['frameId']
28
29
  if frame_id.is_a?(String)
29
30
  @frame_manager.frame(frame_id)
@@ -42,7 +43,24 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
42
43
  if (element.nodeType !== Node.ELEMENT_NODE)
43
44
  return 'Node is not of type HTMLElement';
44
45
 
45
- 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
+ }
46
64
  return false;
47
65
  }
48
66
  JAVASCRIPT
@@ -62,7 +80,14 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
62
80
  end
63
81
 
64
82
  def clickable_point
65
- result = @remote_object.content_quads(@client)
83
+ result =
84
+ begin
85
+ @remote_object.content_quads(@client)
86
+ rescue => err
87
+ debug_puts(err)
88
+ nil
89
+ end
90
+
66
91
  if !result || result["quads"].empty?
67
92
  raise ElementNotVisibleError.new
68
93
  end
@@ -208,10 +233,11 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
208
233
  define_async_method :async_type_text
209
234
 
210
235
  # @param key [String]
236
+ # @param text [String]
211
237
  # @param delay [number|nil]
212
- def press(key, delay: nil)
238
+ def press(key, delay: nil, text: nil)
213
239
  focus
214
- @page.keyboard.press(key, delay: delay)
240
+ @page.keyboard.press(key, delay: delay, text: text)
215
241
  end
216
242
 
217
243
  define_async_method :async_press
@@ -379,21 +405,24 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
379
405
 
380
406
  define_async_method :async_Sx
381
407
 
382
- # /**
383
- # * @returns {!Promise<boolean>}
384
- # */
385
- # isIntersectingViewport() {
386
- # return this.evaluate(async element => {
387
- # const visibleRatio = await new Promise(resolve => {
388
- # const observer = new IntersectionObserver(entries => {
389
- # resolve(entries[0].intersectionRatio);
390
- # observer.disconnect();
391
- # });
392
- # observer.observe(element);
393
- # });
394
- # return visibleRatio > 0;
395
- # });
396
- # }
408
+ # in JS, #isIntersectingViewport.
409
+ # @return [Boolean]
410
+ def intersecting_viewport?
411
+ js = <<~JAVASCRIPT
412
+ async element => {
413
+ const visibleRatio = await new Promise(resolve => {
414
+ const observer = new IntersectionObserver(entries => {
415
+ resolve(entries[0].intersectionRatio);
416
+ observer.disconnect();
417
+ });
418
+ observer.observe(element);
419
+ });
420
+ return visibleRatio > 0;
421
+ }
422
+ JAVASCRIPT
423
+
424
+ evaluate(js)
425
+ end
397
426
 
398
427
  # @param quad [Array<Point>]
399
428
  private def compute_quad_area(quad)
@@ -0,0 +1,23 @@
1
+ class Puppeteer::Env
2
+ # indicates whether DEBUG=1 is specified.
3
+ #
4
+ # @return [Boolean]
5
+ def debug?
6
+ ['1', 'true'].include?(ENV['DEBUG'].to_s)
7
+ end
8
+
9
+ def ci?
10
+ ['1', 'true'].include?(ENV['CI'].to_s)
11
+ end
12
+
13
+ # check if running on macOS
14
+ def darwin?
15
+ RUBY_PLATFORM.include?('darwin')
16
+ end
17
+ end
18
+
19
+ class Puppeteer
20
+ def self.env
21
+ Puppeteer::Env.new
22
+ end
23
+ end
@@ -142,7 +142,7 @@ class Puppeteer::Frame
142
142
  end
143
143
 
144
144
  def child_frames
145
- @child_frames.dup
145
+ @child_frames.to_a
146
146
  end
147
147
 
148
148
  def detached?
@@ -208,28 +208,6 @@ class Puppeteer::Frame
208
208
 
209
209
  define_async_method :async_type_text
210
210
 
211
- # /**
212
- # * @param {(string|number|Function)} selectorOrFunctionOrTimeout
213
- # * @param {!Object=} options
214
- # * @param {!Array<*>} args
215
- # * @return {!Promise<?Puppeteer.JSHandle>}
216
- # */
217
- # waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
218
- # const xPathPattern = '//';
219
-
220
- # if (helper.isString(selectorOrFunctionOrTimeout)) {
221
- # const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
222
- # if (string.startsWith(xPathPattern))
223
- # return this.waitForXPath(string, options);
224
- # return this.waitForSelector(string, options);
225
- # }
226
- # if (helper.isNumber(selectorOrFunctionOrTimeout))
227
- # return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout)));
228
- # if (typeof selectorOrFunctionOrTimeout === 'function')
229
- # return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
230
- # return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
231
- # }
232
-
233
211
  # @param selector [String]
234
212
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
235
213
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
@@ -247,6 +225,11 @@ class Puppeteer::Frame
247
225
 
248
226
  define_async_method :async_wait_for_selector
249
227
 
228
+ # @param milliseconds [Integer] the number of milliseconds to wait.
229
+ def wait_for_timeout(milliseconds)
230
+ sleep(milliseconds / 1000.0)
231
+ end
232
+
250
233
  # @param xpath [String]
251
234
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
252
235
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
@@ -264,12 +247,13 @@ class Puppeteer::Frame
264
247
 
265
248
  define_async_method :async_wait_for_xpath
266
249
 
267
- # @param {Function|string} pageFunction
268
- # @param {!{polling?: string|number, timeout?: number}=} options
269
- # @param {!Array<*>} args
270
- # @return {!Promise<!Puppeteer.JSHandle>}
271
- def wait_for_function(page_function, options = {}, *args)
272
- @main_world.wait_for_function(page_function, options, *args)
250
+ # @param page_function [String]
251
+ # @param args [Integer|Array]
252
+ # @param polling [String]
253
+ # @param timeout [Integer]
254
+ # @return [Puppeteer::JSHandle]
255
+ def wait_for_function(page_function, args: [], polling: nil, timeout: nil)
256
+ @main_world.wait_for_function(page_function, args: args, polling: polling, timeout: timeout)
273
257
  end
274
258
 
275
259
  define_async_method :async_wait_for_function
@@ -282,9 +266,7 @@ class Puppeteer::Frame
282
266
  # @param frame_payload [Hash]
283
267
  def navigated(frame_payload)
284
268
  @name = frame_payload['name']
285
- # TODO(lushnikov): remove this once requestInterception has loaderId exposed.
286
- @navigation_url = frame_payload['url']
287
- @url = frame_payload['url']
269
+ @url = "#{frame_payload['url']}#{frame_payload['urlFragment']}"
288
270
 
289
271
  # Ensure loaderId updated.
290
272
  # The order of [Page.lifecycleEvent name="init"] and [Page.frameNavigated] is random... for some reason...
@@ -1,5 +1,6 @@
1
1
  class Puppeteer::JSHandle
2
2
  using Puppeteer::DefineAsyncMethod
3
+ include Puppeteer::IfPresent
3
4
 
4
5
  # @param context [Puppeteer::ExecutionContext]
5
6
  # @param remote_object [Puppeteer::RemoteObject]
@@ -57,21 +58,29 @@ class Puppeteer::JSHandle
57
58
 
58
59
  define_async_method :async_evaluate_handle
59
60
 
60
- # /**
61
- # * @param {string} propertyName
62
- # * @return {!Promise<?JSHandle>}
63
- # */
64
- # async getProperty(propertyName) {
65
- # const objectHandle = await this.evaluateHandle((object, propertyName) => {
66
- # const result = {__proto__: null};
67
- # result[propertyName] = object[propertyName];
68
- # return result;
69
- # }, propertyName);
70
- # const properties = await objectHandle.getProperties();
71
- # const result = properties.get(propertyName) || null;
72
- # await objectHandle.dispose();
73
- # return result;
74
- # }
61
+ # getProperty(propertyName) in JavaScript
62
+ # @param name [String]
63
+ # @return [Puppeteer::JSHandle]
64
+ def property(name)
65
+ js = <<~JAVASCRIPT
66
+ (object, propertyName) => {
67
+ const result = {__proto__: null};
68
+ result[propertyName] = object[propertyName];
69
+ return result;
70
+ }
71
+ JAVASCRIPT
72
+ object_handle = evaluate_handle(js, name)
73
+ properties = object_handle.properties
74
+ result = properties[name]
75
+ object_handle.dispose
76
+ result
77
+ end
78
+
79
+ # @param name [String]
80
+ # @return [Puppeteer::JSHandle]
81
+ def [](name)
82
+ property(name)
83
+ end
75
84
 
76
85
  # getProperties in JavaScript.
77
86
  # @return [Hash<String, JSHandle>]
@@ -101,7 +110,7 @@ class Puppeteer::JSHandle
101
110
  #
102
111
  # However it would be better that RemoteObject is responsible for
103
112
  # the logic `if (this._remoteObject.objectId) { ... }`.
104
- @remote_object.evaluate_self(@client) || @remote_object.value
113
+ @remote_object.evaluate_self(@client)&.value || @remote_object.value
105
114
  end
106
115
 
107
116
  def as_element
@@ -119,15 +128,16 @@ class Puppeteer::JSHandle
119
128
  @disposed
120
129
  end
121
130
 
122
- # /**
123
- # * @override
124
- # * @return {string}
125
- # */
126
- # toString() {
127
- # if (this._remoteObject.objectId) {
128
- # const type = this._remoteObject.subtype || this._remoteObject.type;
129
- # return 'JSHandle@' + type;
130
- # }
131
- # return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
132
- # }
131
+ def to_s
132
+ # original logic was:
133
+ # if (this._remoteObject.objectId) {
134
+ # const type = this._remoteObject.subtype || this._remoteObject.type;
135
+ # return 'JSHandle@' + type;
136
+ # }
137
+ # return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
138
+ #
139
+ # However it would be better that RemoteObject is responsible for
140
+ # the logic `if (this._remoteObject.objectId) { ... }`.
141
+ if_present(@remote_object.type_str) { |type_str| "JSHandle@#{type_str}" } || "JSHandle:#{@remote_object.value || 'undefined'}"
142
+ end
133
143
  end