puppeteer-ruby 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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