puppeteer-ruby 0.0.4 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +30 -0
  3. data/.rubocop.yml +4 -5
  4. data/README.md +3 -2
  5. data/docs/Puppeteer.html +2020 -0
  6. data/docs/Puppeteer/AsyncAwaitBehavior.html +105 -0
  7. data/docs/Puppeteer/Browser.html +2148 -0
  8. data/docs/Puppeteer/BrowserContext.html +809 -0
  9. data/docs/Puppeteer/BrowserFetcher.html +214 -0
  10. data/docs/Puppeteer/BrowserRunner.html +914 -0
  11. data/docs/Puppeteer/BrowserRunner/BrowserProcess.html +477 -0
  12. data/docs/Puppeteer/CDPSession.html +813 -0
  13. data/docs/Puppeteer/CDPSession/Error.html +124 -0
  14. data/docs/Puppeteer/ConcurrentRubyUtils.html +430 -0
  15. data/docs/Puppeteer/Connection.html +960 -0
  16. data/docs/Puppeteer/Connection/MessageCallback.html +434 -0
  17. data/docs/Puppeteer/Connection/ProtocolError.html +216 -0
  18. data/docs/Puppeteer/Connection/RequestDebugPrinter.html +217 -0
  19. data/docs/Puppeteer/Connection/ResponseDebugPrinter.html +244 -0
  20. data/docs/Puppeteer/ConsoleMessage.html +565 -0
  21. data/docs/Puppeteer/ConsoleMessage/Location.html +433 -0
  22. data/docs/Puppeteer/DOMWorld.html +2219 -0
  23. data/docs/Puppeteer/DOMWorld/DetachedError.html +124 -0
  24. data/docs/Puppeteer/DOMWorld/DocumentEvaluationError.html +124 -0
  25. data/docs/Puppeteer/DebugPrint.html +233 -0
  26. data/docs/Puppeteer/Device.html +470 -0
  27. data/docs/Puppeteer/Devices.html +139 -0
  28. data/docs/Puppeteer/ElementHandle.html +2542 -0
  29. data/docs/Puppeteer/ElementHandle/ElementNotFoundError.html +206 -0
  30. data/docs/Puppeteer/ElementHandle/ElementNotVisibleError.html +206 -0
  31. data/docs/Puppeteer/ElementHandle/Point.html +492 -0
  32. data/docs/Puppeteer/ElementHandle/ScrollIntoViewError.html +124 -0
  33. data/docs/Puppeteer/EmulationManager.html +454 -0
  34. data/docs/Puppeteer/EventCallbackable.html +433 -0
  35. data/docs/Puppeteer/EventCallbackable/EventListeners.html +435 -0
  36. data/docs/Puppeteer/ExecutionContext.html +998 -0
  37. data/docs/Puppeteer/ExecutionContext/EvaluationError.html +124 -0
  38. data/docs/Puppeteer/ExecutionContext/JavaScriptExpression.html +357 -0
  39. data/docs/Puppeteer/ExecutionContext/JavaScriptFunction.html +389 -0
  40. data/docs/Puppeteer/FileChooser.html +455 -0
  41. data/docs/Puppeteer/Frame.html +3677 -0
  42. data/docs/Puppeteer/FrameManager.html +2410 -0
  43. data/docs/Puppeteer/FrameManager/NavigationError.html +124 -0
  44. data/docs/Puppeteer/IfPresent.html +222 -0
  45. data/docs/Puppeteer/JSHandle.html +1352 -0
  46. data/docs/Puppeteer/Keyboard.html +1557 -0
  47. data/docs/Puppeteer/Keyboard/KeyDefinition.html +831 -0
  48. data/docs/Puppeteer/Keyboard/KeyDescription.html +603 -0
  49. data/docs/Puppeteer/Launcher.html +237 -0
  50. data/docs/Puppeteer/Launcher/Base.html +385 -0
  51. data/docs/Puppeteer/Launcher/Base/ExecutablePathNotFound.html +124 -0
  52. data/docs/Puppeteer/Launcher/BrowserOptions.html +441 -0
  53. data/docs/Puppeteer/Launcher/Chrome.html +669 -0
  54. data/docs/Puppeteer/Launcher/Chrome/DefaultArgs.html +382 -0
  55. data/docs/Puppeteer/Launcher/ChromeArgOptions.html +531 -0
  56. data/docs/Puppeteer/Launcher/LaunchOptions.html +893 -0
  57. data/docs/Puppeteer/LifecycleWatcher.html +834 -0
  58. data/docs/Puppeteer/LifecycleWatcher/ExpectedLifecycle.html +363 -0
  59. data/docs/Puppeteer/LifecycleWatcher/FrameDetachedError.html +206 -0
  60. data/docs/Puppeteer/LifecycleWatcher/TerminatedError.html +124 -0
  61. data/docs/Puppeteer/Mouse.html +1105 -0
  62. data/docs/Puppeteer/Mouse/Button.html +136 -0
  63. data/docs/Puppeteer/NetworkManager.html +901 -0
  64. data/docs/Puppeteer/NetworkManager/Credentials.html +385 -0
  65. data/docs/Puppeteer/Page.html +5970 -0
  66. data/docs/Puppeteer/Page/FileChooserTimeoutError.html +206 -0
  67. data/docs/Puppeteer/Page/ScreenshotOptions.html +845 -0
  68. data/docs/Puppeteer/Page/ScriptTag.html +555 -0
  69. data/docs/Puppeteer/Page/StyleTag.html +448 -0
  70. data/docs/Puppeteer/Page/TargetCrashedError.html +124 -0
  71. data/docs/Puppeteer/RemoteObject.html +1087 -0
  72. data/docs/Puppeteer/Target.html +1336 -0
  73. data/docs/Puppeteer/Target/InitializeFailure.html +124 -0
  74. data/docs/Puppeteer/Target/TargetInfo.html +729 -0
  75. data/docs/Puppeteer/TimeoutError.html +135 -0
  76. data/docs/Puppeteer/TimeoutSettings.html +496 -0
  77. data/docs/Puppeteer/TouchScreen.html +464 -0
  78. data/docs/Puppeteer/Viewport.html +837 -0
  79. data/docs/Puppeteer/WaitTask.html +637 -0
  80. data/docs/Puppeteer/WaitTask/TerminatedError.html +124 -0
  81. data/docs/Puppeteer/WaitTask/TimeoutError.html +206 -0
  82. data/docs/Puppeteer/WebSocket.html +673 -0
  83. data/docs/Puppeteer/WebSocket/DriverImpl.html +412 -0
  84. data/docs/Puppeteer/WebSocketTransport.html +600 -0
  85. data/docs/Puppeteer/WebSocktTransportError.html +124 -0
  86. data/docs/_index.html +823 -0
  87. data/docs/class_list.html +51 -0
  88. data/docs/css/common.css +1 -0
  89. data/docs/css/full_list.css +58 -0
  90. data/docs/css/style.css +496 -0
  91. data/docs/file.README.html +123 -0
  92. data/docs/file_list.html +56 -0
  93. data/docs/frames.html +17 -0
  94. data/docs/index.html +123 -0
  95. data/docs/js/app.js +314 -0
  96. data/docs/js/full_list.js +216 -0
  97. data/docs/js/jquery.js +4 -0
  98. data/docs/method_list.html +4075 -0
  99. data/docs/top-level-namespace.html +126 -0
  100. data/lib/puppeteer.rb +16 -8
  101. data/lib/puppeteer/async_await_behavior.rb +6 -0
  102. data/lib/puppeteer/browser.rb +25 -6
  103. data/lib/puppeteer/browser_runner.rb +1 -1
  104. data/lib/puppeteer/cdp_session.rb +33 -11
  105. data/lib/puppeteer/connection.rb +1 -1
  106. data/lib/puppeteer/dom_world.rb +121 -104
  107. data/lib/puppeteer/element_handle.rb +186 -224
  108. data/lib/puppeteer/element_handle/bounding_box.rb +12 -0
  109. data/lib/puppeteer/element_handle/box_model.rb +19 -0
  110. data/lib/puppeteer/element_handle/point.rb +26 -0
  111. data/lib/puppeteer/errors.rb +1 -3
  112. data/lib/puppeteer/execution_context.rb +36 -17
  113. data/lib/puppeteer/file_chooser.rb +29 -0
  114. data/lib/puppeteer/frame.rb +17 -11
  115. data/lib/puppeteer/frame_manager.rb +1 -3
  116. data/lib/puppeteer/js_handle.rb +3 -2
  117. data/lib/puppeteer/launcher.rb +0 -1
  118. data/lib/puppeteer/launcher/chrome.rb +48 -2
  119. data/lib/puppeteer/lifecycle_watcher.rb +3 -3
  120. data/lib/puppeteer/page.rb +121 -68
  121. data/lib/puppeteer/remote_object.rb +15 -1
  122. data/lib/puppeteer/target.rb +24 -24
  123. data/lib/puppeteer/version.rb +1 -1
  124. data/lib/puppeteer/viewport.rb +18 -0
  125. data/lib/puppeteer/wait_task.rb +183 -1
  126. data/lib/puppeteer/web_socket.rb +3 -1
  127. data/lib/puppeteer/web_socket_transport.rb +1 -1
  128. data/puppeteer-ruby.gemspec +4 -1
  129. metadata +145 -4
