puppeteer-ruby 0.0.3 → 0.0.8

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.
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
@@ -1,3 +1,5 @@
1
+ require 'mime/types'
2
+
1
3
  class Puppeteer::ElementHandle < Puppeteer::JSHandle
2
4
  include Puppeteer::IfPresent
3
5
  using Puppeteer::AsyncAwaitBehavior
@@ -28,62 +30,80 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
28
30
  end
29
31
  end
30
32
 
31
- # async _scrollIntoViewIfNeeded() {
32
- # const error = await this.evaluate(async(element, pageJavascriptEnabled) => {
33
- # if (!element.isConnected)
34
- # return 'Node is detached from document';
35
- # if (element.nodeType !== Node.ELEMENT_NODE)
36
- # return 'Node is not of type HTMLElement';
37
- # // force-scroll if page's javascript is disabled.
38
- # if (!pageJavascriptEnabled) {
39
- # element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
40
- # return false;
41
- # }
42
- # const visibleRatio = await new Promise(resolve => {
43
- # const observer = new IntersectionObserver(entries => {
44
- # resolve(entries[0].intersectionRatio);
45
- # observer.disconnect();
46
- # });
47
- # observer.observe(element);
48
- # });
49
- # if (visibleRatio !== 1.0)
50
- # element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
51
- # return false;
52
- # }, this._page._javascriptEnabled);
53
- # if (error)
54
- # throw new Error(error);
55
- # }
33
+ class ScrollIntoViewError < StandardError; end
34
+
35
+ def scroll_into_view_if_needed
36
+ js = <<~JAVASCRIPT
37
+ async(element, pageJavascriptEnabled) => {
38
+ if (!element.isConnected)
39
+ return 'Node is detached from document';
40
+ if (element.nodeType !== Node.ELEMENT_NODE)
41
+ return 'Node is not of type HTMLElement';
42
+
43
+ element.scrollIntoViewIfNeeded({block: 'center', inline: 'center', behavior: 'instant'});
44
+ return false;
45
+ }
46
+ JAVASCRIPT
47
+ error = evaluate(js, @page.javascript_enabled) # returns String or false
48
+ if error
49
+ raise ScrollIntoViewError.new(error)
50
+ end
51
+ # clickpoint is often calculated before scrolling is completed.
52
+ # So, just sleep about 10 frames
53
+ sleep 0.16
54
+ end
56
55
 
57
- # /**
58
- # * @return {!Promise<!{x: number, y: number}>}
59
- # */
60
- # async _clickablePoint() {
61
- # const [result, layoutMetrics] = await Promise.all([
62
- # this._client.send('DOM.getContentQuads', {
63
- # objectId: this._remoteObject.objectId
64
- # }).catch(debugError),
65
- # this._client.send('Page.getLayoutMetrics'),
66
- # ]);
67
- # if (!result || !result.quads.length)
68
- # throw new Error('Node is either not visible or not an HTMLElement');
69
- # // Filter out quads that have too small area to click into.
70
- # const {clientWidth, clientHeight} = layoutMetrics.layoutViewport;
71
- # const quads = result.quads.map(quad => this._fromProtocolQuad(quad)).map(quad => this._intersectQuadWithViewport(quad, clientWidth, clientHeight)).filter(quad => computeQuadArea(quad) > 1);
72
- # if (!quads.length)
73
- # throw new Error('Node is either not visible or not an HTMLElement');
74
- # // Return the middle point of the first quad.
75
- # const quad = quads[0];
76
- # let x = 0;
77
- # let y = 0;
78
- # for (const point of quad) {
79
- # x += point.x;
80
- # y += point.y;
81
- # }
82
- # return {
83
- # x: x / 4,
84
- # y: y / 4
85
- # };
86
- # }
56
+ class Point
57
+ def initialize(x:, y:)
58
+ @x = x
59
+ @y = y
60
+ end
61
+
62
+ def +(other)
63
+ Point.new(
64
+ x: @x + other.x,
65
+ y: @y + other.y,
66
+ )
67
+ end
68
+
69
+ def /(num)
70
+ Point.new(
71
+ x: @x / num,
72
+ y: @y / num,
73
+ )
74
+ end
75
+
76
+ attr_reader :x, :y
77
+ end
78
+
79
+ class ElementNotVisibleError < StandardError
80
+ def initialize
81
+ super("Node is either not visible or not an HTMLElement")
82
+ end
83
+ end
84
+
85
+ def clickable_point
86
+ result = @remote_object.content_quads(@client)
87
+ if !result || result["quads"].empty?
88
+ raise ElementNotVisibleError.new
89
+ end
90
+
91
+ # Filter out quads that have too small area to click into.
92
+ layout_metrics = @client.send_message('Page.getLayoutMetrics')
93
+ client_width = layout_metrics["layoutViewport"]["clientWidth"]
94
+ client_height = layout_metrics["layoutViewport"]["clientHeight"]
95
+
96
+ quads = result["quads"].
97
+ map { |quad| from_protocol_quad(quad) }.
98
+ map { |quad| intersect_quad_with_viewport(quad, client_width, client_height) }.
99
+ select { |quad| compute_quad_area(quad) > 1 }
100
+ if quads.empty?
101
+ raise ElementNotVisibleError.new
102
+ end
103
+
104
+ # Return the middle point of the first quad.
105
+ quads.first.reduce(:+) / 4
106
+ end
87
107
 
