puppeteer-ruby 0.28.1 → 0.29.0
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 +4 -4
- data/.circleci/config.yml +1 -1
- data/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- data/.github/workflows/reviewdog.yml +1 -1
- data/CHANGELOG.md +8 -1
- data/README.md +18 -2
- data/lib/puppeteer.rb +3 -0
- data/lib/puppeteer/aria_query_handler.rb +71 -0
- data/lib/puppeteer/custom_query_handler.rb +51 -0
- data/lib/puppeteer/dom_world.rb +231 -74
- data/lib/puppeteer/element_handle.rb +13 -22
- data/lib/puppeteer/execution_context.rb +12 -0
- data/lib/puppeteer/query_handler_manager.rb +65 -0
- data/lib/puppeteer/remote_object.rb +12 -0
- data/lib/puppeteer/version.rb +1 -1
- data/lib/puppeteer/wait_task.rb +16 -4
- data/puppeteer-ruby.gemspec +1 -1
- metadata +8 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2b852a48cd11f269c3b4eec0a85583b120a5cd1b0bc204e27a6ec0e365aa33dd
|
4
|
+
data.tar.gz: 8864c21fe02f6e3fcbb07c6c009516e91273ecfe0636437bc41015187e9985ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd3cdb6d7b64b0b6f6e2568eb83cad62f36ea45943f0df9cd434c6e30221ccd47b161a0368cbe02cacf4a6a3c6c6ecf293bd1313829d7f02daed5d1d2c5c9f73
|
7
|
+
data.tar.gz: 541c2dffa4c6359dff6c2d9d03e70a039057d554c1ee6509d0574d194f17a6407a968c30153bae850f8c4c0e9fa7ade7beaea2dcc8d136a896e80b22831a436f
|
data/.circleci/config.yml
CHANGED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
name: Feature request
|
3
|
+
about: Request a new feature for playwright-ruby-client
|
4
|
+
title: ''
|
5
|
+
labels: ''
|
6
|
+
assignees: ''
|
7
|
+
|
8
|
+
---
|
9
|
+
|
10
|
+
### Simple description about the feature
|
11
|
+
|
12
|
+
|
13
|
+
### Usecase / Motivation
|
14
|
+
|
15
|
+
<!-- Describe why the feature helps. -->
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,14 @@
|
|
1
|
-
### master [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.
|
1
|
+
### master [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.29.0...master)]
|
2
2
|
|
3
3
|
* xxx
|
4
4
|
|
5
|
+
### 0.29.0 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.28.1...0.29.0)]
|
6
|
+
|
7
|
+
New features:
|
8
|
+
|
9
|
+
* Add `AriaQueryHandler`. Now we can use "aria/...." for selectors.
|
10
|
+
|
11
|
+
|
5
12
|
### 0.28.1 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.0.27...0.28.1)]
|
6
13
|
|
7
14
|
New features:
|
data/README.md
CHANGED
@@ -1,15 +1,31 @@
|
|
1
1
|
[](https://badge.fury.io/rb/puppeteer-ruby)
|
2
2
|
|
3
|
-
# Puppeteer in Ruby
|
3
|
+
# Puppeteer in Ruby
|
4
4
|
|
5
5
|
A Ruby port of [puppeteer](https://pptr.dev/).
|
6
6
|
|
7
7
|

|
8
8
|
|
9
|
-
REMARK: This Gem
|
9
|
+
REMARK: This Gem covers just a part of Puppeteer APIs. Feedbacks and feature requests are welcome :)
|
10
10
|
|
11
11
|
## Getting Started
|
12
12
|
|
13
|
+
### Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'puppeteer-ruby'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install puppeteer-ruby
|
28
|
+
|
13
29
|
### Capture a site
|
14
30
|
|
15
31
|
```ruby
|
data/lib/puppeteer.rb
CHANGED
@@ -19,12 +19,14 @@ require 'puppeteer/event_callbackable'
|
|
19
19
|
require 'puppeteer/if_present'
|
20
20
|
|
21
21
|
# Classes & values.
|
22
|
+
require 'puppeteer/aria_query_handler'
|
22
23
|
require 'puppeteer/browser'
|
23
24
|
require 'puppeteer/browser_context'
|
24
25
|
require 'puppeteer/browser_runner'
|
25
26
|
require 'puppeteer/cdp_session'
|
26
27
|
require 'puppeteer/connection'
|
27
28
|
require 'puppeteer/console_message'
|
29
|
+
require 'puppeteer/custom_query_handler'
|
28
30
|
require 'puppeteer/devices'
|
29
31
|
require 'puppeteer/dialog'
|
30
32
|
require 'puppeteer/dom_world'
|
@@ -41,6 +43,7 @@ require 'puppeteer/lifecycle_watcher'
|
|
41
43
|
require 'puppeteer/mouse'
|
42
44
|
require 'puppeteer/network_manager'
|
43
45
|
require 'puppeteer/page'
|
46
|
+
require 'puppeteer/query_handler_manager'
|
44
47
|
require 'puppeteer/remote_object'
|
45
48
|
require 'puppeteer/request'
|
46
49
|
require 'puppeteer/response'
|
@@ -0,0 +1,71 @@
|
|
1
|
+
class Puppeteer::AriaQueryHandler
|
2
|
+
private def normalize(value)
|
3
|
+
value.gsub(/ +/, ' ').strip
|
4
|
+
end
|
5
|
+
|
6
|
+
# @param selector [String]
|
7
|
+
private def parse_aria_selector(selector)
|
8
|
+
known_attributes = %w(name role)
|
9
|
+
query_options = {}
|
10
|
+
attribute_regexp = /\[\s*(?<attribute>\w+)\s*=\s*"(?<value>\\.|[^"\\]*)"\s*\]/
|
11
|
+
default_name = selector.gsub(attribute_regexp) do
|
12
|
+
attribute = $1.strip
|
13
|
+
value = $2
|
14
|
+
unless known_attributes.include?(attribute)
|
15
|
+
raise ArgumentError.new("Unkown aria attribute \"#{attribute}\" in selector")
|
16
|
+
end
|
17
|
+
query_options[attribute.to_sym] = normalize(value)
|
18
|
+
''
|
19
|
+
end
|
20
|
+
|
21
|
+
if default_name.length > 0
|
22
|
+
query_options[:name] ||= normalize(default_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
query_options
|
26
|
+
end
|
27
|
+
|
28
|
+
def query_one(element, selector)
|
29
|
+
context = element.execution_context
|
30
|
+
parse_result = parse_aria_selector(selector)
|
31
|
+
res = element.query_ax_tree(accessible_name: parse_result[:name], role: parse_result[:role])
|
32
|
+
if res.empty?
|
33
|
+
nil
|
34
|
+
else
|
35
|
+
context.adopt_backend_node_id(res.first['backendDOMNodeId'])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil)
|
40
|
+
binding_function = Puppeteer::DOMWorld::BindingFunction.new(
|
41
|
+
name: 'ariaQuerySelector',
|
42
|
+
proc: -> (selector) { query_one(dom_world.send(:document), selector) },
|
43
|
+
)
|
44
|
+
dom_world.send(:wait_for_selector_in_page,
|
45
|
+
'(_, selector) => globalThis.ariaQuerySelector(selector)',
|
46
|
+
selector,
|
47
|
+
visible: visible,
|
48
|
+
hidden: hidden,
|
49
|
+
timeout: timeout,
|
50
|
+
binding_function: binding_function)
|
51
|
+
end
|
52
|
+
|
53
|
+
def query_all(element, selector)
|
54
|
+
context = element.execution_context
|
55
|
+
parse_result = parse_aria_selector(selector)
|
56
|
+
res = element.query_ax_tree(accessible_name: parse_result[:name], role: parse_result[:role])
|
57
|
+
if res.empty?
|
58
|
+
nil
|
59
|
+
else
|
60
|
+
promises = res.map do |ax_node|
|
61
|
+
context.send(:async_adopt_backend_node_id, ax_node['backendDOMNodeId'])
|
62
|
+
end
|
63
|
+
await_all(*promises)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def query_all_array(element, selector)
|
68
|
+
element_handles = query_all(element, selector)
|
69
|
+
element.execution_context.evaluate_handle('(...elements) => elements', *element_handles)
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class Puppeteer::CustomQueryHandler
|
2
|
+
# @param query_one [String] JS function (element: Element | Document, selector: string) => Element | null;
|
3
|
+
# @param query_all [String] JS function (element: Element | Document, selector: string) => Element[] | NodeListOf<Element>;
|
4
|
+
def initialize(query_one: nil, query_all: nil)
|
5
|
+
@query_one = query_one
|
6
|
+
@query_all = query_all
|
7
|
+
end
|
8
|
+
|
9
|
+
def query_one(element, selector)
|
10
|
+
unless @query_one
|
11
|
+
raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
|
12
|
+
end
|
13
|
+
|
14
|
+
handle = element.evaluate_handle(@query_one, selector)
|
15
|
+
element = handle.as_element
|
16
|
+
|
17
|
+
if element
|
18
|
+
return element
|
19
|
+
end
|
20
|
+
handle.dispose
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil)
|
25
|
+
unless @query_one
|
26
|
+
raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
|
27
|
+
end
|
28
|
+
|
29
|
+
dom_world.send(:wait_for_selector_in_page, @query_one, selector, visible: visible, hidden: hidden, timeout: timeout)
|
30
|
+
end
|
31
|
+
|
32
|
+
def query_all(element, selector)
|
33
|
+
unless @query_all
|
34
|
+
raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
|
35
|
+
end
|
36
|
+
|
37
|
+
handles = element.evaluate_handle(@query_all, selector)
|
38
|
+
properties = handles.properties
|
39
|
+
handles.dispose
|
40
|
+
properties.values.map(&:as_element).compact
|
41
|
+
end
|
42
|
+
|
43
|
+
def query_all_array(element, selector)
|
44
|
+
unless @query_all
|
45
|
+
raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
|
46
|
+
end
|
47
|
+
|
48
|
+
handles = element.evaluate_handle(@query_all, selector)
|
49
|
+
handles.evaluate_handle('(res) => Array.from(res)')
|
50
|
+
end
|
51
|
+
end
|
data/lib/puppeteer/dom_world.rb
CHANGED
@@ -4,6 +4,47 @@ require 'thread'
|
|
4
4
|
class Puppeteer::DOMWorld
|
5
5
|
using Puppeteer::DefineAsyncMethod
|
6
6
|
|
7
|
+
class BindingFunction
|
8
|
+
def initialize(name:, proc:)
|
9
|
+
@name = name
|
10
|
+
@proc = proc
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(*args)
|
14
|
+
@proc.call(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :name
|
18
|
+
|
19
|
+
def page_binding_init_string
|
20
|
+
<<~JAVASCRIPT
|
21
|
+
(type, bindingName) => {
|
22
|
+
/* Cast window to any here as we're about to add properties to it
|
23
|
+
* via win[bindingName] which TypeScript doesn't like.
|
24
|
+
*/
|
25
|
+
const win = window;
|
26
|
+
const binding = win[bindingName];
|
27
|
+
|
28
|
+
win[bindingName] = (...args) => {
|
29
|
+
const me = window[bindingName];
|
30
|
+
let callbacks = me.callbacks;
|
31
|
+
if (!callbacks) {
|
32
|
+
callbacks = new Map();
|
33
|
+
me.callbacks = callbacks;
|
34
|
+
}
|
35
|
+
const seq = (me.lastSeq || 0) + 1;
|
36
|
+
me.lastSeq = seq;
|
37
|
+
const promise = new Promise((resolve, reject) =>
|
38
|
+
callbacks.set(seq, { resolve, reject })
|
39
|
+
);
|
40
|
+
binding(JSON.stringify({ type, name: bindingName, seq, args }));
|
41
|
+
return promise;
|
42
|
+
};
|
43
|
+
}
|
44
|
+
JAVASCRIPT
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
7
48
|
# @param {!Puppeteer.FrameManager} frameManager
|
8
49
|
# @param {!Puppeteer.Frame} frame
|
9
50
|
# @param {!Puppeteer.TimeoutSettings} timeoutSettings
|
@@ -13,19 +54,29 @@ class Puppeteer::DOMWorld
|
|
13
54
|
@timeout_settings = timeout_settings
|
14
55
|
@context_promise = resolvable_future
|
15
56
|
@wait_tasks = Set.new
|
57
|
+
@bound_functions = {}
|
58
|
+
@ctx_bindings = Set.new
|
16
59
|
@detached = false
|
60
|
+
|
61
|
+
frame_manager.client.on_event('Runtime.bindingCalled', &method(:handle_binding_called))
|
17
62
|
end
|
18
63
|
|
19
64
|
attr_reader :frame
|
20
65
|
|
21
66
|
# only used in Puppeteer::WaitTask#initialize
|
22
|
-
def _wait_tasks
|
67
|
+
private def _wait_tasks
|
23
68
|
@wait_tasks
|
24
69
|
end
|
25
70
|
|
71
|
+
# only used in Puppeteer::WaitTask#initialize
|
72
|
+
private def _bound_functions
|
73
|
+
@bound_functions
|
74
|
+
end
|
75
|
+
|
26
76
|
# @param context [Puppeteer::ExecutionContext]
|
27
77
|
def context=(context)
|
28
78
|
if context
|
79
|
+
@ctx_bindings.clear
|
29
80
|
unless @context_promise.resolved?
|
30
81
|
@context_promise.fulfill(context)
|
31
82
|
end
|
@@ -378,61 +429,127 @@ class Puppeteer::DOMWorld
|
|
378
429
|
# @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
|
379
430
|
# @param timeout [Integer]
|
380
431
|
def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
|
381
|
-
|
432
|
+
# call wait_for_selector_in_page with custom query selector.
|
433
|
+
query_selector_manager = Puppeteer::QueryHandlerManager.instance
|
434
|
+
query_selector_manager.detect_query_handler(selector).wait_for(self, visible: visible, hidden: hidden, timeout: timeout)
|
382
435
|
end
|
383
436
|
|
384
|
-
|
385
|
-
|
386
|
-
# @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
|
387
|
-
# @param timeout [Integer]
|
388
|
-
def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
|
389
|
-
wait_for_selector_or_xpath(xpath, true, visible: visible, hidden: hidden, timeout: timeout)
|
437
|
+
private def binding_identifier(name, context)
|
438
|
+
"#{name}_#{context.send(:_context_id)}"
|
390
439
|
end
|
391
440
|
|
392
|
-
# /**
|
393
|
-
# * @param {Function|string} pageFunction
|
394
|
-
# * @param {!{polling?: string|number, timeout?: number}=} options
|
395
|
-
# * @return {!Promise<!Puppeteer.JSHandle>}
|
396
|
-
# */
|
397
|
-
# waitForFunction(pageFunction, options = {}, ...args) {
|
398
|
-
# const {
|
399
|
-
# polling = 'raf',
|
400
|
-
# timeout = this._timeoutSettings.timeout(),
|
401
|
-
# } = options;
|
402
|
-
# return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
|
403
|
-
# }
|
404
441
|
|
405
|
-
|
406
|
-
|
407
|
-
|
442
|
+
def add_binding_to_context(context, binding_function)
|
443
|
+
return if @ctx_bindings.include?(binding_identifier(binding_function.name, context))
|
444
|
+
|
445
|
+
expression = binding_function.page_binding_init_string
|
446
|
+
begin
|
447
|
+
context.client.send_message('Runtime.addBinding',
|
448
|
+
name: binding_function.name,
|
449
|
+
executionContextName: context.send(:_context_name))
|
450
|
+
context.evaluate(expression, 'internal', binding_function.name)
|
451
|
+
rescue => err
|
452
|
+
# We could have tried to evaluate in a context which was already
|
453
|
+
# destroyed. This happens, for example, if the page is navigated while
|
454
|
+
# we are trying to add the binding
|
455
|
+
allowed = [
|
456
|
+
'Execution context was destroyed',
|
457
|
+
'Cannot find context with specified id',
|
458
|
+
]
|
459
|
+
if allowed.any? { |msg| err.message.include?(msg) }
|
460
|
+
# ignore
|
461
|
+
else
|
462
|
+
raise
|
463
|
+
end
|
464
|
+
end
|
465
|
+
@ctx_bindings << binding_identifier(binding_function.name, context)
|
466
|
+
end
|
467
|
+
|
468
|
+
private def handle_binding_called(event)
|
469
|
+
return unless has_context?
|
470
|
+
payload = JSON.parse(event['payload']) rescue nil
|
471
|
+
name = payload['name']
|
472
|
+
args = payload['args']
|
473
|
+
|
474
|
+
# The binding was either called by something in the page or it was
|
475
|
+
# called before our wrapper was initialized.
|
476
|
+
return unless payload
|
477
|
+
return unless payload['type'] == 'internal'
|
478
|
+
context = execution_context
|
479
|
+
return unless @ctx_bindings.include?(binding_identifier(name, context))
|
480
|
+
return unless context.send(:_context_id) == event['executionContextId']
|
481
|
+
|
482
|
+
result = @bound_functions[name].call(*args)
|
483
|
+
deliver_result_js = <<~JAVASCRIPT
|
484
|
+
(name, seq, result) => {
|
485
|
+
globalThis[name].callbacks.get(seq).resolve(result);
|
486
|
+
globalThis[name].callbacks.delete(seq);
|
487
|
+
}
|
488
|
+
JAVASCRIPT
|
489
|
+
|
490
|
+
begin
|
491
|
+
context.evaluate(deliver_result_js, name, payload['seq'], result)
|
492
|
+
rescue => err
|
493
|
+
# The WaitTask may already have been resolved by timing out, or the
|
494
|
+
# exection context may have been destroyed.
|
495
|
+
# In both caes, the promises above are rejected with a protocol error.
|
496
|
+
# We can safely ignores these, as the WaitTask is re-installed in
|
497
|
+
# the next execution context if needed.
|
498
|
+
return if err.message.include?('Protocol error')
|
499
|
+
raise
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
# @param query_one [String] JS function (element: Element | Document, selector: string) => Element | null;
|
504
|
+
# @param selector [String]
|
505
|
+
# @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
|
506
|
+
# @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
|
408
507
|
# @param timeout [Integer]
|
409
|
-
|
410
|
-
|
411
|
-
|
508
|
+
private def wait_for_selector_in_page(query_one, selector, visible: nil, hidden: nil, timeout: nil, binding_function: nil)
|
509
|
+
option_wait_for_visible = visible || false
|
510
|
+
option_wait_for_hidden = hidden || false
|
412
511
|
option_timeout = timeout || @timeout_settings.timeout
|
413
512
|
|
414
|
-
|
513
|
+
polling =
|
514
|
+
if option_wait_for_visible || option_wait_for_hidden
|
515
|
+
'raf'
|
516
|
+
else
|
517
|
+
'mutation'
|
518
|
+
end
|
519
|
+
title = "selector #{selector}#{option_wait_for_hidden ? 'to be hidden' : ''}"
|
520
|
+
|
521
|
+
selector_predicate = make_predicate_string(
|
522
|
+
predicate_arg_def: '(selector, waitForVisible, waitForHidden)',
|
523
|
+
predicate_query_handler: query_one,
|
524
|
+
async: true,
|
525
|
+
predicate_body: <<~JAVASCRIPT
|
526
|
+
const node = await predicateQueryHandler(document, selector)
|
527
|
+
return checkWaitForOptions(node, waitForVisible, waitForHidden);
|
528
|
+
JAVASCRIPT
|
529
|
+
)
|
530
|
+
|
531
|
+
wait_task = Puppeteer::WaitTask.new(
|
415
532
|
dom_world: self,
|
416
|
-
predicate_body:
|
417
|
-
title:
|
418
|
-
polling:
|
533
|
+
predicate_body: selector_predicate,
|
534
|
+
title: title,
|
535
|
+
polling: polling,
|
419
536
|
timeout: option_timeout,
|
420
|
-
args:
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
537
|
+
args: [selector, option_wait_for_visible, option_wait_for_hidden],
|
538
|
+
binding_function: binding_function,
|
539
|
+
)
|
540
|
+
handle = wait_task.await_promise
|
541
|
+
unless handle.as_element
|
542
|
+
handle.dispose
|
543
|
+
return nil
|
544
|
+
end
|
545
|
+
handle.as_element
|
428
546
|
end
|
429
547
|
|
430
|
-
# @param
|
431
|
-
# @param is_xpath [Boolean]
|
548
|
+
# @param xpath [String]
|
432
549
|
# @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
|
433
550
|
# @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
|
434
551
|
# @param timeout [Integer]
|
435
|
-
|
552
|
+
def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
|
436
553
|
option_wait_for_visible = visible || false
|
437
554
|
option_wait_for_hidden = hidden || false
|
438
555
|
option_timeout = timeout || @timeout_settings.timeout
|
@@ -443,15 +560,23 @@ class Puppeteer::DOMWorld
|
|
443
560
|
else
|
444
561
|
'mutation'
|
445
562
|
end
|
446
|
-
title = "
|
563
|
+
title = "XPath #{xpath}#{option_wait_for_hidden ? 'to be hidden' : ''}"
|
564
|
+
|
565
|
+
xpath_predicate = make_predicate_string(
|
566
|
+
predicate_arg_def: '(selector, waitForVisible, waitForHidden)',
|
567
|
+
predicate_body: <<~JAVASCRIPT
|
568
|
+
const node = document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
569
|
+
return checkWaitForOptions(node, waitForVisible, waitForHidden);
|
570
|
+
JAVASCRIPT
|
571
|
+
)
|
447
572
|
|
448
573
|
wait_task = Puppeteer::WaitTask.new(
|
449
574
|
dom_world: self,
|
450
|
-
predicate_body:
|
575
|
+
predicate_body: xpath_predicate,
|
451
576
|
title: title,
|
452
577
|
polling: polling,
|
453
578
|
timeout: option_timeout,
|
454
|
-
args: [
|
579
|
+
args: [xpath, option_wait_for_visible, option_wait_for_hidden],
|
455
580
|
)
|
456
581
|
handle = wait_task.await_promise
|
457
582
|
unless handle.as_element
|
@@ -461,34 +586,66 @@ class Puppeteer::DOMWorld
|
|
461
586
|
handle.as_element
|
462
587
|
end
|
463
588
|
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
589
|
+
# @param page_function [String]
|
590
|
+
# @param args [Array]
|
591
|
+
# @param polling [Integer|String]
|
592
|
+
# @param timeout [Integer]
|
593
|
+
# @return [Puppeteer::JSHandle]
|
594
|
+
def wait_for_function(page_function, args: [], polling: nil, timeout: nil)
|
595
|
+
option_polling = polling || 'raf'
|
596
|
+
option_timeout = timeout || @timeout_settings.timeout
|
597
|
+
|
598
|
+
Puppeteer::WaitTask.new(
|
599
|
+
dom_world: self,
|
600
|
+
predicate_body: page_function,
|
601
|
+
title: 'function',
|
602
|
+
polling: option_polling,
|
603
|
+
timeout: option_timeout,
|
604
|
+
args: args,
|
605
|
+
).await_promise
|
606
|
+
end
|
607
|
+
|
608
|
+
|
609
|
+
# @return [String]
|
610
|
+
def title
|
611
|
+
evaluate('() => document.title')
|
612
|
+
end
|
613
|
+
|
614
|
+
private def make_predicate_string(predicate_arg_def:, predicate_body:, predicate_query_handler: nil, async: false)
|
615
|
+
predicate_query_handler_string =
|
616
|
+
if predicate_query_handler
|
617
|
+
"const predicateQueryHandler = #{predicate_query_handler}"
|
618
|
+
else
|
619
|
+
""
|
620
|
+
end
|
621
|
+
|
622
|
+
<<~JAVASCRIPT
|
623
|
+
#{async ? 'async ' : ''}function _#{predicate_arg_def} {
|
624
|
+
#{predicate_query_handler_string}
|
625
|
+
#{predicate_body}
|
626
|
+
|
627
|
+
function checkWaitForOptions(node, waitForVisible, waitForHidden) {
|
628
|
+
if (!node) return waitForHidden;
|
629
|
+
if (!waitForVisible && !waitForHidden) return node;
|
630
|
+
const element =
|
631
|
+
node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
|
632
|
+
|
633
|
+
const style = window.getComputedStyle(element);
|
634
|
+
const isVisible =
|
635
|
+
style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
|
636
|
+
const success =
|
637
|
+
waitForVisible === isVisible || waitForHidden === !isVisible;
|
638
|
+
return success ? node : null;
|
639
|
+
|
640
|
+
/**
|
641
|
+
* @return {boolean}
|
642
|
+
*/
|
643
|
+
function hasVisibleBoundingBox() {
|
644
|
+
const rect = element.getBoundingClientRect();
|
645
|
+
return !!(rect.top || rect.bottom || rect.width || rect.height);
|
646
|
+
}
|
647
|
+
}
|
648
|
+
}
|
649
|
+
JAVASCRIPT
|
650
|
+
end
|
494
651
|
end
|
@@ -314,32 +314,20 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
|
|
314
314
|
end
|
315
315
|
end
|
316
316
|
|
317
|
+
private def query_handler_manager
|
318
|
+
Puppeteer::QueryHandlerManager.instance
|
319
|
+
end
|
320
|
+
|
317
321
|
# `$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
|
318
322
|
# @param selector [String]
|
319
323
|
def S(selector)
|
320
|
-
|
321
|
-
'(element, selector) => element.querySelector(selector)',
|
322
|
-
selector,
|
323
|
-
)
|
324
|
-
element = handle.as_element
|
325
|
-
|
326
|
-
if element
|
327
|
-
return element
|
328
|
-
end
|
329
|
-
handle.dispose
|
330
|
-
nil
|
324
|
+
query_handler_manager.detect_query_handler(selector).query_one(self)
|
331
325
|
end
|
332
326
|
|
333
327
|
# `$$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
|
334
328
|
# @param selector [String]
|
335
329
|
def SS(selector)
|
336
|
-
|
337
|
-
'(element, selector) => element.querySelectorAll(selector)',
|
338
|
-
selector,
|
339
|
-
)
|
340
|
-
properties = handles.properties
|
341
|
-
handles.dispose
|
342
|
-
properties.values.map(&:as_element).compact
|
330
|
+
query_handler_manager.detect_query_handler(selector).query_all(self)
|
343
331
|
end
|
344
332
|
|
345
333
|
class ElementNotFoundError < StandardError
|
@@ -370,10 +358,7 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
|
|
370
358
|
# @param page_function [String]
|
371
359
|
# @return [Object]
|
372
360
|
def SSeval(selector, page_function, *args)
|
373
|
-
handles =
|
374
|
-
'(element, selector) => Array.from(element.querySelectorAll(selector))',
|
375
|
-
selector,
|
376
|
-
)
|
361
|
+
handles = query_handler_manager.detect_query_handler(selector).query_all_array(self)
|
377
362
|
result = handles.evaluate(page_function, *args)
|
378
363
|
handles.dispose
|
379
364
|
|
@@ -430,4 +415,10 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
|
|
430
415
|
# https://en.wikipedia.org/wiki/Polygon#Simple_polygons
|
431
416
|
quad.zip(quad.rotate).map { |p1, p2| (p1.x * p2.y - p2.x * p1.y) / 2 }.reduce(:+).abs
|
432
417
|
end
|
418
|
+
|
419
|
+
# used in AriaQueryHandler
|
420
|
+
def query_ax_tree(accessible_name: nil, role: nil)
|
421
|
+
@remote_object.query_ax_tree(@client,
|
422
|
+
accessible_name: accessible_name, role: role)
|
423
|
+
end
|
433
424
|
end
|
@@ -12,10 +12,21 @@ class Puppeteer::ExecutionContext
|
|
12
12
|
@client = client
|
13
13
|
@world = world
|
14
14
|
@context_id = context_payload['id']
|
15
|
+
@context_name = context_payload['name']
|
15
16
|
end
|
16
17
|
|
17
18
|
attr_reader :client, :world
|
18
19
|
|
20
|
+
# only used in DOMWorld
|
21
|
+
private def _context_id
|
22
|
+
@context_id
|
23
|
+
end
|
24
|
+
|
25
|
+
# only used in DOMWorld::BindingFunction#add_binding_to_context
|
26
|
+
private def _context_name
|
27
|
+
@context_name
|
28
|
+
end
|
29
|
+
|
19
30
|
# @return [Puppeteer::Frame]
|
20
31
|
def frame
|
21
32
|
if_present(@world) do |world|
|
@@ -223,6 +234,7 @@ class Puppeteer::ExecutionContext
|
|
223
234
|
remote_object: Puppeteer::RemoteObject.new(response["object"]),
|
224
235
|
)
|
225
236
|
end
|
237
|
+
private define_async_method :async_adopt_backend_node_id
|
226
238
|
|
227
239
|
# @param element_handle [Puppeteer::ElementHandle]
|
228
240
|
# @return [Puppeteer::ElementHandle]
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
class Puppeteer::QueryHandlerManager
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
def query_handlers
|
7
|
+
@query_handlers ||= {
|
8
|
+
aria: Puppeteer::AriaQueryHandler.new,
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
private def default_handler
|
13
|
+
@default_handler ||= Puppeteer::CustomQueryHandler.new(
|
14
|
+
query_one: '(element, selector) => element.querySelector(selector)',
|
15
|
+
query_all: '(element, selector) => element.querySelectorAll(selector)',
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
class Result
|
20
|
+
def initialize(query_handler:, selector:)
|
21
|
+
@query_handler = query_handler
|
22
|
+
@selector = selector
|
23
|
+
end
|
24
|
+
|
25
|
+
def query_one(element_handle)
|
26
|
+
@query_handler.query_one(element_handle, @selector)
|
27
|
+
end
|
28
|
+
|
29
|
+
def wait_for(dom_world, visible:, hidden:, timeout:)
|
30
|
+
@query_handler.wait_for(dom_world, @selector, visible: visible, hidden: hidden, timeout: timeout)
|
31
|
+
end
|
32
|
+
|
33
|
+
def query_all(element_handle)
|
34
|
+
@query_handler.query_all(element_handle, @selector)
|
35
|
+
end
|
36
|
+
|
37
|
+
def query_all_array(element_handle)
|
38
|
+
@query_handler.query_all_array(element_handle, @selector)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def detect_query_handler(selector)
|
43
|
+
unless /^[a-zA-Z]+\// =~ selector
|
44
|
+
return Result.new(
|
45
|
+
query_handler: default_handler,
|
46
|
+
selector: selector,
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
chunk = selector.split("/")
|
51
|
+
name = chunk.shift
|
52
|
+
updated_selector = chunk.join("/")
|
53
|
+
|
54
|
+
query_handler = query_handlers[name.to_sym]
|
55
|
+
|
56
|
+
unless query_handler
|
57
|
+
raise ArgumentError.new("Query set to use \"#{name}\", but no query handler of that name was found")
|
58
|
+
end
|
59
|
+
|
60
|
+
Result.new(
|
61
|
+
query_handler: query_handler,
|
62
|
+
selector: updated_selector,
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
@@ -97,6 +97,18 @@ class Puppeteer::RemoteObject
|
|
97
97
|
nil
|
98
98
|
end
|
99
99
|
|
100
|
+
# used in ElementHandle#query_ax_tree
|
101
|
+
def query_ax_tree(client, accessible_name: nil, role: nil)
|
102
|
+
result = client.send_message('Accessibility.queryAXTree', {
|
103
|
+
objectId: @object_id,
|
104
|
+
accessibleName: accessible_name,
|
105
|
+
role: role,
|
106
|
+
}.compact)
|
107
|
+
|
108
|
+
result['nodes'].reject do |node|
|
109
|
+
node['role']['value'] == 'text'
|
110
|
+
end
|
111
|
+
end
|
100
112
|
|
101
113
|
# helper#valueFromRemoteObject
|
102
114
|
def value
|
data/lib/puppeteer/version.rb
CHANGED
data/lib/puppeteer/wait_task.rb
CHANGED
@@ -9,7 +9,7 @@ class Puppeteer::WaitTask
|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
def initialize(dom_world:, predicate_body:, title:, polling:, timeout:, args: [])
|
12
|
+
def initialize(dom_world:, predicate_body:, title:, polling:, timeout:, args: [], binding_function: nil)
|
13
13
|
if polling.is_a?(String)
|
14
14
|
if polling != 'raf' && polling != 'mutation'
|
15
15
|
raise ArgumentError.new("Unknown polling option: #{polling}")
|
@@ -27,8 +27,12 @@ class Puppeteer::WaitTask
|
|
27
27
|
@timeout = timeout
|
28
28
|
@predicate_body = "return (#{predicate_body})(...args);"
|
29
29
|
@args = args
|
30
|
+
@binding_function = binding_function
|
30
31
|
@run_count = 0
|
31
|
-
@dom_world._wait_tasks.add(self)
|
32
|
+
@dom_world.send(:_wait_tasks).add(self)
|
33
|
+
if binding_function
|
34
|
+
@dom_world.send(:_bound_functions)[binding_function.name] = binding_function
|
35
|
+
end
|
32
36
|
@promise = resolvable_future
|
33
37
|
|
34
38
|
# Since page navigation requires us to re-install the pageScript, we should track
|
@@ -53,8 +57,16 @@ class Puppeteer::WaitTask
|
|
53
57
|
|
54
58
|
def rerun
|
55
59
|
run_count = (@run_count += 1)
|
60
|
+
context = @dom_world.execution_context
|
61
|
+
|
62
|
+
return if @terminated || run_count != @run_count
|
63
|
+
if @binding_function
|
64
|
+
@dom_world.add_binding_to_context(context, @binding_function)
|
65
|
+
end
|
66
|
+
return if @terminated || run_count != @run_count
|
67
|
+
|
56
68
|
begin
|
57
|
-
success =
|
69
|
+
success = context.evaluate_handle(
|
58
70
|
WAIT_FOR_PREDICATE_PAGE_FUNCTION,
|
59
71
|
@predicate_body,
|
60
72
|
@polling,
|
@@ -103,7 +115,7 @@ class Puppeteer::WaitTask
|
|
103
115
|
|
104
116
|
private def cleanup
|
105
117
|
@timeout_cleared = true
|
106
|
-
@dom_world._wait_tasks.delete(self)
|
118
|
+
@dom_world.send(:_wait_tasks).delete(self)
|
107
119
|
end
|
108
120
|
|
109
121
|
private define_async_method :async_rerun
|
data/puppeteer-ruby.gemspec
CHANGED
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_development_dependency 'rake', '~> 13.0.3'
|
28
28
|
spec.add_development_dependency 'rspec', '~> 3.10.0 '
|
29
29
|
spec.add_development_dependency 'rspec_junit_formatter' # for CircleCI.
|
30
|
-
spec.add_development_dependency 'rubocop', '~> 1.
|
30
|
+
spec.add_development_dependency 'rubocop', '~> 1.10.0'
|
31
31
|
spec.add_development_dependency 'rubocop-rspec'
|
32
32
|
spec.add_development_dependency 'sinatra'
|
33
33
|
spec.add_development_dependency 'webrick'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: puppeteer-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.29.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- YusukeIwaki
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-02-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -142,14 +142,14 @@ dependencies:
|
|
142
142
|
requirements:
|
143
143
|
- - "~>"
|
144
144
|
- !ruby/object:Gem::Version
|
145
|
-
version: 1.
|
145
|
+
version: 1.10.0
|
146
146
|
type: :development
|
147
147
|
prerelease: false
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
149
149
|
requirements:
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
|
-
version: 1.
|
152
|
+
version: 1.10.0
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
154
|
name: rubocop-rspec
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -215,6 +215,7 @@ extra_rdoc_files: []
|
|
215
215
|
files:
|
216
216
|
- ".circleci/config.yml"
|
217
217
|
- ".github/ISSUE_TEMPLATE/bug_report.md"
|
218
|
+
- ".github/ISSUE_TEMPLATE/feature_request.md"
|
218
219
|
- ".github/stale.yml"
|
219
220
|
- ".github/workflows/docs.yml"
|
220
221
|
- ".github/workflows/reviewdog.yml"
|
@@ -231,6 +232,7 @@ files:
|
|
231
232
|
- bin/setup
|
232
233
|
- docker-compose.yml
|
233
234
|
- lib/puppeteer.rb
|
235
|
+
- lib/puppeteer/aria_query_handler.rb
|
234
236
|
- lib/puppeteer/browser.rb
|
235
237
|
- lib/puppeteer/browser_context.rb
|
236
238
|
- lib/puppeteer/browser_fetcher.rb
|
@@ -239,6 +241,7 @@ files:
|
|
239
241
|
- lib/puppeteer/concurrent_ruby_utils.rb
|
240
242
|
- lib/puppeteer/connection.rb
|
241
243
|
- lib/puppeteer/console_message.rb
|
244
|
+
- lib/puppeteer/custom_query_handler.rb
|
242
245
|
- lib/puppeteer/debug_print.rb
|
243
246
|
- lib/puppeteer/define_async_method.rb
|
244
247
|
- lib/puppeteer/device.rb
|
@@ -279,6 +282,7 @@ files:
|
|
279
282
|
- lib/puppeteer/page/pdf_options.rb
|
280
283
|
- lib/puppeteer/page/screenshot_options.rb
|
281
284
|
- lib/puppeteer/page/screenshot_task_queue.rb
|
285
|
+
- lib/puppeteer/query_handler_manager.rb
|
282
286
|
- lib/puppeteer/remote_object.rb
|
283
287
|
- lib/puppeteer/request.rb
|
284
288
|
- lib/puppeteer/response.rb
|