@@ -1,3 +1,7 @@
1
+ require_relative './element_handle/bounding_box'
2
+ require_relative './element_handle/box_model'
3
+ require_relative './element_handle/point'
4
+
1
5
  class Puppeteer::ElementHandle < Puppeteer::JSHandle
2
6
  include Puppeteer::IfPresent
3
7
  using Puppeteer::AsyncAwaitBehavior
@@ -37,20 +41,8 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
37
41
  return 'Node is detached from document';
38
42
  if (element.nodeType !== Node.ELEMENT_NODE)
39
43
  return 'Node is not of type HTMLElement';
40
- // force-scroll if page's javascript is disabled.
41
- if (!pageJavascriptEnabled) {
42
- element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
43
- return false;
44
- }
45
- const visibleRatio = await new Promise(resolve => {
46
- const observer = new IntersectionObserver(entries => {
47
- resolve(entries[0].intersectionRatio);
48
- observer.disconnect();
49
- });
50
- observer.observe(element);
51
- });
52
- if (visibleRatio !== 1.0)
53
- element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
44
+
45
+ element.scrollIntoViewIfNeeded({block: 'center', inline: 'center', behavior: 'instant'});
54
46
  return false;
55
47
  }
56
48
  JAVASCRIPT
@@ -58,29 +50,9 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
58
50
  if error
