puppeteer-ruby 0.0.10

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 (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