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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +30 -0
- data/.github/stale.yml +16 -0
- data/.rubocop.yml +4 -5
- data/README.md +4 -1
- data/docs/Puppeteer.html +2020 -0
- data/docs/Puppeteer/AsyncAwaitBehavior.html +105 -0
- data/docs/Puppeteer/Browser.html +2150 -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 +2224 -0
- data/docs/Puppeteer/ElementHandle/ElementNotFoundError.html +206 -0
- data/docs/Puppeteer/ElementHandle/ElementNotVisibleError.html +206 -0
- data/docs/Puppeteer/ElementHandle/Point.html +481 -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 +2414 -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 +1016 -0
- data/docs/Puppeteer/Target.html +1384 -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 +757 -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 +809 -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 +3979 -0
- data/docs/top-level-namespace.html +126 -0
- data/lib/puppeteer.rb +16 -8
- data/lib/puppeteer/async_await_behavior.rb +6 -0
- data/lib/puppeteer/browser.rb +21 -1
- data/lib/puppeteer/browser_runner.rb +1 -1
- data/lib/puppeteer/cdp_session.rb +33 -11
- data/lib/puppeteer/connection.rb +1 -1
- data/lib/puppeteer/dom_world.rb +142 -121
- data/lib/puppeteer/element_handle.rb +223 -181
- data/lib/puppeteer/execution_context.rb +41 -17
- data/lib/puppeteer/file_chooser.rb +29 -0
- data/lib/puppeteer/frame.rb +23 -15
- data/lib/puppeteer/frame_manager.rb +7 -9
- data/lib/puppeteer/js_handle.rb +3 -3
- data/lib/puppeteer/keyboard.rb +1 -1
- data/lib/puppeteer/keyboard/us_keyboard_layout.rb +4 -4
- data/lib/puppeteer/launcher.rb +0 -1
- data/lib/puppeteer/launcher/chrome.rb +48 -2
- data/lib/puppeteer/lifecycle_watcher.rb +9 -4
- data/lib/puppeteer/mouse.rb +10 -7
- data/lib/puppeteer/page.rb +134 -70
- data/lib/puppeteer/remote_object.rb +11 -1
- data/lib/puppeteer/version.rb +1 -1
- data/lib/puppeteer/wait_task.rb +183 -1
- data/puppeteer-ruby.gemspec +4 -1
- 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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
#
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
#
|
112
|
-
#
|
113
|
-
#
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
#
|
131
|
-
#
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
-
#
|
140
|
-
#
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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
|
-
|
383
|
-
|
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
|
-
#
|
392
|
-
#
|
393
|
-
#
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
#
|
399
|
-
#
|
400
|
-
#
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
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
|