59
51
  raise ScrollIntoViewError.new(error)
60
52
  end
61
- end
62
-
63
- class Point
64
- def initialize(x:, y:)
65
- @x = x
66
- @y = y
67
- end
68
-
69
- def +(other)
70
- Point.new(
71
- x: @x + other.x,
72
- y: @y + other.y,
73
- )
74
- end
75
-
76
- def /(num)
77
- Point.new(
78
- x: @x / num,
79
- y: @y / num,
80
- )
81
- end
82
-
83
- attr_reader :x, :y
53
+ # clickpoint is often calculated before scrolling is completed.
54
+ # So, just sleep about 10 frames
55
+ sleep 0.16
84
56
  end
85
57
 
86
58
  class ElementNotVisibleError < StandardError
@@ -112,15 +84,6 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
112
84
  quads.first.reduce(:+) / 4
113
85
  end
114
86
 
115
- # /**
116
- # * @return {!Promise<void|Protocol.DOM.getBoxModelReturnValue>}
117
- # */
118
- # _getBoxModel() {
119
- # return this._client.send('DOM.getBoxModel', {
120
- # objectId: this._remoteObject.objectId
121
- # }).catch(error => debugError(error));
122
- # }
123
-
124
87
  # @param quad [Array<number>]
125
88
  # @return [Array<Point>]
126
89
  private def from_protocol_quad(quad)
@@ -157,83 +120,84 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
157
120
  @page.mouse.click(point.x, point.y, delay: delay, button: button, click_count: click_count)
158
121
  end
159
122
 
160
- # /**
161
- # * @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
162
- # */
163
- # async click(options) {
164
- # await this._scrollIntoViewIfNeeded();
165
- # const {x, y} = await this._clickablePoint();
166
- # await this._page.mouse.click(x, y, options);
167
- # }
123
+ # @param delay [Number]
124
+ # @param button [String] "left"|"right"|"middle"
125
+ # @param click_count [Number]
126
+ async def async_click(delay: nil, button: nil, click_count: nil)
127
+ click(delay: delay, button: button, click_count: click_count)
128
+ end
168
129
 
