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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +71 -0
- data/.github/stale.yml +16 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +302 -0
- data/.travis.yml +7 -0
- data/Dockerfile +6 -0
- data/Gemfile +6 -0
- data/README.md +54 -0
- data/Rakefile +1 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/docker-compose.yml +15 -0
- data/docs/Puppeteer.html +2020 -0
- data/docs/Puppeteer/AsyncAwaitBehavior.html +105 -0
- data/docs/Puppeteer/Browser.html +2148 -0
- data/docs/Puppeteer/BrowserContext.html +809 -0
- data/docs/Puppeteer/BrowserFetcher.html +214 -0
- data/docs/Puppeteer/BrowserRunner.html +914 -0
- data/docs/Puppeteer/BrowserRunner/BrowserProcess.html +477 -0
- data/docs/Puppeteer/CDPSession.html +813 -0
- data/docs/Puppeteer/CDPSession/Error.html +124 -0
- data/docs/Puppeteer/ConcurrentRubyUtils.html +430 -0
- data/docs/Puppeteer/Connection.html +960 -0
- data/docs/Puppeteer/Connection/MessageCallback.html +434 -0
- data/docs/Puppeteer/Connection/ProtocolError.html +216 -0
- data/docs/Puppeteer/Connection/RequestDebugPrinter.html +217 -0
- data/docs/Puppeteer/Connection/ResponseDebugPrinter.html +244 -0
- data/docs/Puppeteer/ConsoleMessage.html +565 -0
- data/docs/Puppeteer/ConsoleMessage/Location.html +433 -0
- data/docs/Puppeteer/DOMWorld.html +2219 -0
- data/docs/Puppeteer/DOMWorld/DetachedError.html +124 -0
- data/docs/Puppeteer/DOMWorld/DocumentEvaluationError.html +124 -0
- data/docs/Puppeteer/DebugPrint.html +233 -0
- data/docs/Puppeteer/Device.html +470 -0
- data/docs/Puppeteer/Devices.html +139 -0
- data/docs/Puppeteer/ElementHandle.html +2542 -0
- data/docs/Puppeteer/ElementHandle/ElementNotFoundError.html +206 -0
- data/docs/Puppeteer/ElementHandle/ElementNotVisibleError.html +206 -0
- data/docs/Puppeteer/ElementHandle/Point.html +492 -0
- data/docs/Puppeteer/ElementHandle/ScrollIntoViewError.html +124 -0
- data/docs/Puppeteer/EmulationManager.html +454 -0
- data/docs/Puppeteer/EventCallbackable.html +433 -0
- data/docs/Puppeteer/EventCallbackable/EventListeners.html +435 -0
- data/docs/Puppeteer/ExecutionContext.html +998 -0
- data/docs/Puppeteer/ExecutionContext/EvaluationError.html +124 -0
- data/docs/Puppeteer/ExecutionContext/JavaScriptExpression.html +357 -0
- data/docs/Puppeteer/ExecutionContext/JavaScriptFunction.html +389 -0
- data/docs/Puppeteer/FileChooser.html +455 -0
- data/docs/Puppeteer/Frame.html +3677 -0
- data/docs/Puppeteer/FrameManager.html +2410 -0
- data/docs/Puppeteer/FrameManager/NavigationError.html +124 -0
- data/docs/Puppeteer/IfPresent.html +222 -0
- data/docs/Puppeteer/JSHandle.html +1352 -0
- data/docs/Puppeteer/Keyboard.html +1557 -0
- data/docs/Puppeteer/Keyboard/KeyDefinition.html +831 -0
- data/docs/Puppeteer/Keyboard/KeyDescription.html +603 -0
- data/docs/Puppeteer/Launcher.html +237 -0
- data/docs/Puppeteer/Launcher/Base.html +385 -0
- data/docs/Puppeteer/Launcher/Base/ExecutablePathNotFound.html +124 -0
- data/docs/Puppeteer/Launcher/BrowserOptions.html +441 -0
- data/docs/Puppeteer/Launcher/Chrome.html +669 -0
- data/docs/Puppeteer/Launcher/Chrome/DefaultArgs.html +382 -0
- data/docs/Puppeteer/Launcher/ChromeArgOptions.html +531 -0
- data/docs/Puppeteer/Launcher/LaunchOptions.html +893 -0
- data/docs/Puppeteer/LifecycleWatcher.html +834 -0
- data/docs/Puppeteer/LifecycleWatcher/ExpectedLifecycle.html +363 -0
- data/docs/Puppeteer/LifecycleWatcher/FrameDetachedError.html +206 -0
- data/docs/Puppeteer/LifecycleWatcher/TerminatedError.html +124 -0
- data/docs/Puppeteer/Mouse.html +1105 -0
- data/docs/Puppeteer/Mouse/Button.html +136 -0
- data/docs/Puppeteer/NetworkManager.html +901 -0
- data/docs/Puppeteer/NetworkManager/Credentials.html +385 -0
- data/docs/Puppeteer/Page.html +5970 -0
- data/docs/Puppeteer/Page/FileChooserTimeoutError.html +206 -0
- data/docs/Puppeteer/Page/ScreenshotOptions.html +845 -0
- data/docs/Puppeteer/Page/ScriptTag.html +555 -0
- data/docs/Puppeteer/Page/StyleTag.html +448 -0
- data/docs/Puppeteer/Page/TargetCrashedError.html +124 -0
- data/docs/Puppeteer/RemoteObject.html +1087 -0
- data/docs/Puppeteer/Target.html +1336 -0
- data/docs/Puppeteer/Target/InitializeFailure.html +124 -0
- data/docs/Puppeteer/Target/TargetInfo.html +729 -0
- data/docs/Puppeteer/TimeoutError.html +135 -0
- data/docs/Puppeteer/TimeoutSettings.html +496 -0
- data/docs/Puppeteer/TouchScreen.html +464 -0
- data/docs/Puppeteer/Viewport.html +837 -0
- data/docs/Puppeteer/WaitTask.html +637 -0
- data/docs/Puppeteer/WaitTask/TerminatedError.html +124 -0
- data/docs/Puppeteer/WaitTask/TimeoutError.html +206 -0
- data/docs/Puppeteer/WebSocket.html +673 -0
- data/docs/Puppeteer/WebSocket/DriverImpl.html +412 -0
- data/docs/Puppeteer/WebSocketTransport.html +600 -0
- data/docs/Puppeteer/WebSocktTransportError.html +124 -0
- data/docs/_index.html +823 -0
- data/docs/class_list.html +51 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +58 -0
- data/docs/css/style.css +496 -0
- data/docs/file.README.html +123 -0
- data/docs/file_list.html +56 -0
- data/docs/frames.html +17 -0
- data/docs/index.html +123 -0
- data/docs/js/app.js +314 -0
- data/docs/js/full_list.js +216 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +4075 -0
- data/docs/top-level-namespace.html +126 -0
- data/lib/puppeteer.rb +200 -0
- data/lib/puppeteer/async_await_behavior.rb +38 -0
- data/lib/puppeteer/browser.rb +259 -0
- data/lib/puppeteer/browser_context.rb +90 -0
- data/lib/puppeteer/browser_fetcher.rb +6 -0
- data/lib/puppeteer/browser_runner.rb +161 -0
- data/lib/puppeteer/cdp_session.rb +100 -0
- data/lib/puppeteer/concurrent_ruby_utils.rb +37 -0
- data/lib/puppeteer/connection.rb +254 -0
- data/lib/puppeteer/console_message.rb +24 -0
- data/lib/puppeteer/debug_print.rb +20 -0
- data/lib/puppeteer/device.rb +12 -0
- data/lib/puppeteer/devices.rb +885 -0
- data/lib/puppeteer/dom_world.rb +484 -0
- data/lib/puppeteer/element_handle.rb +433 -0
- data/lib/puppeteer/element_handle/bounding_box.rb +12 -0
- data/lib/puppeteer/element_handle/box_model.rb +19 -0
- data/lib/puppeteer/element_handle/point.rb +26 -0
- data/lib/puppeteer/emulation_manager.rb +46 -0
- data/lib/puppeteer/errors.rb +2 -0
- data/lib/puppeteer/event_callbackable.rb +88 -0
- data/lib/puppeteer/execution_context.rb +254 -0
- data/lib/puppeteer/file_chooser.rb +29 -0
- data/lib/puppeteer/frame.rb +286 -0
- data/lib/puppeteer/frame_manager.rb +378 -0
- data/lib/puppeteer/if_present.rb +18 -0
- data/lib/puppeteer/js_handle.rb +142 -0
- data/lib/puppeteer/keyboard.rb +183 -0
- data/lib/puppeteer/keyboard/key_description.rb +19 -0
- data/lib/puppeteer/keyboard/us_keyboard_layout.rb +283 -0
- data/lib/puppeteer/launcher.rb +25 -0
- data/lib/puppeteer/launcher/base.rb +48 -0
- data/lib/puppeteer/launcher/browser_options.rb +41 -0
- data/lib/puppeteer/launcher/chrome.rb +211 -0
- data/lib/puppeteer/launcher/chrome_arg_options.rb +49 -0
- data/lib/puppeteer/launcher/launch_options.rb +68 -0
- data/lib/puppeteer/lifecycle_watcher.rb +171 -0
- data/lib/puppeteer/mouse.rb +123 -0
- data/lib/puppeteer/network_manager.rb +122 -0
- data/lib/puppeteer/page.rb +1065 -0
- data/lib/puppeteer/page/screenshot_options.rb +78 -0
- data/lib/puppeteer/remote_object.rb +143 -0
- data/lib/puppeteer/target.rb +150 -0
- data/lib/puppeteer/timeout_settings.rb +15 -0
- data/lib/puppeteer/touch_screen.rb +43 -0
- data/lib/puppeteer/version.rb +3 -0
- data/lib/puppeteer/viewport.rb +54 -0
- data/lib/puppeteer/wait_task.rb +188 -0
- data/lib/puppeteer/web_socket.rb +122 -0
- data/lib/puppeteer/web_socket_transport.rb +49 -0
- data/puppeteer-ruby.gemspec +32 -0
- 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,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
|