puppeteer-ruby 0.0.16 → 0.0.21

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.
@@ -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?
@@ -225,6 +225,11 @@ class Puppeteer::Frame
225
225
 
226
226
  define_async_method :async_wait_for_selector
227
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
+
228
233
  # @param xpath [String]
229
234
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
230
235
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
@@ -242,12 +247,13 @@ class Puppeteer::Frame
242
247
 
243
248
  define_async_method :async_wait_for_xpath
244
249
 
245
- # @param {Function|string} pageFunction
246
- # @param {!{polling?: string|number, timeout?: number}=} options
247
- # @param {!Array<*>} args
248
- # @return {!Promise<!Puppeteer.JSHandle>}
249
- def wait_for_function(page_function, options = {}, *args)
250
- @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)
251
257
  end
252
258
 
253
259
  define_async_method :async_wait_for_function
@@ -260,9 +266,7 @@ class Puppeteer::Frame
260
266
  # @param frame_payload [Hash]
261
267
  def navigated(frame_payload)
262
268
  @name = frame_payload['name']
263
- # TODO(lushnikov): remove this once requestInterception has loaderId exposed.
264
- @navigation_url = frame_payload['url']
265
- @url = frame_payload['url']
269
+ @url = "#{frame_payload['url']}#{frame_payload['urlFragment']}"
266
270
 
267
271
  # Ensure loaderId updated.
268
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
@@ -149,9 +149,10 @@ class Puppeteer::Keyboard
149
149
  define_async_method :async_type_text
150
150
 
151
151
  # @param key [String]
152
+ # @param text [String]
152
153
  # @return [Future]
153
- def press(key, delay: nil)
154
- down(key)
154
+ def press(key, delay: nil, text: nil)
155
+ down(key, text: text)
155
156
  if delay
156
157
  sleep(delay.to_i / 1000.0)
157
158
  end
@@ -89,7 +89,7 @@ class Puppeteer::Keyboard
89
89
  'Digit6': KeyDefinition.new({ 'keyCode': 54, 'code': 'Digit6', 'shiftKey': '^', 'key': '6' }),
90
90
  'Digit7': KeyDefinition.new({ 'keyCode': 55, 'code': 'Digit7', 'shiftKey': '&', 'key': '7' }),
91
91
  'Digit8': KeyDefinition.new({ 'keyCode': 56, 'code': 'Digit8', 'shiftKey': '*', 'key': '8' }),
92
- 'Digit9': KeyDefinition.new({ 'keyCode': 57, 'code': 'Digit9', 'shiftKey': '\(', 'key': '9' }),
92
+ 'Digit9': KeyDefinition.new({ 'keyCode': 57, 'code': 'Digit9', 'shiftKey': '(', 'key': '9' }),
93
93
  'KeyA': KeyDefinition.new({ 'keyCode': 65, 'code': 'KeyA', 'shiftKey': 'A', 'key': 'a' }),
94
94
  'KeyB': KeyDefinition.new({ 'keyCode': 66, 'code': 'KeyB', 'shiftKey': 'B', 'key': 'b' }),
95
95
  'KeyC': KeyDefinition.new({ 'keyCode': 67, 'code': 'KeyC', 'shiftKey': 'C', 'key': 'c' }),
@@ -235,7 +235,7 @@ class Puppeteer::Keyboard
235
235
  '%': KeyDefinition.new({ 'keyCode': 53, 'key': '%', 'code': 'Digit5' }),
236
236
  '^': KeyDefinition.new({ 'keyCode': 54, 'key': '^', 'code': 'Digit6' }),
237
237
  '&': KeyDefinition.new({ 'keyCode': 55, 'key': '&', 'code': 'Digit7' }),
238
- '(': KeyDefinition.new({ 'keyCode': 57, 'key': '\(', 'code': 'Digit9' }),
238
+ '(': KeyDefinition.new({ 'keyCode': 57, 'key': '(', 'code': 'Digit9' }),
239
239
  'A': KeyDefinition.new({ 'keyCode': 65, 'key': 'A', 'code': 'KeyA' }),
240
240
  'B': KeyDefinition.new({ 'keyCode': 66, 'key': 'B', 'code': 'KeyB' }),
241
241
  'C': KeyDefinition.new({ 'keyCode': 67, 'key': 'C', 'code': 'KeyC' }),
@@ -2,6 +2,7 @@ require_relative './launcher/base'
2
2
  require_relative './launcher/browser_options'
3
3
  require_relative './launcher/chrome'
4
4
  require_relative './launcher/chrome_arg_options'
5
+ require_relative './launcher/firefox'
5
6
  require_relative './launcher/launch_options'
6
7
 
7
8
  # https://github.com/puppeteer/puppeteer/blob/main/src/node/Launcher.ts
@@ -9,10 +10,19 @@ module Puppeteer::Launcher
9
10
  # @param project_root [String]
10
11
  # @param prefereed_revision [String]
11
12
  # @param is_puppeteer_core [String]
12
- # @param product [String] 'chrome' or 'firefox' (not implemented yet)
13
+ # @param product [String] 'chrome' or 'firefox'
13
14
  # @return [Puppeteer::Launcher::Chrome]
14
15
  module_function def new(project_root:, preferred_revision:, is_puppeteer_core:, product:)
16
+ unless is_puppeteer_core
17
+ product ||= ENV['PUPPETEER_PRODUCT']
18
+ end
19
+
15
20
  if product == 'firefox'
21
+ return Firefox.new(
22
+ project_root: project_root,
23
+ preferred_revision: preferred_revision,
24
+ is_puppeteer_core: is_puppeteer_core,
25
+ )
16
26
  raise NotImplementedError.new('FirefoxLauncher is not implemented yet.')
17
27
  end
18
28