169
- # /**
170
- # * @param {!Array<string>} values
171
- # * @return {!Promise<!Array<string>>}
172
- # */
173
- # async select(...values) {
174
- # for (const value of values)
175
- # assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
176
- # return this.evaluate((element, values) => {
177
- # if (element.nodeName.toLowerCase() !== 'select')
178
- # throw new Error('Element is not a <select> element.');
179
-
180
- # const options = Array.from(element.options);
181
- # element.value = undefined;
182
- # for (const option of options) {
183
- # option.selected = values.includes(option.value);
184
- # if (option.selected && !element.multiple)
185
- # break;
186
- # }
187
- # element.dispatchEvent(new Event('input', { bubbles: true }));
188
- # element.dispatchEvent(new Event('change', { bubbles: true }));
189
- # return options.filter(option => option.selected).map(option => option.value);
190
- # }, values);
191
- # }
130
+ # @return [Array<String>]
131
+ def select(*values)
132
+ if nonstring = values.find { |value| !value.is_a?(String) }
133
+ raise ArgumentError.new("Values must be strings. Found value \"#{nonstring}\" of type \"#{nonstring.class}\"")
134
+ end
192
135
 
193
- # /**
194
- # * @param {!Array<string>} filePaths
195
- # */
196
- # async uploadFile(...filePaths) {
197
- # const isMultiple = await this.evaluate(element => element.multiple);
198
- # assert(filePaths.length <= 1 || isMultiple, 'Multiple file uploads only work with <input type=file multiple>');
199
- # // These imports are only needed for `uploadFile`, so keep them
200
- # // scoped here to avoid paying the cost unnecessarily.
201
- # const path = require('path');
202
- # const mime = require('mime-types');
203
- # const fs = require('fs');
204
- # const readFileAsync = helper.promisify(fs.readFile);
205
-
206
- # const promises = filePaths.map(filePath => readFileAsync(filePath));
207
- # const files = [];
208
- # for (let i = 0; i < filePaths.length; i++) {
209
- # const buffer = await promises[i];
210
- # const filePath = path.basename(filePaths[i]);
211
- # const file = {
212
- # name: filePath,
213
- # content: buffer.toString('base64'),
214
- # mimeType: mime.lookup(filePath),
215
- # };
216
- # files.push(file);
217
- # }
218
- # await this.evaluateHandle(async(element, files) => {
219
- # const dt = new DataTransfer();
220
- # for (const item of files) {
221
- # const response = await fetch(`data:${item.mimeType};base64,${item.content}`);
222
- # const file = new File([await response.blob()], item.name);
223
- # dt.items.add(file);
224
- # }
225
- # element.files = dt.files;
226
- # element.dispatchEvent(new Event('input', { bubbles: true }));
227
- # element.dispatchEvent(new Event('change', { bubbles: true }));
228
- # }, files);
229
- # }
136
+ fn = <<~JAVASCRIPT
137
+ (element, values) => {
138
+ if (element.nodeName.toLowerCase() !== 'select') {
139
+ throw new Error('Element is not a <select> element.');
140
+ }
230
141
 
231
- # async tap() {
232
- # await this._scrollIntoViewIfNeeded();
233
- # const {x, y} = await this._clickablePoint();
234
- # await this._page.touchscreen.tap(x, y);
235
- # }
142
+ const options = Array.from(element.options);
143
+ element.value = undefined;
144
+ for (const option of options) {
145
+ option.selected = values.includes(option.value);
146
+ if (option.selected && !element.multiple) {
147
+ break;
148
+ }
149
+ }
150
+ element.dispatchEvent(new Event('input', { bubbles: true }));
151
+ element.dispatchEvent(new Event('change', { bubbles: true }));
152
+ return options.filter(option => option.selected).map(option => option.value);
153
+ }
154
+ JAVASCRIPT
155
+ evaluate(fn, values)
156
+ end
157
+
158
+ # @param file_paths [Array<String>]
159
+ def upload_file(*file_paths)
160
+ is_multiple = evaluate("el => el.multiple")
161
+ if !is_multiple && file_paths.length >= 2
162
+ raise ArgumentError.new('Multiple file uploads only work with <input type=file multiple>')
163
+ end
164
+
165
+ if error_path = file_paths.find { |file_path| !File.exist?(file_path) }
166
+ raise ArgmentError.new("#{error_path} does not exist or is not readable")
167
+ end
168
+
169
+ backend_node_id = @remote_object.node_info(@client)["node"]["backendNodeId"]
170
+
171
+ # The zero-length array is a special case, it seems that DOM.setFileInputFiles does
172
+ # not actually update the files in that case, so the solution is to eval the element
173
+ # value to a new FileList directly.
174
+ if file_paths.empty?
175
+ fn = <<~JAVASCRIPT
176
+ (element) => {
177
+ element.files = new DataTransfer().files;
178
+
179
+ // Dispatch events for this case because it should behave akin to a user action.
180
+ element.dispatchEvent(new Event('input', { bubbles: true }));
181
+ element.dispatchEvent(new Event('change', { bubbles: true }));
182
+ }
183
+ JAVASCRIPT
184
+ await this.evaluate(fn)
185
+ else
186
+ @remote_object.set_file_input_files(@client, file_paths, backend_node_id)
187
+ end
188
+ end
189
+
190
+ def tap(&block)
191
+ return super(&block) if block
192
+
193
+ scroll_into_view_if_needed
194
+ point = clickable_point
195
+ @page.touchscreen.tap(point.x, point.y)
196
+ end
236
197
 