88
108
  # /**
89
109
  # * @return {!Promise<void|Protocol.DOM.getBoxModelReturnValue>}
@@ -94,31 +114,26 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
94
114
  # }).catch(error => debugError(error));
95
115
  # }
96
116
 
97
- # /**
98
- # * @param {!Array<number>} quad
99
- # * @return {!Array<{x: number, y: number}>}
100
- # */
101
- # _fromProtocolQuad(quad) {
102
- # return [
103
- # {x: quad[0], y: quad[1]},
104
- # {x: quad[2], y: quad[3]},
105
- # {x: quad[4], y: quad[5]},
106
- # {x: quad[6], y: quad[7]}
107
- # ];
108
- # }
117
+ # @param quad [Array<number>]
118
+ # @return [Array<Point>]
119
+ private def from_protocol_quad(quad)
120
+ quad.each_slice(2).map do |x, y|
121
+ Point.new(x: x, y: y)
122
+ end
123
+ end
109
124
 
110
- # /**
111
- # * @param {!Array<{x: number, y: number}>} quad
112
- # * @param {number} width
113
- # * @param {number} height
114
- # * @return {!Array<{x: number, y: number}>}
115
- # */
116
- # _intersectQuadWithViewport(quad, width, height) {
117
- # return quad.map(point => ({
118
- # x: Math.min(Math.max(point.x, 0), width),
119
- # y: Math.min(Math.max(point.y, 0), height),
120
- # }));
121
- # }
125
+ # @param quad [Array<Point>]
126
+ # @param width [number]
127
+ # @param height [number]
128
+ # @return [Array<Point>]
129
+ private def intersect_quad_with_viewport(quad, width, height)
130
+ quad.map do |point|
131
+ Point.new(
132
+ x: [[point.x, 0].max, width].min,
133
+ y: [[point.y, 0].max, height].min,
134
+ )
135
+ end
136
+ end
122
137
 
123
138
  # async hover() {
124
139
  # await this._scrollIntoViewIfNeeded();
@@ -126,83 +141,93 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
126
141
  # await this._page.mouse.move(x, y);
127
142
  # }
128
143
 
129
- # /**
130
- # * @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
131
- # */
132
- # async click(options) {
133
- # await this._scrollIntoViewIfNeeded();
134
- # const {x, y} = await this._clickablePoint();
135
- # await this._page.mouse.click(x, y, options);
136
- # }
144
+ # @param delay [Number]
145
+ # @param button [String] "left"|"right"|"middle"
146
+ # @param click_count [Number]
147
+ def click(delay: nil, button: nil, click_count: nil)
148
+ scroll_into_view_if_needed
149
+ point = clickable_point
150
+ @page.mouse.click(point.x, point.y, delay: delay, button: button, click_count: click_count)
151
+ end
137
152
 
138
- # /**
139
- # * @param {!Array<string>} values
140
- # * @return {!Promise<!Array<string>>}
141
- # */
142
- # async select(...values) {
143
- # for (const value of values)
144
- # assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
145
- # return this.evaluate((element, values) => {
146
- # if (element.nodeName.toLowerCase() !== 'select')
147
- # throw new Error('Element is not a <select> element.');
148
-
149
- # const options = Array.from(element.options);
150
- # element.value = undefined;
151
- # for (const option of options) {
152
- # option.selected = values.includes(option.value);
153
- # if (option.selected && !element.multiple)
154
- # break;
155
- # }
156
- # element.dispatchEvent(new Event('input', { bubbles: true }));
157
- # element.dispatchEvent(new Event('change', { bubbles: true }));
158
- # return options.filter(option => option.selected).map(option => option.value);
159
- # }, values);
160
- # }
153
+ # @param delay [Number]
154
+ # @param button [String] "left"|"right"|"middle"
155
+ # @param click_count [Number]
156
+ async def async_click(delay: nil, button: nil, click_count: nil)
157
+ click(delay: delay, button: button, click_count: click_count)
158
+ end
161
159
 
