puppeteer-ruby 0.0.2
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/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +36 -0
- data/.travis.yml +7 -0
- data/Dockerfile +6 -0
- data/Gemfile +6 -0
- data/README.md +41 -0
- data/Rakefile +1 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/docker-compose.yml +15 -0
- data/example.rb +7 -0
- data/lib/puppeteer.rb +192 -0
- data/lib/puppeteer/async_await_behavior.rb +34 -0
- data/lib/puppeteer/browser.rb +240 -0
- data/lib/puppeteer/browser_context.rb +90 -0
- data/lib/puppeteer/browser_fetcher.rb +6 -0
- data/lib/puppeteer/browser_runner.rb +142 -0
- data/lib/puppeteer/cdp_session.rb +78 -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 +447 -0
- data/lib/puppeteer/element_handle.rb +433 -0
- data/lib/puppeteer/emulation_manager.rb +46 -0
- data/lib/puppeteer/errors.rb +4 -0
- data/lib/puppeteer/event_callbackable.rb +88 -0
- data/lib/puppeteer/execution_context.rb +230 -0
- data/lib/puppeteer/frame.rb +278 -0
- data/lib/puppeteer/frame_manager.rb +380 -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 +26 -0
- data/lib/puppeteer/launcher/base.rb +48 -0
- data/lib/puppeteer/launcher/browser_options.rb +41 -0
- data/lib/puppeteer/launcher/chrome.rb +165 -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 +168 -0
- data/lib/puppeteer/mouse.rb +120 -0
- data/lib/puppeteer/network_manager.rb +122 -0
- data/lib/puppeteer/page.rb +1001 -0
- data/lib/puppeteer/page/screenshot_options.rb +78 -0
- data/lib/puppeteer/remote_object.rb +124 -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 +36 -0
- data/lib/puppeteer/wait_task.rb +6 -0
- data/lib/puppeteer/web_socket.rb +117 -0
- data/lib/puppeteer/web_socket_transport.rb +49 -0
- data/puppeteer-ruby.gemspec +29 -0
- metadata +213 -0
@@ -0,0 +1,447 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
# https://github.com/puppeteer/puppeteer/blob/master/src/DOMWorld.js
|
4
|
+
class Puppeteer::DOMWorld
|
5
|
+
using Puppeteer::AsyncAwaitBehavior
|
6
|
+
|
7
|
+
# @param {!Puppeteer.FrameManager} frameManager
|
8
|
+
# @param {!Puppeteer.Frame} frame
|
9
|
+
# @param {!Puppeteer.TimeoutSettings} timeoutSettings
|
10
|
+
def initialize(frame_manager, frame, timeout_settings)
|
11
|
+
@frame_manager = frame_manager
|
12
|
+
@frame = frame
|
13
|
+
@timeout_settings = timeout_settings
|
14
|
+
@context_promise = resolvable_future
|
15
|
+
@wait_tasks = Set.new
|
16
|
+
@detached = false
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :frame
|
20
|
+
|
21
|
+
# @param {?Puppeteer.ExecutionContext} context
|
22
|
+
def context=(context)
|
23
|
+
if context
|
24
|
+
@context_promise.fulfill(context)
|
25
|
+
# for (const waitTask of this._waitTasks)
|
26
|
+
# waitTask.rerun();
|
27
|
+
else
|
28
|
+
@document = nil
|
29
|
+
@context_promise = resolvable_future
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def has_context?
|
34
|
+
@context_promise.resolved?
|
35
|
+
end
|
36
|
+
|
37
|
+
private def detach
|
38
|
+
@detached = true
|
39
|
+
@wait_tasks.each do |wait_task|
|
40
|
+
wait_task.terminate(Puppeteer::WaitTask::TerminatedError.new('waitForFunction failed: frame got detached.'))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class DetachedError < StandardError ; end
|
45
|
+
|
46
|
+
# @return {!Promise<!Puppeteer.ExecutionContext>}
|
47
|
+
def execution_context
|
48
|
+
if @detached
|
49
|
+
raise DetachedError.new("Execution Context is not available in detached frame \"#{@frame.url}\" (are you trying to evaluate?)")
|
50
|
+
end
|
51
|
+
@context_promise.value!
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param {Function|string} pageFunction
|
55
|
+
# @param {!Array<*>} args
|
56
|
+
# @return {!Promise<!Puppeteer.JSHandle>}
|
57
|
+
def evaluate_handle(page_function, *args)
|
58
|
+
execution_context.evaluate_handle(page_function, *args)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param {Function|string} pageFunction
|
62
|
+
# @param {!Array<*>} args
|
63
|
+
# @return {!Promise<*>}
|
64
|
+
def evaluate(page_function, *args)
|
65
|
+
execution_context.evaluate(page_function, *args)
|
66
|
+
end
|
67
|
+
|
68
|
+
# `$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
|
69
|
+
# @param {string} selector
|
70
|
+
# @return {!Promise<?Puppeteer.ElementHandle>}
|
71
|
+
def S(selector)
|
72
|
+
document.S(selector)
|
73
|
+
end
|
74
|
+
|
75
|
+
private def document
|
76
|
+
@document ||= execution_context.evaluate_handle('document').as_element
|
77
|
+
end
|
78
|
+
|
79
|
+
# `$x()` in JavaScript. $ is not allowed to use as a method name in Ruby.
|
80
|
+
# @param {string} expression
|
81
|
+
# @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
|
82
|
+
def Sx(expression)
|
83
|
+
document.Sx(expression)
|
84
|
+
end
|
85
|
+
|
86
|
+
# `$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
|
87
|
+
# @param {string} selector
|
88
|
+
# @param {Function|string} pageFunction
|
89
|
+
# @param {!Array<*>} args
|
90
|
+
# @return {!Promise<(!Object|undefined)>}
|
91
|
+
def Seval(selector, page_function, *args)
|
92
|
+
document.Seval(selector, page_function, *args)
|
93
|
+
end
|
94
|
+
|
95
|
+
# `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
|
96
|
+
# @param {string} selector
|
97
|
+
# @param {Function|string} pageFunction
|
98
|
+
# @param {!Array<*>} args
|
99
|
+
# @return {!Promise<(!Object|undefined)>}
|
100
|
+
def SSeval(selector, page_function, *args)
|
101
|
+
document.SSeval(selector, page_function, *args)
|
102
|
+
end
|
103
|
+
|
104
|
+
# `$$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
|
105
|
+
# @param {string} selector
|
106
|
+
# @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
|
107
|
+
def SS(selector)
|
108
|
+
document.SS(selector)
|
109
|
+
end
|
110
|
+
|
111
|
+
# /**
|
112
|
+
# * @return {!Promise<String>}
|
113
|
+
# */
|
114
|
+
# async content() {
|
115
|
+
# return await this.evaluate(() => {
|
116
|
+
# let retVal = '';
|
117
|
+
# if (document.doctype)
|
118
|
+
# retVal = new XMLSerializer().serializeToString(document.doctype);
|
119
|
+
# if (document.documentElement)
|
120
|
+
# retVal += document.documentElement.outerHTML;
|
121
|
+
# return retVal;
|
122
|
+
# });
|
123
|
+
# }
|
124
|
+
|
125
|
+
# /**
|
126
|
+
# * @param {string} html
|
127
|
+
# * @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
|
128
|
+
# */
|
129
|
+
# async setContent(html, options = {}) {
|
130
|
+
# const {
|
131
|
+
# waitUntil = ['load'],
|
132
|
+
# timeout = this._timeoutSettings.navigationTimeout(),
|
133
|
+
# } = options;
|
134
|
+
# // We rely upon the fact that document.open() will reset frame lifecycle with "init"
|
135
|
+
# // lifecycle event. @see https://crrev.com/608658
|
136
|
+
# await this.evaluate(html => {
|
137
|
+
# document.open();
|
138
|
+
# document.write(html);
|
139
|
+
# document.close();
|
140
|
+
# }, html);
|
141
|
+
# const watcher = new LifecycleWatcher(this._frameManager, this._frame, waitUntil, timeout);
|
142
|
+
# const error = await Promise.race([
|
143
|
+
# watcher.timeoutOrTerminationPromise(),
|
144
|
+
# watcher.lifecyclePromise(),
|
145
|
+
# ]);
|
146
|
+
# watcher.dispose();
|
147
|
+
# if (error)
|
148
|
+
# throw error;
|
149
|
+
# }
|
150
|
+
|
151
|
+
# /**
|
152
|
+
# * @param {!{url?: string, path?: string, content?: string, type?: string}} options
|
153
|
+
# * @return {!Promise<!Puppeteer.ElementHandle>}
|
154
|
+
# */
|
155
|
+
# async addScriptTag(options) {
|
156
|
+
# const {
|
157
|
+
# url = null,
|
158
|
+
# path = null,
|
159
|
+
# content = null,
|
160
|
+
# type = ''
|
161
|
+
# } = options;
|
162
|
+
# if (url !== null) {
|
163
|
+
# try {
|
164
|
+
# const context = await this.executionContext();
|
165
|
+
# return (await context.evaluateHandle(addScriptUrl, url, type)).asElement();
|
166
|
+
# } catch (error) {
|
167
|
+
# throw new Error(`Loading script from ${url} failed`);
|
168
|
+
# }
|
169
|
+
# }
|
170
|
+
|
171
|
+
# if (path !== null) {
|
172
|
+
# let contents = await readFileAsync(path, 'utf8');
|
173
|
+
# contents += '//# sourceURL=' + path.replace(/\n/g, '');
|
174
|
+
# const context = await this.executionContext();
|
175
|
+
# return (await context.evaluateHandle(addScriptContent, contents, type)).asElement();
|
176
|
+
# }
|
177
|
+
|
178
|
+
# if (content !== null) {
|
179
|
+
# const context = await this.executionContext();
|
180
|
+
# return (await context.evaluateHandle(addScriptContent, content, type)).asElement();
|
181
|
+
# }
|
182
|
+
|
183
|
+
# throw new Error('Provide an object with a `url`, `path` or `content` property');
|
184
|
+
|
185
|
+
# /**
|
186
|
+
# * @param {string} url
|
187
|
+
# * @param {string} type
|
188
|
+
# * @return {!Promise<!HTMLElement>}
|
189
|
+
# */
|
190
|
+
# async function addScriptUrl(url, type) {
|
191
|
+
# const script = document.createElement('script');
|
192
|
+
# script.src = url;
|
193
|
+
# if (type)
|
194
|
+
# script.type = type;
|
195
|
+
# const promise = new Promise((res, rej) => {
|
196
|
+
# script.onload = res;
|
197
|
+
# script.onerror = rej;
|
198
|
+
# });
|
199
|
+
# document.head.appendChild(script);
|
200
|
+
# await promise;
|
201
|
+
# return script;
|
202
|
+
# }
|
203
|
+
|
204
|
+
# /**
|
205
|
+
# * @param {string} content
|
206
|
+
# * @param {string} type
|
207
|
+
# * @return {!HTMLElement}
|
208
|
+
# */
|
209
|
+
# function addScriptContent(content, type = 'text/javascript') {
|
210
|
+
# const script = document.createElement('script');
|
211
|
+
# script.type = type;
|
212
|
+
# script.text = content;
|
213
|
+
# let error = null;
|
214
|
+
# script.onerror = e => error = e;
|
215
|
+
# document.head.appendChild(script);
|
216
|
+
# if (error)
|
217
|
+
# throw error;
|
218
|
+
# return script;
|
219
|
+
# }
|
220
|
+
# }
|
221
|
+
|
222
|
+
# /**
|
223
|
+
# * @param {!{url?: string, path?: string, content?: string}} options
|
224
|
+
# * @return {!Promise<!Puppeteer.ElementHandle>}
|
225
|
+
# */
|
226
|
+
# async addStyleTag(options) {
|
227
|
+
# const {
|
228
|
+
# url = null,
|
229
|
+
# path = null,
|
230
|
+
# content = null
|
231
|
+
# } = options;
|
232
|
+
# if (url !== null) {
|
233
|
+
# try {
|
234
|
+
# const context = await this.executionContext();
|
235
|
+
# return (await context.evaluateHandle(addStyleUrl, url)).asElement();
|
236
|
+
# } catch (error) {
|
237
|
+
# throw new Error(`Loading style from ${url} failed`);
|
238
|
+
# }
|
239
|
+
# }
|
240
|
+
|
241
|
+
# if (path !== null) {
|
242
|
+
# let contents = await readFileAsync(path, 'utf8');
|
243
|
+
# contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/';
|
244
|
+
# const context = await this.executionContext();
|
245
|
+
# return (await context.evaluateHandle(addStyleContent, contents)).asElement();
|
246
|
+
# }
|
247
|
+
|
248
|
+
# if (content !== null) {
|
249
|
+
# const context = await this.executionContext();
|
250
|
+
# return (await context.evaluateHandle(addStyleContent, content)).asElement();
|
251
|
+
# }
|
252
|
+
|
253
|
+
# throw new Error('Provide an object with a `url`, `path` or `content` property');
|
254
|
+
|
255
|
+
# /**
|
256
|
+
# * @param {string} url
|
257
|
+
# * @return {!Promise<!HTMLElement>}
|
258
|
+
# */
|
259
|
+
# async function addStyleUrl(url) {
|
260
|
+
# const link = document.createElement('link');
|
261
|
+
# link.rel = 'stylesheet';
|
262
|
+
# link.href = url;
|
263
|
+
# const promise = new Promise((res, rej) => {
|
264
|
+
# link.onload = res;
|
265
|
+
# link.onerror = rej;
|
266
|
+
# });
|
267
|
+
# document.head.appendChild(link);
|
268
|
+
# await promise;
|
269
|
+
# return link;
|
270
|
+
# }
|
271
|
+
|
272
|
+
# /**
|
273
|
+
# * @param {string} content
|
274
|
+
# * @return {!Promise<!HTMLElement>}
|
275
|
+
# */
|
276
|
+
# async function addStyleContent(content) {
|
277
|
+
# const style = document.createElement('style');
|
278
|
+
# style.type = 'text/css';
|
279
|
+
# style.appendChild(document.createTextNode(content));
|
280
|
+
# const promise = new Promise((res, rej) => {
|
281
|
+
# style.onload = res;
|
282
|
+
# style.onerror = rej;
|
283
|
+
# });
|
284
|
+
# document.head.appendChild(style);
|
285
|
+
# await promise;
|
286
|
+
# return style;
|
287
|
+
# }
|
288
|
+
# }
|
289
|
+
|
290
|
+
# /**
|
291
|
+
# * @param {string} selector
|
292
|
+
# * @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
|
293
|
+
# */
|
294
|
+
# async click(selector, options) {
|
295
|
+
# const handle = await this.$(selector);
|
296
|
+
# assert(handle, 'No node found for selector: ' + selector);
|
297
|
+
# await handle.click(options);
|
298
|
+
# await handle.dispose();
|
299
|
+
# }
|
300
|
+
|
301
|
+
# /**
|
302
|
+
# * @param {string} selector
|
303
|
+
# */
|
304
|
+
# async focus(selector) {
|
305
|
+
# const handle = await this.$(selector);
|
306
|
+
# assert(handle, 'No node found for selector: ' + selector);
|
307
|
+
# await handle.focus();
|
308
|
+
# await handle.dispose();
|
309
|
+
# }
|
310
|
+
|
311
|
+
# /**
|
312
|
+
# * @param {string} selector
|
313
|
+
# */
|
314
|
+
# async hover(selector) {
|
315
|
+
# const handle = await this.$(selector);
|
316
|
+
# assert(handle, 'No node found for selector: ' + selector);
|
317
|
+
# await handle.hover();
|
318
|
+
# await handle.dispose();
|
319
|
+
# }
|
320
|
+
|
321
|
+
# /**
|
322
|
+
# * @param {string} selector
|
323
|
+
# * @param {!Array<string>} values
|
324
|
+
# * @return {!Promise<!Array<string>>}
|
325
|
+
# */
|
326
|
+
# async select(selector, ...values) {
|
327
|
+
# const handle = await this.$(selector);
|
328
|
+
# assert(handle, 'No node found for selector: ' + selector);
|
329
|
+
# const result = await handle.select(...values);
|
330
|
+
# await handle.dispose();
|
331
|
+
# return result;
|
332
|
+
# }
|
333
|
+
|
334
|
+
# /**
|
335
|
+
# * @param {string} selector
|
336
|
+
# */
|
337
|
+
# async tap(selector) {
|
338
|
+
# const handle = await this.$(selector);
|
339
|
+
# assert(handle, 'No node found for selector: ' + selector);
|
340
|
+
# await handle.tap();
|
341
|
+
# await handle.dispose();
|
342
|
+
# }
|
343
|
+
|
344
|
+
# /**
|
345
|
+
# * @param {string} selector
|
346
|
+
# * @param {string} text
|
347
|
+
# * @param {{delay: (number|undefined)}=} options
|
348
|
+
# */
|
349
|
+
# async type(selector, text, options) {
|
350
|
+
# const handle = await this.$(selector);
|
351
|
+
# assert(handle, 'No node found for selector: ' + selector);
|
352
|
+
# await handle.type(text, options);
|
353
|
+
# await handle.dispose();
|
354
|
+
# }
|
355
|
+
|
356
|
+
# /**
|
357
|
+
# * @param {string} selector
|
358
|
+
# * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
|
359
|
+
# * @return {!Promise<?Puppeteer.ElementHandle>}
|
360
|
+
# */
|
361
|
+
# waitForSelector(selector, options) {
|
362
|
+
# return this._waitForSelectorOrXPath(selector, false, options);
|
363
|
+
# }
|
364
|
+
|
365
|
+
# /**
|
366
|
+
# * @param {string} xpath
|
367
|
+
# * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
|
368
|
+
# * @return {!Promise<?Puppeteer.ElementHandle>}
|
369
|
+
# */
|
370
|
+
# waitForXPath(xpath, options) {
|
371
|
+
# return this._waitForSelectorOrXPath(xpath, true, options);
|
372
|
+
# }
|
373
|
+
|
374
|
+
# /**
|
375
|
+
# * @param {Function|string} pageFunction
|
376
|
+
# * @param {!{polling?: string|number, timeout?: number}=} options
|
377
|
+
# * @return {!Promise<!Puppeteer.JSHandle>}
|
378
|
+
# */
|
379
|
+
# waitForFunction(pageFunction, options = {}, ...args) {
|
380
|
+
# const {
|
381
|
+
# polling = 'raf',
|
382
|
+
# timeout = this._timeoutSettings.timeout(),
|
383
|
+
# } = options;
|
384
|
+
# return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
|
385
|
+
# }
|
386
|
+
|
387
|
+
# /**
|
388
|
+
# * @return {!Promise<string>}
|
389
|
+
# */
|
390
|
+
# async title() {
|
391
|
+
# return this.evaluate(() => document.title);
|
392
|
+
# }
|
393
|
+
|
394
|
+
# /**
|
395
|
+
# * @param {string} selectorOrXPath
|
396
|
+
# * @param {boolean} isXPath
|
397
|
+
# * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
|
398
|
+
# * @return {!Promise<?Puppeteer.ElementHandle>}
|
399
|
+
# */
|
400
|
+
# async _waitForSelectorOrXPath(selectorOrXPath, isXPath, options = {}) {
|
401
|
+
# const {
|
402
|
+
# visible: waitForVisible = false,
|
403
|
+
# hidden: waitForHidden = false,
|
404
|
+
# timeout = this._timeoutSettings.timeout(),
|
405
|
+
# } = options;
|
406
|
+
# const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
407
|
+
# const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
|
408
|
+
# const waitTask = new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, isXPath, waitForVisible, waitForHidden);
|
409
|
+
# const handle = await waitTask.promise;
|
410
|
+
# if (!handle.asElement()) {
|
411
|
+
# await handle.dispose();
|
412
|
+
# return null;
|
413
|
+
# }
|
414
|
+
# return handle.asElement();
|
415
|
+
|
416
|
+
# /**
|
417
|
+
# * @param {string} selectorOrXPath
|
418
|
+
# * @param {boolean} isXPath
|
419
|
+
# * @param {boolean} waitForVisible
|
420
|
+
# * @param {boolean} waitForHidden
|
421
|
+
# * @return {?Node|boolean}
|
422
|
+
# */
|
423
|
+
# function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {
|
424
|
+
# const node = isXPath
|
425
|
+
# ? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
|
426
|
+
# : document.querySelector(selectorOrXPath);
|
427
|
+
# if (!node)
|
428
|
+
# return waitForHidden;
|
429
|
+
# if (!waitForVisible && !waitForHidden)
|
430
|
+
# return node;
|
431
|
+
# const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
|
432
|
+
|
433
|
+
# const style = window.getComputedStyle(element);
|
434
|
+
# const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
|
435
|
+
# const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
|
436
|
+
# return success ? node : null;
|
437
|
+
|
438
|
+
# /**
|
439
|
+
# * @return {boolean}
|
440
|
+
# */
|
441
|
+
# function hasVisibleBoundingBox() {
|
442
|
+
# const rect = element.getBoundingClientRect();
|
443
|
+
# return !!(rect.top || rect.bottom || rect.width || rect.height);
|
444
|
+
# }
|
445
|
+
# }
|
446
|
+
# }
|
447
|
+
end
|
@@ -0,0 +1,433 @@
|
|
1
|
+
class Puppeteer::ElementHandle < Puppeteer::JSHandle
|
2
|
+
include Puppeteer::IfPresent
|
3
|
+
using Puppeteer::AsyncAwaitBehavior
|
4
|
+
|
5
|
+
# @param context [Puppeteer::ExecutionContext]
|
6
|
+
# @param client [Puppeteer::CDPSession]
|
7
|
+
# @param remote_object [Puppeteer::RemoteObject]
|
8
|
+
# @param page [Puppeteer::Page]
|
9
|
+
# @param frame_manager [Puppeteer::FrameManager]
|
10
|
+
def initialize(context:, client:, remote_object:, page:, frame_manager:)
|
11
|
+
super(context: context, client: client, remote_object: remote_object)
|
12
|
+
@page = page
|
13
|
+
@frame_manager = frame_manager
|
14
|
+
@disposed = false
|
15
|
+
end
|
16
|
+
|
17
|
+
def as_element
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def content_frame
|
22
|
+
node_info = @remote_object.node_info
|
23
|
+
frame_id = node_info["node"]["frameId"]
|
24
|
+
if frame_id.is_a?(String)
|
25
|
+
@frame_manager.frame(frame_id)
|
26
|
+
else
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
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
|
+
# }
|
56
|
+
|
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
|
+
# }
|
87
|
+
|
88
|
+
# /**
|
89
|
+
# * @return {!Promise<void|Protocol.DOM.getBoxModelReturnValue>}
|
90
|
+
# */
|
91
|
+
# _getBoxModel() {
|
92
|
+
# return this._client.send('DOM.getBoxModel', {
|
93
|
+
# objectId: this._remoteObject.objectId
|
94
|
+
# }).catch(error => debugError(error));
|
95
|
+
# }
|
96
|
+
|
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
|
+
# }
|
109
|
+
|
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
|
+
# }
|
122
|
+
|
123
|
+
# async hover() {
|
124
|
+
# await this._scrollIntoViewIfNeeded();
|
125
|
+
# const {x, y} = await this._clickablePoint();
|
126
|
+
# await this._page.mouse.move(x, y);
|
127
|
+
# }
|
128
|
+
|
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
|
+
# }
|
137
|
+
|
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
|
+
# }
|
161
|
+
|
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
|
+
# }
|
199
|
+
|
200
|
+
# async tap() {
|
201
|
+
# await this._scrollIntoViewIfNeeded();
|
202
|
+
# const {x, y} = await this._clickablePoint();
|
203
|
+
# await this._page.touchscreen.tap(x, y);
|
204
|
+
# }
|
205
|
+
|
206
|
+
|
207
|
+
def focus
|
208
|
+
evaluate('element => element.focus()')
|
209
|
+
end
|
210
|
+
|
211
|
+
async def async_focus
|
212
|
+
focus
|
213
|
+
end
|
214
|
+
|
215
|
+
# @param text [String]
|
216
|
+
# @param delay [number|nil]
|
217
|
+
def type_text(text, delay: nil)
|
218
|
+
focus
|
219
|
+
@page.keyboard.type_text(text, delay: delay)
|
220
|
+
end
|
221
|
+
|
222
|
+
# @param text [String]
|
223
|
+
# @param delay [number|nil]
|
224
|
+
# @return [Future]
|
225
|
+
async def async_type_text(text, delay: nil)
|
226
|
+
type_text(text, delay: delay)
|
227
|
+
end
|
228
|
+
|
229
|
+
# @param key [String]
|
230
|
+
# @param delay [number|nil]
|
231
|
+
def press(key, delay: nil)
|
232
|
+
focus
|
233
|
+
@page.keyboard.press(key, delay: delay)
|
234
|
+
end
|
235
|
+
|
236
|
+
# @param key [String]
|
237
|
+
# @param delay [number|nil]
|
238
|
+
# @return [Future]
|
239
|
+
async def async_press(key, delay: nil)
|
240
|
+
press(key, delay: delay)
|
241
|
+
end
|
242
|
+
|
243
|
+
# /**
|
244
|
+
# * @return {!Promise<?{x: number, y: number, width: number, height: number}>}
|
245
|
+
# */
|
246
|
+
# async boundingBox() {
|
247
|
+
# const result = await this._getBoxModel();
|
248
|
+
|
249
|
+
# if (!result)
|
250
|
+
# return null;
|
251
|
+
|
252
|
+
# const quad = result.model.border;
|
253
|
+
# const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
|
254
|
+
# const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
|
255
|
+
# const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
|
256
|
+
# const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
|
257
|
+
|
258
|
+
# return {x, y, width, height};
|
259
|
+
# }
|
260
|
+
|
261
|
+
# /**
|
262
|
+
# * @return {!Promise<?BoxModel>}
|
263
|
+
# */
|
264
|
+
# async boxModel() {
|
265
|
+
# const result = await this._getBoxModel();
|
266
|
+
|
267
|
+
# if (!result)
|
268
|
+
# return null;
|
269
|
+
|
270
|
+
# const {content, padding, border, margin, width, height} = result.model;
|
271
|
+
# return {
|
272
|
+
# content: this._fromProtocolQuad(content),
|
273
|
+
# padding: this._fromProtocolQuad(padding),
|
274
|
+
# border: this._fromProtocolQuad(border),
|
275
|
+
# margin: this._fromProtocolQuad(margin),
|
276
|
+
# width,
|
277
|
+
# height
|
278
|
+
# };
|
279
|
+
# }
|
280
|
+
|
281
|
+
# /**
|
282
|
+
# *
|
283
|
+
# * @param {!Object=} options
|
284
|
+
# * @returns {!Promise<string|!Buffer>}
|
285
|
+
# */
|
286
|
+
# async screenshot(options = {}) {
|
287
|
+
# let needsViewportReset = false;
|
288
|
+
|
289
|
+
# let boundingBox = await this.boundingBox();
|
290
|
+
# assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
291
|
+
|
292
|
+
# const viewport = this._page.viewport();
|
293
|
+
|
294
|
+
# if (viewport && (boundingBox.width > viewport.width || boundingBox.height > viewport.height)) {
|
295
|
+
# const newViewport = {
|
296
|
+
# width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
|
297
|
+
# height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
|
298
|
+
# };
|
299
|
+
# await this._page.setViewport(Object.assign({}, viewport, newViewport));
|
300
|
+
|
301
|
+
# needsViewportReset = true;
|
302
|
+
# }
|
303
|
+
|
304
|
+
# await this._scrollIntoViewIfNeeded();
|
305
|
+
|
306
|
+
# boundingBox = await this.boundingBox();
|
307
|
+
# assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
308
|
+
# assert(boundingBox.width !== 0, 'Node has 0 width.');
|
309
|
+
# assert(boundingBox.height !== 0, 'Node has 0 height.');
|
310
|
+
|
311
|
+
# const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics');
|
312
|
+
|
313
|
+
# const clip = Object.assign({}, boundingBox);
|
314
|
+
# clip.x += pageX;
|
315
|
+
# clip.y += pageY;
|
316
|
+
|
317
|
+
# const imageData = await this._page.screenshot(Object.assign({}, {
|
318
|
+
# clip
|
319
|
+
# }, options));
|
320
|
+
|
321
|
+
# if (needsViewportReset)
|
322
|
+
# await this._page.setViewport(viewport);
|
323
|
+
|
324
|
+
# return imageData;
|
325
|
+
# }
|
326
|
+
|
327
|
+
# `$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
|
328
|
+
# @param selector [String]
|
329
|
+
def S(selector)
|
330
|
+
handle = evaluate_handle(
|
331
|
+
"(element, selector) => element.querySelector(selector)",
|
332
|
+
selector,
|
333
|
+
)
|
334
|
+
element = handle.as_element
|
335
|
+
|
336
|
+
if element
|
337
|
+
return element
|
338
|
+
end
|
339
|
+
handle.dispose
|
340
|
+
nil
|
341
|
+
end
|
342
|
+
|
343
|
+
# `$$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
|
344
|
+
# @param selector [String]
|
345
|
+
def SS(selector)
|
346
|
+
handles = evaluate_handle(
|
347
|
+
"(element, selector) => element.querySelectorAll(selector)",
|
348
|
+
selector,
|
349
|
+
)
|
350
|
+
properties = handles.properties
|
351
|
+
handles.dispose
|
352
|
+
properties.values.map(&:as_element).compact
|
353
|
+
end
|
354
|
+
|
355
|
+
class ElementNotFoundError < StandardError
|
356
|
+
def initialize(selector)
|
357
|
+
super("failed to find element matching selector \"#{selector}\"")
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
# `$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
|
362
|
+
# @param selector [String]
|
363
|
+
# @param page_function [String]
|
364
|
+
# @return [Object]
|
365
|
+
def Seval(selector, page_function, *args)
|
366
|
+
element_handle = S(selector)
|
367
|
+
unless element_handle
|
368
|
+
raise ElementNotFoundError.new(selector)
|
369
|
+
end
|
370
|
+
result = element_handle.evaluate(page_function, *args)
|
371
|
+
element_handle.dispose
|
372
|
+
|
373
|
+
result
|
374
|
+
end
|
375
|
+
|
376
|
+
# `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
|
377
|
+
# @param selector [String]
|
378
|
+
# @param page_function [String]
|
379
|
+
# @return [Object]
|
380
|
+
def SSeval(selector, page_function, *args)
|
381
|
+
handles = evaluate_handle(
|
382
|
+
'(element, selector) => Array.from(element.querySelectorAll(selector))',
|
383
|
+
selector)
|
384
|
+
result = handles.evaluate(page_function, *args)
|
385
|
+
handles.dispose
|
386
|
+
|
387
|
+
result
|
388
|
+
end
|
389
|
+
|
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
|
+
# }
|
417
|
+
|
418
|
+
# /**
|
419
|
+
# * @returns {!Promise<boolean>}
|
420
|
+
# */
|
421
|
+
# isIntersectingViewport() {
|
422
|
+
# return this.evaluate(async element => {
|
423
|
+
# const visibleRatio = await new Promise(resolve => {
|
424
|
+
# const observer = new IntersectionObserver(entries => {
|
425
|
+
# resolve(entries[0].intersectionRatio);
|
426
|
+
# observer.disconnect();
|
427
|
+
# });
|
428
|
+
# observer.observe(element);
|
429
|
+
# });
|
430
|
+
# return visibleRatio > 0;
|
431
|
+
# });
|
432
|
+
# }
|
433
|
+
end
|