198
+ async def async_tap
199
+ tap
200
+ end
237
201
 
238
202
  def focus
239
203
  evaluate('element => element.focus()')
@@ -271,89 +235,77 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
271
235
  press(key, delay: delay)
272
236
  end
273
237
 
274
- # /**
275
- # * @return {!Promise<?{x: number, y: number, width: number, height: number}>}
276
- # */
277
- # async boundingBox() {
278
- # const result = await this._getBoxModel();
279
-
280
- # if (!result)
281
- # return null;
282
-
283
- # const quad = result.model.border;
284
- # const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
285
- # const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
286
- # const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
287
- # const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
288
-
289
- # return {x, y, width, height};
290
- # }
291
-
292
- # /**
293
- # * @return {!Promise<?BoxModel>}
294
- # */
295
- # async boxModel() {
296
- # const result = await this._getBoxModel();
297
-
298
- # if (!result)
299
- # return null;
300
-
301
- # const {content, padding, border, margin, width, height} = result.model;
302
- # return {
303
- # content: this._fromProtocolQuad(content),
304
- # padding: this._fromProtocolQuad(padding),
305
- # border: this._fromProtocolQuad(border),
306
- # margin: this._fromProtocolQuad(margin),
307
- # width,
308
- # height
309
- # };
310
- # }
311
-
312
- # /**
313
- # *
314
- # * @param {!Object=} options
315
- # * @returns {!Promise<string|!Buffer>}
316
- # */
317
- # async screenshot(options = {}) {
318
- # let needsViewportReset = false;
319
-
320
- # let boundingBox = await this.boundingBox();
321
- # assert(boundingBox, 'Node is either not visible or not an HTMLElement');
322
-
323
- # const viewport = this._page.viewport();
324
-
325
- # if (viewport && (boundingBox.width > viewport.width || boundingBox.height > viewport.height)) {
326
- # const newViewport = {
327
- # width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
328
- # height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
329
- # };
330
- # await this._page.setViewport(Object.assign({}, viewport, newViewport));
331
-
332
- # needsViewportReset = true;
333
- # }
238
+ # @return [BoundingBox|nil]
239
+ def bounding_box
240
+ if_present(box_model) do |result_model|
241
+ quads = result_model.border
242
+
243
+ x = quads.map(&:x).min
244
+ y = quads.map(&:y).min
245
+ BoundingBox.new(
246
+ x: x,
247
+ y: y,
248
+ width: quads.map(&:x).max - x,
249
+ height: quads.map(&:y).max - y,
250
+ )
251
+ end
252
+ end
334
253
 
335
- # await this._scrollIntoViewIfNeeded();
254
+ # @return [BoxModel|nil]
255
+ def box_model
256
+ if_present(@remote_object.box_model(@client)) do |result|
257
+ BoxModel.new(result['model'])
258
+ end
259
+ end
336
260
 
337
- # boundingBox = await this.boundingBox();
338
- # assert(boundingBox, 'Node is either not visible or not an HTMLElement');
339
- # assert(boundingBox.width !== 0, 'Node has 0 width.');
340
- # assert(boundingBox.height !== 0, 'Node has 0 height.');
261
+ def screenshot(options = {})
262
+ needs_viewport_reset = false
341
263
 
342
- # const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics');
264
+ box = bounding_box
265
+ unless box
266
+ raise ElementNotVisibleError.new
267
+ end
343
268
 
344
- # const clip = Object.assign({}, boundingBox);
345
- # clip.x += pageX;
346
- # clip.y += pageY;
269
+ viewport = @page.viewport
270
+ if viewport && (box.width > viewport.width || box.height > viewport.height)
271
+ new_viewport = viewport.merge(
272
+ width: [viewport.width, box.width.to_i].min,
273
+ height: [viewport.height, box.height.to_i].min,
274
+ )
275
+ @page.viewport = new_viewport
347
276
 