162
- # /**
163
- # * @param {!Array<string>} filePaths
164
- # */
165
- # async uploadFile(...filePaths) {
166
- # const isMultiple = await this.evaluate(element => element.multiple);
167
- # assert(filePaths.length <= 1 || isMultiple, 'Multiple file uploads only work with <input type=file multiple>');
168
- # // These imports are only needed for `uploadFile`, so keep them
169
- # // scoped here to avoid paying the cost unnecessarily.
170
- # const path = require('path');
171
- # const mime = require('mime-types');
172
- # const fs = require('fs');
173
- # const readFileAsync = helper.promisify(fs.readFile);
174
-
175
- # const promises = filePaths.map(filePath => readFileAsync(filePath));
176
- # const files = [];
177
- # for (let i = 0; i < filePaths.length; i++) {
178
- # const buffer = await promises[i];
179
- # const filePath = path.basename(filePaths[i]);
180
- # const file = {
181
- # name: filePath,
182
- # content: buffer.toString('base64'),
183
- # mimeType: mime.lookup(filePath),
184
- # };
185
- # files.push(file);
186
- # }
187
- # await this.evaluateHandle(async(element, files) => {
188
- # const dt = new DataTransfer();
189
- # for (const item of files) {
190
- # const response = await fetch(`data:${item.mimeType};base64,${item.content}`);
191
- # const file = new File([await response.blob()], item.name);
192
- # dt.items.add(file);
193
- # }
194
- # element.files = dt.files;
195
- # element.dispatchEvent(new Event('input', { bubbles: true }));
196
- # element.dispatchEvent(new Event('change', { bubbles: true }));
197
- # }, files);
198
- # }
160
+ # @return [Array<String>]
161
+ def select(*values)
162
+ if nonstring = values.find { |value| !value.is_a?(String) }
163
+ raise ArgumentError.new("Values must be strings. Found value \"#{nonstring}\" of type \"#{nonstring.class}\"")
164
+ end
199
165
 
200
- # async tap() {
201
- # await this._scrollIntoViewIfNeeded();
202
- # const {x, y} = await this._clickablePoint();
203
- # await this._page.touchscreen.tap(x, y);
204
- # }
166
+ fn = <<~JAVASCRIPT
167
+ (element, values) => {
168
+ if (element.nodeName.toLowerCase() !== 'select') {
169
+ throw new Error('Element is not a <select> element.');
170
+ }
171
+
172
+ const options = Array.from(element.options);
173
+ element.value = undefined;
174
+ for (const option of options) {
175
+ option.selected = values.includes(option.value);
176
+ if (option.selected && !element.multiple) {
177
+ break;
178
+ }
179
+ }
180
+ element.dispatchEvent(new Event('input', { bubbles: true }));
181
+ element.dispatchEvent(new Event('change', { bubbles: true }));
182
+ return options.filter(option => option.selected).map(option => option.value);
183
+ }
184
+ JAVASCRIPT
185
+ evaluate(fn, values)
186
+ end
205
187
 
