puppeteer-ruby 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (161) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +71 -0
  3. data/.github/stale.yml +16 -0
  4. data/.gitignore +19 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +302 -0
  7. data/.travis.yml +7 -0
  8. data/Dockerfile +6 -0
  9. data/Gemfile +6 -0
  10. data/README.md +54 -0
  11. data/Rakefile +1 -0
  12. data/bin/console +11 -0
  13. data/bin/setup +8 -0
  14. data/docker-compose.yml +15 -0
  15. data/docs/Puppeteer.html +2020 -0
  16. data/docs/Puppeteer/AsyncAwaitBehavior.html +105 -0
  17. data/docs/Puppeteer/Browser.html +2148 -0
  18. data/docs/Puppeteer/BrowserContext.html +809 -0
  19. data/docs/Puppeteer/BrowserFetcher.html +214 -0
  20. data/docs/Puppeteer/BrowserRunner.html +914 -0
  21. data/docs/Puppeteer/BrowserRunner/BrowserProcess.html +477 -0
  22. data/docs/Puppeteer/CDPSession.html +813 -0
  23. data/docs/Puppeteer/CDPSession/Error.html +124 -0
  24. data/docs/Puppeteer/ConcurrentRubyUtils.html +430 -0
  25. data/docs/Puppeteer/Connection.html +960 -0
  26. data/docs/Puppeteer/Connection/MessageCallback.html +434 -0
  27. data/docs/Puppeteer/Connection/ProtocolError.html +216 -0
  28. data/docs/Puppeteer/Connection/RequestDebugPrinter.html +217 -0
  29. data/docs/Puppeteer/Connection/ResponseDebugPrinter.html +244 -0
  30. data/docs/Puppeteer/ConsoleMessage.html +565 -0
  31. data/docs/Puppeteer/ConsoleMessage/Location.html +433 -0
  32. data/docs/Puppeteer/DOMWorld.html +2219 -0
  33. data/docs/Puppeteer/DOMWorld/DetachedError.html +124 -0
  34. data/docs/Puppeteer/DOMWorld/DocumentEvaluationError.html +124 -0
  35. data/docs/Puppeteer/DebugPrint.html +233 -0
  36. data/docs/Puppeteer/Device.html +470 -0
  37. data/docs/Puppeteer/Devices.html +139 -0
  38. data/docs/Puppeteer/ElementHandle.html +2542 -0
  39. data/docs/Puppeteer/ElementHandle/ElementNotFoundError.html +206 -0
  40. data/docs/Puppeteer/ElementHandle/ElementNotVisibleError.html +206 -0
  41. data/docs/Puppeteer/ElementHandle/Point.html +492 -0
  42. data/docs/Puppeteer/ElementHandle/ScrollIntoViewError.html +124 -0
  43. data/docs/Puppeteer/EmulationManager.html +454 -0
  44. data/docs/Puppeteer/EventCallbackable.html +433 -0
  45. data/docs/Puppeteer/EventCallbackable/EventListeners.html +435 -0
  46. data/docs/Puppeteer/ExecutionContext.html +998 -0
  47. data/docs/Puppeteer/ExecutionContext/EvaluationError.html +124 -0
  48. data/docs/Puppeteer/ExecutionContext/JavaScriptExpression.html +357 -0
  49. data/docs/Puppeteer/ExecutionContext/JavaScriptFunction.html +389 -0
  50. data/docs/Puppeteer/FileChooser.html +455 -0
  51. data/docs/Puppeteer/Frame.html +3677 -0
  52. data/docs/Puppeteer/FrameManager.html +2410 -0
  53. data/docs/Puppeteer/FrameManager/NavigationError.html +124 -0
  54. data/docs/Puppeteer/IfPresent.html +222 -0
  55. data/docs/Puppeteer/JSHandle.html +1352 -0
  56. data/docs/Puppeteer/Keyboard.html +1557 -0
  57. data/docs/Puppeteer/Keyboard/KeyDefinition.html +831 -0
  58. data/docs/Puppeteer/Keyboard/KeyDescription.html +603 -0
  59. data/docs/Puppeteer/Launcher.html +237 -0
  60. data/docs/Puppeteer/Launcher/Base.html +385 -0
  61. data/docs/Puppeteer/Launcher/Base/ExecutablePathNotFound.html +124 -0
  62. data/docs/Puppeteer/Launcher/BrowserOptions.html +441 -0
  63. data/docs/Puppeteer/Launcher/Chrome.html +669 -0
  64. data/docs/Puppeteer/Launcher/Chrome/DefaultArgs.html +382 -0
  65. data/docs/Puppeteer/Launcher/ChromeArgOptions.html +531 -0
  66. data/docs/Puppeteer/Launcher/LaunchOptions.html +893 -0
  67. data/docs/Puppeteer/LifecycleWatcher.html +834 -0
  68. data/docs/Puppeteer/LifecycleWatcher/ExpectedLifecycle.html +363 -0
  69. data/docs/Puppeteer/LifecycleWatcher/FrameDetachedError.html +206 -0
  70. data/docs/Puppeteer/LifecycleWatcher/TerminatedError.html +124 -0
  71. data/docs/Puppeteer/Mouse.html +1105 -0
  72. data/docs/Puppeteer/Mouse/Button.html +136 -0
  73. data/docs/Puppeteer/NetworkManager.html +901 -0
  74. data/docs/Puppeteer/NetworkManager/Credentials.html +385 -0
  75. data/docs/Puppeteer/Page.html +5970 -0
  76. data/docs/Puppeteer/Page/FileChooserTimeoutError.html +206 -0
  77. data/docs/Puppeteer/Page/ScreenshotOptions.html +845 -0
  78. data/docs/Puppeteer/Page/ScriptTag.html +555 -0
  79. data/docs/Puppeteer/Page/StyleTag.html +448 -0
  80. data/docs/Puppeteer/Page/TargetCrashedError.html +124 -0
  81. data/docs/Puppeteer/RemoteObject.html +1087 -0
  82. data/docs/Puppeteer/Target.html +1336 -0
  83. data/docs/Puppeteer/Target/InitializeFailure.html +124 -0
  84. data/docs/Puppeteer/Target/TargetInfo.html +729 -0
  85. data/docs/Puppeteer/TimeoutError.html +135 -0
  86. data/docs/Puppeteer/TimeoutSettings.html +496 -0
  87. data/docs/Puppeteer/TouchScreen.html +464 -0
  88. data/docs/Puppeteer/Viewport.html +837 -0
  89. data/docs/Puppeteer/WaitTask.html +637 -0
  90. data/docs/Puppeteer/WaitTask/TerminatedError.html +124 -0
  91. data/docs/Puppeteer/WaitTask/TimeoutError.html +206 -0
  92. data/docs/Puppeteer/WebSocket.html +673 -0
  93. data/docs/Puppeteer/WebSocket/DriverImpl.html +412 -0
  94. data/docs/Puppeteer/WebSocketTransport.html +600 -0
  95. data/docs/Puppeteer/WebSocktTransportError.html +124 -0
  96. data/docs/_index.html +823 -0
  97. data/docs/class_list.html +51 -0
  98. data/docs/css/common.css +1 -0
  99. data/docs/css/full_list.css +58 -0
  100. data/docs/css/style.css +496 -0
  101. data/docs/file.README.html +123 -0
  102. data/docs/file_list.html +56 -0
  103. data/docs/frames.html +17 -0
  104. data/docs/index.html +123 -0
  105. data/docs/js/app.js +314 -0
  106. data/docs/js/full_list.js +216 -0
  107. data/docs/js/jquery.js +4 -0
  108. data/docs/method_list.html +4075 -0
  109. data/docs/top-level-namespace.html +126 -0
  110. data/lib/puppeteer.rb +200 -0
  111. data/lib/puppeteer/async_await_behavior.rb +38 -0
  112. data/lib/puppeteer/browser.rb +259 -0
  113. data/lib/puppeteer/browser_context.rb +90 -0
  114. data/lib/puppeteer/browser_fetcher.rb +6 -0
  115. data/lib/puppeteer/browser_runner.rb +161 -0
  116. data/lib/puppeteer/cdp_session.rb +100 -0
  117. data/lib/puppeteer/concurrent_ruby_utils.rb +37 -0
  118. data/lib/puppeteer/connection.rb +254 -0
  119. data/lib/puppeteer/console_message.rb +24 -0
  120. data/lib/puppeteer/debug_print.rb +20 -0
  121. data/lib/puppeteer/device.rb +12 -0
  122. data/lib/puppeteer/devices.rb +885 -0
  123. data/lib/puppeteer/dom_world.rb +484 -0
  124. data/lib/puppeteer/element_handle.rb +433 -0
  125. data/lib/puppeteer/element_handle/bounding_box.rb +12 -0
  126. data/lib/puppeteer/element_handle/box_model.rb +19 -0
  127. data/lib/puppeteer/element_handle/point.rb +26 -0
  128. data/lib/puppeteer/emulation_manager.rb +46 -0
  129. data/lib/puppeteer/errors.rb +2 -0
  130. data/lib/puppeteer/event_callbackable.rb +88 -0
  131. data/lib/puppeteer/execution_context.rb +254 -0
  132. data/lib/puppeteer/file_chooser.rb +29 -0
  133. data/lib/puppeteer/frame.rb +286 -0
  134. data/lib/puppeteer/frame_manager.rb +378 -0
  135. data/lib/puppeteer/if_present.rb +18 -0
  136. data/lib/puppeteer/js_handle.rb +142 -0
  137. data/lib/puppeteer/keyboard.rb +183 -0
  138. data/lib/puppeteer/keyboard/key_description.rb +19 -0
  139. data/lib/puppeteer/keyboard/us_keyboard_layout.rb +283 -0
  140. data/lib/puppeteer/launcher.rb +25 -0
  141. data/lib/puppeteer/launcher/base.rb +48 -0
  142. data/lib/puppeteer/launcher/browser_options.rb +41 -0
  143. data/lib/puppeteer/launcher/chrome.rb +211 -0
  144. data/lib/puppeteer/launcher/chrome_arg_options.rb +49 -0
  145. data/lib/puppeteer/launcher/launch_options.rb +68 -0
  146. data/lib/puppeteer/lifecycle_watcher.rb +171 -0
  147. data/lib/puppeteer/mouse.rb +123 -0
  148. data/lib/puppeteer/network_manager.rb +122 -0
  149. data/lib/puppeteer/page.rb +1065 -0
  150. data/lib/puppeteer/page/screenshot_options.rb +78 -0
  151. data/lib/puppeteer/remote_object.rb +143 -0
  152. data/lib/puppeteer/target.rb +150 -0
  153. data/lib/puppeteer/timeout_settings.rb +15 -0
  154. data/lib/puppeteer/touch_screen.rb +43 -0
  155. data/lib/puppeteer/version.rb +3 -0
  156. data/lib/puppeteer/viewport.rb +54 -0
  157. data/lib/puppeteer/wait_task.rb +188 -0
  158. data/lib/puppeteer/web_socket.rb +122 -0
  159. data/lib/puppeteer/web_socket_transport.rb +49 -0
  160. data/puppeteer-ruby.gemspec +32 -0
  161. metadata +355 -0