348
- # const imageData = await this._page.screenshot(Object.assign({}, {
349
- # clip
350
- # }, options));
277
+ needs_viewport_reset = true
278
+ end
279
+ scroll_into_view_if_needed
351
280
 
352
- # if (needsViewportReset)
353
- # await this._page.setViewport(viewport);
281
+ box = bounding_box
282
+ unless box
283
+ raise ElementNotVisibleError.new
284
+ end
285
+ if box.width == 0
286
+ raise 'Node has 0 width.'
287
+ end
288
+ if box.height == 0
289
+ raise 'Node has 0 height.'
290
+ end
354
291
 
355
- # return imageData;
356
- # }
292
+ layout_metrics = @client.send_message('Page.getLayoutMetrics')
293
+ page_x = layout_metrics["layoutViewport"]["pageX"]
294
+ page_y = layout_metrics["layoutViewport"]["pageY"]
295
+
296
+ clip = {
297
+ x: page_x + box.x,
298
+ y: page_y + box.y,
299
+ width: box.width,
300
+ height: box.height,
301
+ }
302
+
303
+ @page.screenshot({ clip: clip }.merge(options))
304
+ ensure
305
+ if needs_viewport_reset
306
+ @page.viewport = viewport
307
+ end
308
+ end
357
309
 
358
310
  # `$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
359
311
  # @param selector [String]
@@ -404,47 +356,57 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
404
356
  result
405
357
  end
406
358
 
359
+ # `$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
360
+ # @param selector [String]
361
+ # @param page_function [String]
362
+ # @return [Object]
363
+ async def async_Seval(selector, page_function, *args)
364
+ Seval(selector, page_function, *args)
365
+ end
366
+
407
367
  # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
408
368
  # @param selector [String]
409
369
  # @param page_function [String]
410
370
  # @return [Object]
411
371
  def SSeval(selector, page_function, *args)
412
372
  handles = evaluate_handle(
413
- '(element, selector) => Array.from(element.querySelectorAll(selector))',
414
- selector)
373
+ '(element, selector) => Array.from(element.querySelectorAll(selector))',
374
+ selector,
375
+ )
415
376
  result = handles.evaluate(page_function, *args)
416
377
  handles.dispose
417
378
 
418
379
  result
419
380
  end
420
381
 
421
- # /**
422
- # * @param {string} expression
423
- # * @return {!Promise<!Array<!ElementHandle>>}
424
- # */
425
- # async $x(expression) {
426
- # const arrayHandle = await this.evaluateHandle(
427
- # (element, expression) => {
428
- # const document = element.ownerDocument || element;
429
- # const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
430
- # const array = [];
431
- # let item;
432
- # while ((item = iterator.iterateNext()))
433
- # array.push(item);
434
- # return array;
435
- # },
436
- # expression
437
- # );
438
- # const properties = await arrayHandle.getProperties();
439
- # await arrayHandle.dispose();
440
- # const result = [];
441
- # for (const property of properties.values()) {
442
- # const elementHandle = property.asElement();
443
- # if (elementHandle)
444
- # result.push(elementHandle);
445
- # }
446
- # return result;
447
- # }
382
+ # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
383
+ # @param selector [String]
384
+ # @param page_function [String]
385
+ # @return [Object]
386
+ async def async_SSeval(selector, page_function, *args)
387
+ SSeval(selector, page_function, *args)
388
+ end
389
+
390
+ # `$x()` in JavaScript. $ is not allowed to use as a method name in Ruby.
391
+ # @param expression [String]
392
+ # @return [Array<ElementHandle>]
393
+ def Sx(expression)
394
+ fn = <<~JAVASCRIPT
395
+ (element, expression) => {
396
+ const document = element.ownerDocument || element;
397
+ const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
398
+ const array = [];
399
+ let item;
400
+ while ((item = iterator.iterateNext()))
401
+ array.push(item);
402
+ return array;
403
+ }
404
+ JAVASCRIPT
405
+ handles = evaluate_handle(fn, expression)
406
+ properties = handles.properties
407
+ handles.dispose
408
+ properties.values.map(&:as_element).compact
409
+ end
448
410
 
449
411
  # /**
450
412
  # * @returns {!Promise<boolean>}