puppeteer-ruby 0.0.15 → 0.0.20

Sign up to get free protection for your applications and to get access to all the features.
@@ -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