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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +36 -0
  5. data/.travis.yml +7 -0
  6. data/Dockerfile +6 -0
  7. data/Gemfile +6 -0
  8. data/README.md +41 -0
  9. data/Rakefile +1 -0
  10. data/bin/console +11 -0
  11. data/bin/setup +8 -0
  12. data/docker-compose.yml +15 -0
  13. data/example.rb +7 -0
  14. data/lib/puppeteer.rb +192 -0
  15. data/lib/puppeteer/async_await_behavior.rb +34 -0
  16. data/lib/puppeteer/browser.rb +240 -0
  17. data/lib/puppeteer/browser_context.rb +90 -0
  18. data/lib/puppeteer/browser_fetcher.rb +6 -0
  19. data/lib/puppeteer/browser_runner.rb +142 -0
  20. data/lib/puppeteer/cdp_session.rb +78 -0
  21. data/lib/puppeteer/concurrent_ruby_utils.rb +37 -0
  22. data/lib/puppeteer/connection.rb +254 -0
  23. data/lib/puppeteer/console_message.rb +24 -0
  24. data/lib/puppeteer/debug_print.rb +20 -0
  25. data/lib/puppeteer/device.rb +12 -0
  26. data/lib/puppeteer/devices.rb +885 -0
  27. data/lib/puppeteer/dom_world.rb +447 -0
  28. data/lib/puppeteer/element_handle.rb +433 -0
  29. data/lib/puppeteer/emulation_manager.rb +46 -0
  30. data/lib/puppeteer/errors.rb +4 -0
  31. data/lib/puppeteer/event_callbackable.rb +88 -0
  32. data/lib/puppeteer/execution_context.rb +230 -0
  33. data/lib/puppeteer/frame.rb +278 -0
  34. data/lib/puppeteer/frame_manager.rb +380 -0
  35. data/lib/puppeteer/if_present.rb +18 -0
  36. data/lib/puppeteer/js_handle.rb +142 -0
  37. data/lib/puppeteer/keyboard.rb +183 -0
  38. data/lib/puppeteer/keyboard/key_description.rb +19 -0
  39. data/lib/puppeteer/keyboard/us_keyboard_layout.rb +283 -0
  40. data/lib/puppeteer/launcher.rb +26 -0
  41. data/lib/puppeteer/launcher/base.rb +48 -0
  42. data/lib/puppeteer/launcher/browser_options.rb +41 -0
  43. data/lib/puppeteer/launcher/chrome.rb +165 -0
  44. data/lib/puppeteer/launcher/chrome_arg_options.rb +49 -0
  45. data/lib/puppeteer/launcher/launch_options.rb +68 -0
  46. data/lib/puppeteer/lifecycle_watcher.rb +168 -0
  47. data/lib/puppeteer/mouse.rb +120 -0
  48. data/lib/puppeteer/network_manager.rb +122 -0
  49. data/lib/puppeteer/page.rb +1001 -0
  50. data/lib/puppeteer/page/screenshot_options.rb +78 -0
  51. data/lib/puppeteer/remote_object.rb +124 -0
  52. data/lib/puppeteer/target.rb +150 -0
  53. data/lib/puppeteer/timeout_settings.rb +15 -0
  54. data/lib/puppeteer/touch_screen.rb +43 -0
  55. data/lib/puppeteer/version.rb +3 -0
  56. data/lib/puppeteer/viewport.rb +36 -0
  57. data/lib/puppeteer/wait_task.rb +6 -0
  58. data/lib/puppeteer/web_socket.rb +117 -0
  59. data/lib/puppeteer/web_socket_transport.rb +49 -0
  60. data/puppeteer-ruby.gemspec +29 -0
  61. 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