puppeteer-ruby 0.0.3 → 0.0.8

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