188
+ # @param file_paths [Array<String>]
189
+ def upload_file(*file_paths)
190
+ is_multiple = evaluate("el => el.multiple")
191
+ if !is_multiple && file_paths.length >= 2
192
+ raise ArgumentError.new('Multiple file uploads only work with <input type=file multiple>')
193
+ end
194
+
195
+ if error_path = file_paths.find { |file_path| !File.exist?(file_path) }
196
+ raise ArgmentError.new("#{error_path} does not exist or is not readable")
197
+ end
198
+
199
+ backend_node_id = @remote_object.node_info(@client)["node"]["backendNodeId"]
200
+
201
+ # The zero-length array is a special case, it seems that DOM.setFileInputFiles does
202
+ # not actually update the files in that case, so the solution is to eval the element
203
+ # value to a new FileList directly.
204
+ if file_paths.empty?
205
+ fn = <<~JAVASCRIPT
206
+ (element) => {
207
+ element.files = new DataTransfer().files;
208
+
209
+ // Dispatch events for this case because it should behave akin to a user action.
210
+ element.dispatchEvent(new Event('input', { bubbles: true }));
211
+ element.dispatchEvent(new Event('change', { bubbles: true }));
212
+ }
213
+ JAVASCRIPT
214
+ await this.evaluate(fn)
215
+ else
216
+ @remote_object.set_file_input_files(@client, file_paths, backend_node_id)
217
+ end
218
+ end
219
+
220
+ def tap(&block)
221
+ return super(&block) if block
222
+
223
+ scroll_into_view_if_needed
224
+ point = clickable_point
225
+ @page.touchscreen.tap(point.x, point.y)
226
+ end
227
+
228
+ async def async_tap
229
+ tap
230
+ end
206
231
 
207
232
  def focus
208
233
  evaluate('element => element.focus()')
@@ -373,47 +398,57 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
373
398
  result
374
399
  end
375
400
 
401
+ # `$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
402
+ # @param selector [String]
403
+ # @param page_function [String]
404
+ # @return [Object]
405
+ async def async_Seval(selector, page_function, *args)
406
+ Seval(selector, page_function, *args)
407
+ end
408
+
376
409
  # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
377
410
  # @param selector [String]
378
411
  # @param page_function [String]
379
412
  # @return [Object]
380
413
  def SSeval(selector, page_function, *args)
381
414
  handles = evaluate_handle(
382
- '(element, selector) => Array.from(element.querySelectorAll(selector))',
383
- selector)
415
+ '(element, selector) => Array.from(element.querySelectorAll(selector))',
416
+ selector,
417
+ )
384
418
  result = handles.evaluate(page_function, *args)
385
419
  handles.dispose
386
420
 
387
421
  result
388
422
  end
389
423
 
390
- # /**
391
- # * @param {string} expression
392
- # * @return {!Promise<!Array<!ElementHandle>>}
393
- # */
394
- # async $x(expression) {
395
- # const arrayHandle = await this.evaluateHandle(
396
- # (element, expression) => {
397
- # const document = element.ownerDocument || element;
398
- # const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
399
- # const array = [];
400
- # let item;
401
- # while ((item = iterator.iterateNext()))
402
- # array.push(item);
403
- # return array;
404
- # },
405
- # expression
406
- # );
407
- # const properties = await arrayHandle.getProperties();
408
- # await arrayHandle.dispose();
409
- # const result = [];
410
- # for (const property of properties.values()) {
411
- # const elementHandle = property.asElement();
412
- # if (elementHandle)
413
- # result.push(elementHandle);
414
- # }
415
- # return result;
416
- # }
424
+ # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
425
+ # @param selector [String]
426
+ # @param page_function [String]
427
+ # @return [Object]
428
+ async def async_SSeval(selector, page_function, *args)
429
+ SSeval(selector, page_function, *args)
430
+ end
431
+
432
+ # `$x()` in JavaScript. $ is not allowed to use as a method name in Ruby.
433
+ # @param expression [String]
434
+ # @return [Array<ElementHandle>]
435
+ def Sx(expression)
436
+ fn = <<~JAVASCRIPT
437
+ (element, expression) => {
438
+ const document = element.ownerDocument || element;
439
+ const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
440
+ const array = [];
441
+ let item;
442
+ while ((item = iterator.iterateNext()))
443
+ array.push(item);
444
+ return array;
445
+ }
446
+ JAVASCRIPT
447
+ handles = evaluate_handle(fn, expression)
448
+ properties = handles.properties
449
+ handles.dispose
450
+ properties.values.map(&:as_element).compact
451
+ end
417
452
 
418
453
  # /**
419
454
  # * @returns {!Promise<boolean>}
@@ -430,4 +465,11 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
430
465
  # return visibleRatio > 0;
431
466
  # });
432
467
  # }
468
+
469
+ # @param quad [Array<Point>]
470
+ private def compute_quad_area(quad)
471
+ # Compute sum of all directed areas of adjacent triangles
472
+ # https://en.wikipedia.org/wiki/Polygon#Simple_polygons
473
+ quad.zip(quad.rotate).map { |p1, p2| (p1.x * p2.y - p2.x * p1.y) / 2 }.reduce(:+).abs
474
+ end
433
475
  end