@@ -0,0 +1,433 @@
1
+ require_relative './element_handle/bounding_box'
2
+ require_relative './element_handle/box_model'
3
+ require_relative './element_handle/point'
4
+
5
+ class Puppeteer::ElementHandle < Puppeteer::JSHandle
6
+ include Puppeteer::IfPresent
7
+ using Puppeteer::AsyncAwaitBehavior
8
+
9
+ # @param context [Puppeteer::ExecutionContext]
10
+ # @param client [Puppeteer::CDPSession]
11
+ # @param remote_object [Puppeteer::RemoteObject]
12
+ # @param page [Puppeteer::Page]
13
+ # @param frame_manager [Puppeteer::FrameManager]
14
+ def initialize(context:, client:, remote_object:, page:, frame_manager:)
15
+ super(context: context, client: client, remote_object: remote_object)
16
+ @page = page
17
+ @frame_manager = frame_manager
18
+ @disposed = false
19
+ end
20
+
21
+ def as_element
22
+ self
23
+ end
24
+
25
+ def content_frame
26
+ node_info = @remote_object.node_info
27
+ frame_id = node_info['node']['frameId']
28
+ if frame_id.is_a?(String)
29
+ @frame_manager.frame(frame_id)
30
+ else
31
+ nil
32
+ end
33
+ end
34
+
35
+ class ScrollIntoViewError < StandardError; end
36
+
37
+ def scroll_into_view_if_needed
38
+ js = <<~JAVASCRIPT
39
+ async(element, pageJavascriptEnabled) => {
40
+ if (!element.isConnected)
41
+ return 'Node is detached from document';
42
+ if (element.nodeType !== Node.ELEMENT_NODE)
43
+ return 'Node is not of type HTMLElement';
44
+
45
+ element.scrollIntoViewIfNeeded({block: 'center', inline: 'center', behavior: 'instant'});
46
+ return false;
47
+ }
48
+ JAVASCRIPT
49
+ error = evaluate(js, @page.javascript_enabled) # returns String or false
50
+ if error
51
+ raise ScrollIntoViewError.new(error)
52
+ end
53
+ # clickpoint is often calculated before scrolling is completed.
54
+ # So, just sleep about 10 frames
55
+ sleep 0.16
56
+ end
57
+
58
+ class ElementNotVisibleError < StandardError
59
+ def initialize
60
+ super("Node is either not visible or not an HTMLElement")
61
+ end
62
+ end
63
+
64
+ def clickable_point
65
+ result = @remote_object.content_quads(@client)
66
+ if !result || result["quads"].empty?
67
+ raise ElementNotVisibleError.new
68
+ end
69
+
70
+ # Filter out quads that have too small area to click into.
71
+ layout_metrics = @client.send_message('Page.getLayoutMetrics')
72
+ client_width = layout_metrics["layoutViewport"]["clientWidth"]
73
+ client_height = layout_metrics["layoutViewport"]["clientHeight"]
74
+
75
+ quads = result["quads"].
76
+ map { |quad| from_protocol_quad(quad) }.
77
+ map { |quad| intersect_quad_with_viewport(quad, client_width, client_height) }.
78
+ select { |quad| compute_quad_area(quad) > 1 }
79
+ if quads.empty?
80
+ raise ElementNotVisibleError.new
81
+ end
82
+
83
+ # Return the middle point of the first quad.
84
+ quads.first.reduce(:+) / 4
85
+ end
86
+
87
+ # @param quad [Array<number>]
88
+ # @return [Array<Point>]
89
+ private def from_protocol_quad(quad)
90
+ quad.each_slice(2).map do |x, y|
91
+ Point.new(x: x, y: y)
92
+ end
93
+ end
94
+
95
+ # @param quad [Array<Point>]
96
+ # @param width [number]
97
+ # @param height [number]
98
+ # @return [Array<Point>]
99
+ private def intersect_quad_with_viewport(quad, width, height)
100
+ quad.map do |point|
101
+ Point.new(
102
+ x: [[point.x, 0].max, width].min,
103
+ y: [[point.y, 0].max, height].min,
104
+ )
105
+ end
106
+ end
107
+
108
+ # async hover() {
109
+ # await this._scrollIntoViewIfNeeded();
110
+ # const {x, y} = await this._clickablePoint();
111
+ # await this._page.mouse.move(x, y);
112
+ # }
113
+
114
+ # @param delay [Number]
115
+ # @param button [String] "left"|"right"|"middle"
116
+ # @param click_count [Number]
117
+ def click(delay: nil, button: nil, click_count: nil)
118
+ scroll_into_view_if_needed
119
+ point = clickable_point
120
+ @page.mouse.click(point.x, point.y, delay: delay, button: button, click_count: click_count)
121
+ end
122
+
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
129
+
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
135
+
136
+ fn = <<~JAVASCRIPT
137
+ (element, values) => {
138
+ if (element.nodeName.toLowerCase() !== 'select') {
139
+ throw new Error('Element is not a <select> element.');
140
+ }
141
+
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
197
+
198
+ async def async_tap
199
+ tap
200
+ end
201
+
202
+ def focus
203
+ evaluate('element => element.focus()')
204
+ end
205
+
206
+ async def async_focus
207
+ focus
208
+ end
209
+
210
+ # @param text [String]
211
+ # @param delay [number|nil]
212
+ def type_text(text, delay: nil)
213
+ focus
214
+ @page.keyboard.type_text(text, delay: delay)
215
+ end
216
+
217
+ # @param text [String]
218
+ # @param delay [number|nil]
219
+ # @return [Future]
220
+ async def async_type_text(text, delay: nil)
221
+ type_text(text, delay: delay)
222
+ end
223
+
224
+ # @param key [String]
225
+ # @param delay [number|nil]
226
+ def press(key, delay: nil)
227
+ focus
228
+ @page.keyboard.press(key, delay: delay)
229
+ end
230
+
231
+ # @param key [String]
232
+ # @param delay [number|nil]
233
+ # @return [Future]
234
+ async def async_press(key, delay: nil)
235
+ press(key, delay: delay)
236
+ end
237
+
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
253
+
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
260
+
261
+ def screenshot(options = {})
262
+ needs_viewport_reset = false
263
+
264
+ box = bounding_box
265
+ unless box
266
+ raise ElementNotVisibleError.new
267
+ end
268
+
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
276
+
277
+ needs_viewport_reset = true
278
+ end
279
+ scroll_into_view_if_needed
280
+
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
291
+
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
309
+
310
+ # `$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
311
+ # @param selector [String]
312
+ def S(selector)
313
+ handle = evaluate_handle(
314
+ '(element, selector) => element.querySelector(selector)',
315
+ selector,
316
+ )
317
+ element = handle.as_element
318
+
319
+ if element
320
+ return element
321
+ end
322
+ handle.dispose
323
+ nil
324
+ end
325
+
326
+ # `$$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
327
+ # @param selector [String]
328
+ def SS(selector)
329
+ handles = evaluate_handle(
330
+ '(element, selector) => element.querySelectorAll(selector)',
331
+ selector,
332
+ )
333
+ properties = handles.properties
334
+ handles.dispose
335
+ properties.values.map(&:as_element).compact
336
+ end
337
+
338
+ class ElementNotFoundError < StandardError
339
+ def initialize(selector)
340
+ super("failed to find element matching selector \"#{selector}\"")
341
+ end
342
+ end
343
+
344
+ # `$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
345
+ # @param selector [String]
346
+ # @param page_function [String]
347
+ # @return [Object]
348
+ def Seval(selector, page_function, *args)
349
+ element_handle = S(selector)
350
+ unless element_handle
351
+ raise ElementNotFoundError.new(selector)
352
+ end
353
+ result = element_handle.evaluate(page_function, *args)
354
+ element_handle.dispose
355
+
356
+ result
357
+ end
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
+
367
+ # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
368
+ # @param selector [String]
369
+ # @param page_function [String]
370
+ # @return [Object]
371
+ def SSeval(selector, page_function, *args)
372
+ handles = evaluate_handle(
373
+ '(element, selector) => Array.from(element.querySelectorAll(selector))',
374
+ selector,
375
+ )
376
+ result = handles.evaluate(page_function, *args)
377
+ handles.dispose
378
+
379
+ result
380
+ end
381
+
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
410
+
411
+ # /**
412
+ # * @returns {!Promise<boolean>}
413
+ # */
414
+ # isIntersectingViewport() {
415
+ # return this.evaluate(async element => {
416
+ # const visibleRatio = await new Promise(resolve => {
417
+ # const observer = new IntersectionObserver(entries => {
418
+ # resolve(entries[0].intersectionRatio);
419
+ # observer.disconnect();
420
+ # });
421
+ # observer.observe(element);
422
+ # });
423
+ # return visibleRatio > 0;
424
+ # });
425
+ # }
426
+
427
+ # @param quad [Array<Point>]
428
+ private def compute_quad_area(quad)
429
+ # Compute sum of all directed areas of adjacent triangles
430
+ # https://en.wikipedia.org/wiki/Polygon#Simple_polygons
431
+ quad.zip(quad.rotate).map { |p1, p2| (p1.x * p2.y - p2.x * p1.y) / 2 }.reduce(:+).abs
432
+ end
433
+ end
@@ -0,0 +1,12 @@
1
+ class Puppeteer::ElementHandle < Puppeteer::JSHandle
2
+ class BoundingBox
3
+ def initialize(x:, y:, width:, height:)
4
+ @x = x
5
+ @y = y
6
+ @width = width
7
+ @height = height
8
+ end
9
+
10
+ attr_reader :x, :y, :width, :height
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ class Puppeteer::ElementHandle < Puppeteer::JSHandle
2
+ class BoxModel
3
+ QUAD_ATTRIBUTE_NAMES = %i(content padding border margin)
4
+ # @param result [Hash]
5
+ def initialize(result_model)
6
+ QUAD_ATTRIBUTE_NAMES.each do |attr_name|
7
+ quad = result_model[attr_name.to_s]
8
+ instance_variable_set(
9
+ :"@#{attr_name}",
10
+ quad.each_slice(2).map { |x, y| Point.new(x: x, y: y) },
11
+ )
12
+ end
13
+ @width = result_model['width']
14
+ @height = result_model['height']
15
+ end
16
+ attr_reader(*QUAD_ATTRIBUTE_NAMES)
17
+ attr_reader :width, :height
18
+ end
19
+ end