puppeteer-ruby 0.43.0 → 0.44.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da17d477ad97a3197fa0eacf237e784204439efdca104e2a304d5cfb58c449d3
4
- data.tar.gz: f56c151a8d7ebd8ac0ff71feafe5a90dcdd14ed997aaa9ea8e198ddd345b93dd
3
+ metadata.gz: 6ade25ba8f8e99240ed29fdad9300af7b65cc12ddf09e45c43f7b672c6324ed4
4
+ data.tar.gz: 97d3bfc48b3a2e3729dd8857c2bb6deca7babd4d0b055195f2dbe7c01c941e97
5
5
  SHA512:
6
- metadata.gz: 3db33eea6388dd6c743395c9e3cd583d95a3f2147627ae371390e40bf6269f32d689687e8adf0fdaef8b99134b6bad2ee55061ca66c037407ec681ee62b61188
7
- data.tar.gz: 2b169890fd8c5dde6850fff2439e5d43c03fa1b65e906ce443389c0599cf31d0e88af1ca0203c12cff90b2789c14af7a992d1c6b95caf7e78b9dadbf4d4a0f09
6
+ metadata.gz: 891d8c29ac63d25cdb34178024d7a9202675d77b121a903e24a51e9465e46a9b7a0a9b70a2d893d9b274724b5547d5bb90e133aa2c217052f3f523f952e1a0a3
7
+ data.tar.gz: eea1fb6ff2f179208932a3bb84ca96841b1b08ff588d914e88ace6ad9e31f71c567c21321e3376b019a265122e51a4352b3f0039dd6bea770dba2a383efe82e5
data/CHANGELOG.md CHANGED
@@ -1,7 +1,17 @@
1
- ### main [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.43.0...main)]
1
+ ### main [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.44.0...main)]
2
2
 
3
3
  - xxx
4
4
 
5
+ ### 0.44.0 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.43.1...0.44.0)]
6
+
7
+ - Port Puppeteer v17.0-v17.1 features.
8
+ - `wait_for_selector` no longer accept `root` parameter.
9
+
10
+
11
+ ### 0.43.1 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.43.0...0.43.1)]
12
+
13
+ - Port Puppeteer v16.1 features, including bugfix and XPath query handler.
14
+
5
15
  ### 0.43.0 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.42.0...0.43.0)]
6
16
 
7
17
  - Port Puppeteer v16.0 features. Increasing stability.
data/docs/api_coverage.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # API coverages
2
- - Puppeteer version: v16.2.0
3
- - puppeteer-ruby version: 0.43.0
2
+ - Puppeteer version: v17.1.3
3
+ - puppeteer-ruby version: 0.44.0
4
4
 
5
5
  ## Puppeteer
6
6
 
@@ -150,13 +150,6 @@
150
150
  * ~~removeAllListeners~~
151
151
  * ~~removeListener~~
152
152
 
153
- ## ExecutionContext
154
-
155
- * evaluate
156
- * evaluateHandle => `#evaluate_handle`
157
- * frame
158
- * ~~queryObjects~~
159
-
160
153
  ## FileChooser
161
154
 
162
155
  * accept
@@ -172,12 +165,12 @@
172
165
  * $x => `#Sx`
173
166
  * addScriptTag => `#add_script_tag`
174
167
  * addStyleTag => `#add_style_tag`
168
+ * addStyleTag => `#add_style_tag`
175
169
  * childFrames => `#child_frames`
176
170
  * click
177
171
  * content
178
172
  * evaluate
179
173
  * evaluateHandle => `#evaluate_handle`
180
- * executionContext => `#execution_context`
181
174
  * focus
182
175
  * goto
183
176
  * hover
@@ -251,7 +244,6 @@
251
244
  * dispose
252
245
  * evaluate
253
246
  * evaluateHandle => `#evaluate_handle`
254
- * executionContext => `#execution_context`
255
247
  * getProperties => `#properties`
256
248
  * getProperty => `#[]`
257
249
  * getProperty => `#[]`
@@ -289,6 +281,7 @@
289
281
  * $x => `#Sx`
290
282
  * addScriptTag => `#add_script_tag`
291
283
  * addStyleTag => `#add_style_tag`
284
+ * addStyleTag => `#add_style_tag`
292
285
  * authenticate
293
286
  * bringToFront => `#bring_to_front`
294
287
  * browser
@@ -313,6 +306,7 @@
313
306
  * exposeFunction => `#expose_function`
314
307
  * focus
315
308
  * frames
309
+ * ~~getDefaultTimeout~~
316
310
  * goBack => `#go_back`
317
311
  * goForward => `#go_forward`
318
312
  * goto
@@ -401,5 +395,4 @@
401
395
 
402
396
  * ~~evaluate~~
403
397
  * ~~evaluateHandle~~
404
- * ~~executionContext~~
405
398
  * ~~url~~
@@ -25,43 +25,83 @@ class Puppeteer::AriaQueryHandler
25
25
  query_options
26
26
  end
27
27
 
28
- def query_one(element, selector)
29
- context = element.execution_context
28
+ # @param element [Puppeteer::ElementHandle]
29
+ # @param selector [String]
30
+ private def query_one_id(element, selector)
30
31
  parse_result = parse_aria_selector(selector)
31
32
  res = element.query_ax_tree(accessible_name: parse_result[:name], role: parse_result[:role])
32
- if res.empty?
33
+
34
+ if res.first.is_a?(Hash)
35
+ res.first['backendDOMNodeId']
36
+ else
33
37
  nil
38
+ end
39
+ end
40
+
41
+ def query_one(element, selector)
42
+ id = query_one_id(element, selector)
43
+
44
+ if id
45
+ element.frame.main_world.adopt_backend_node(id)
34
46
  else
35
- context.adopt_backend_node_id(res.first['backendDOMNodeId'])
47
+ nil
36
48
  end
37
49
  end
38
50
 
39
- def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil, root: nil)
51
+ def wait_for(element_or_frame, selector, visible: nil, hidden: nil, timeout: nil)
52
+ case element_or_frame
53
+ when Puppeteer::Frame
54
+ frame = element_or_frame
55
+ element = nil
56
+ when Puppeteer::ElementHandle
57
+ frame = element_or_frame.frame
58
+ element = frame.puppeteer_world.adopt_handle(element_or_frame)
59
+ else
60
+ raise ArgumentError.new("element_or_frame must be a Frame or ElementHandle. #{element_or_frame.inspect}")
61
+ end
62
+
40
63
  # addHandlerToWorld
41
- binding_function = Puppeteer::DOMWorld::BindingFunction.new(
64
+ binding_function = Puppeteer::IsolaatedWorld::BindingFunction.new(
42
65
  name: 'ariaQuerySelector',
43
- proc: -> (sel) { query_one(root || dom_world.send(:document), sel) },
66
+ proc: -> (sel) {
67
+ id = query_one_id(element || frame.puppeteer_world.document, sel)
68
+
69
+ if id
70
+ frame.puppeteer_world.adopt_backend_node(id)
71
+ else
72
+ nil
73
+ end
74
+ },
44
75
  )
45
- dom_world.send(:wait_for_selector_in_page,
76
+ result = frame.puppeteer_world.send(:wait_for_selector_in_page,
46
77
  '(_, selector) => globalThis.ariaQuerySelector(selector)',
78
+ element,
47
79
  selector,
48
80
  visible: visible,
49
81
  hidden: hidden,
50
82
  timeout: timeout,
51
83
  binding_function: binding_function,
52
- root: root,
53
84
  )
85
+
86
+ element&.dispose
87
+
88
+ if result.is_a?(Puppeteer::ElementHandle)
89
+ result.frame.main_world.transfer_handle(result)
90
+ else
91
+ result&.dispose
92
+ nil
93
+ end
54
94
  end
55
95
 
56
96
  def query_all(element, selector)
57
- context = element.execution_context
97
+ world = element.frame.main_world
58
98
  parse_result = parse_aria_selector(selector)
59
99
  res = element.query_ax_tree(accessible_name: parse_result[:name], role: parse_result[:role])
60
100
  if res.empty?
61
101
  nil
62
102
  else
63
103
  promises = res.map do |ax_node|
64
- context.send(:async_adopt_backend_node_id, ax_node['backendDOMNodeId'])
104
+ world.send(:async_adopt_backend_node, ax_node['backendDOMNodeId'])
65
105
  end
66
106
  await_all(*promises)
67
107
  end
@@ -196,7 +196,7 @@ class Puppeteer::Browser
196
196
  session: session,
197
197
  browser_context: context,
198
198
  target_manager: @target_manager,
199
- session_factory: -> { @connection.create_session(target_info) },
199
+ session_factory: -> (auto_attach_emulated) { @connection.create_session(target_info, auto_attach_emulated: auto_attach_emulated) },
200
200
  ignore_https_errors: @ignore_https_errors,
201
201
  default_viewport: @default_viewport,
202
202
  is_page_target_callback: @is_page_target_callback,
@@ -1,4 +1,5 @@
1
1
  class Puppeteer::ChromeTargetManager
2
+ include Puppeteer::DebugPrint
2
3
  include Puppeteer::EventCallbackable
3
4
 
4
5
  def initialize(connection:, target_factory:, target_filter_callback:)
@@ -33,20 +34,34 @@ class Puppeteer::ChromeTargetManager
33
34
  )
34
35
 
35
36
  setup_attachment_listeners(@connection)
36
- @connection.async_send_message('Target.setDiscoverTargets', discover: true)
37
+ @connection.async_send_message('Target.setDiscoverTargets', {
38
+ discover: true,
39
+ filter: [
40
+ { type: 'tab', exclude: true },
41
+ {},
42
+ ],
43
+ }).then do
44
+ store_existing_targets_for_init
45
+ end.rescue do |err|
46
+ debug_puts(err)
47
+ end
37
48
  end
38
49
 
39
- def init
50
+ private def store_existing_targets_for_init
40
51
  @discovered_targets_by_target_id.each do |target_id, target_info|
41
- if @target_filter_callback.call(target_info)
52
+ if @target_filter_callback.call(target_info) && target_info.type != 'browser'
42
53
  @target_ids_for_init << target_id
43
54
  end
44
55
  end
56
+ end
57
+
58
+ def init
45
59
  @connection.send_message('Target.setAutoAttach', {
46
60
  waitForDebuggerOnStart: true,
47
61
  flatten: true,
48
62
  autoAttach: true,
49
63
  })
64
+ finish_initialization_if_ready
50
65
  @initialize_promise.value!
51
66
  end
52
67
 
@@ -114,7 +129,7 @@ class Puppeteer::ChromeTargetManager
114
129
  # Special case (https://crbug.com/1338156): currently, shared_workers
115
130
  # don't get auto-attached. This should be removed once the auto-attach
116
131
  # works.
117
- @connection.create_session(target_info)
132
+ @connection.create_session(target_info, auto_attach_emulated: true)
118
133
  end
119
134
  end
120
135
 
@@ -170,6 +185,8 @@ class Puppeteer::ChromeTargetManager
170
185
  end
171
186
  }
172
187
 
188
+ return unless @connection.auto_attached?(target_info.target_id)
189
+
173
190
  # Special case for service workers: being attached to service workers will
174
191
  # prevent them from ever being destroyed. Therefore, we silently detach
175
192
  # from service workers unless the connection was manually created via
@@ -197,6 +214,8 @@ class Puppeteer::ChromeTargetManager
197
214
  return
198
215
  end
199
216
 
217
+ is_existing_target = @attached_targets_by_target_id.has_key?(target_info.target_id)
218
+
200
219
  target = @attached_targets_by_target_id[target_info.target_id] || @target_factory.call(target_info, session)
201
220
  setup_attachment_listeners(session)
202
221
 
@@ -218,11 +237,10 @@ class Puppeteer::ChromeTargetManager
218
237
  end
219
238
 
220
239
  @target_ids_for_init.delete(target.target_id)
221
- future { emit_event(TargetManagerEmittedEvents::TargetAvailable, target) }
222
-
223
- if @target_ids_for_init.empty?
224
- @initialize_promise.fulfill(nil) unless @initialize_promise.resolved?
240
+ unless is_existing_target
241
+ future { emit_event(TargetManagerEmittedEvents::TargetAvailable, target) }
225
242
  end
243
+ finish_initialization_if_ready
226
244
 
227
245
  future do
228
246
  # TODO: the browser might be shutting down here. What do we do with the error?
@@ -239,8 +257,8 @@ class Puppeteer::ChromeTargetManager
239
257
  end
240
258
  end
241
259
 
242
- private def finish_initialization_if_ready(target_id)
243
- @target_ids_for_init.delete(target_id)
260
+ private def finish_initialization_if_ready(target_id = nil)
261
+ @target_ids_for_init.delete(target_id) if target_id
244
262
  if @target_ids_for_init.empty?
245
263
  @initialize_promise.fulfill(nil) unless @initialize_promise.resolved?
246
264
  end
@@ -319,15 +319,18 @@ class Puppeteer::Connection
319
319
  end
320
320
 
321
321
  def auto_attached?(target_id)
322
- @manually_attached.include?(target_id)
322
+ !@manually_attached.include?(target_id)
323
323
  end
324
324
 
325
325
  # @param {Protocol.Target.TargetInfo} targetInfo
326
326
  # @return [CDPSession]
327
- def create_session(target_info)
328
- @manually_attached << target_info.target_id
327
+ def create_session(target_info, auto_attach_emulated: false)
328
+ unless auto_attach_emulated
329
+ @manually_attached << target_info.target_id
330
+ end
329
331
  result = send_message('Target.attachToTarget', targetId: target_info.target_id, flatten: true)
330
332
  session_id = result['sessionId']
333
+ @manually_attached.delete(target_info.target_id)
331
334
  @sessions[session_id]
332
335
  end
333
336
  end
@@ -108,7 +108,7 @@ class Puppeteer::Coverage
108
108
 
109
109
  # Filter out empty ranges.
110
110
  results.select do |range|
111
- range[:end] - range[:start] > 1
111
+ range[:end] - range[:start] > 0
112
112
  end
113
113
  end
114
114
  end
@@ -21,12 +21,39 @@ class Puppeteer::CustomQueryHandler
21
21
  nil
22
22
  end
23
23
 
24
- def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil, root: nil)
24
+ def wait_for(element_or_frame, selector, visible: nil, hidden: nil, timeout: nil)
25
+ case element_or_frame
26
+ when Puppeteer::Frame
27
+ frame = element_or_frame
28
+ element = nil
29
+ when Puppeteer::ElementHandle
30
+ frame = element_or_frame.frame
31
+ element = frame.puppeteer_world.adopt_handle(element_or_frame)
32
+ else
33
+ raise ArgumentError.new("element_or_frame must be a Frame or ElementHandle. #{element_or_frame.inspect}")
34
+ end
35
+
25
36
  unless @query_one
26
37
  raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
27
38
  end
28
39
 
29
- dom_world.send(:wait_for_selector_in_page, @query_one, selector, visible: visible, hidden: hidden, timeout: timeout, root: root)
40
+ result = frame.puppeteer_world.send(:wait_for_selector_in_page,
41
+ @query_one,
42
+ element,
43
+ selector,
44
+ visible: visible,
45
+ hidden: hidden,
46
+ timeout: timeout,
47
+ )
48
+
49
+ element&.dispose
50
+
51
+ if result.is_a?(Puppeteer::ElementHandle)
52
+ result.frame.main_world.transfer_handle(result)
53
+ else
54
+ result&.dispose
55
+ nil
56
+ end
30
57
  end
31
58
 
32
59
  def query_all(element, selector)
@@ -12,16 +12,16 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
12
12
  # @param client [Puppeteer::CDPSession]
13
13
  # @param remote_object [Puppeteer::RemoteObject]
14
14
  # @param frame [Puppeteer::Frame]
15
- # @param page [Puppeteer::Page]
16
- # @param frame_manager [Puppeteer::FrameManager]
17
- def initialize(context:, client:, remote_object:, frame:, page:, frame_manager:)
15
+ def initialize(context:, client:, remote_object:, frame:)
18
16
  super(context: context, client: client, remote_object: remote_object)
19
17
  @frame = frame
20
- @page = page
21
- @frame_manager = frame_manager
18
+ @page = frame.page
19
+ @frame_manager = frame.frame_manager
22
20
  @disposed = false
23
21
  end
24
22
 
23
+ attr_reader :page, :frame, :frame_manager
24
+
25
25
  def inspect
26
26
  values = %i[context remote_object page disposed].map do |sym|
27
27
  value = instance_variable_get(:"@#{sym}")
@@ -60,18 +60,7 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
60
60
  # (30 seconds). Pass `0` to disable timeout. The default value can be changed
61
61
  # by using the {@link Page.setDefaultTimeout} method.
62
62
  def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
63
- frame = @context.frame
64
-
65
- secondary_world = frame.secondary_world
66
- adopted_root = secondary_world.execution_context.adopt_element_handle(self)
67
- handle = secondary_world.wait_for_selector(selector, visible: visible, hidden: hidden, timeout: timeout, root: adopted_root)
68
- adopted_root.dispose
69
- return nil unless handle
70
-
71
- main_world = frame.main_world
72
- result = main_world.execution_context.adopt_element_handle(handle)
73
- handle.dispose
74
- result
63
+ query_handler_manager.detect_query_handler(selector).wait_for(self, visible: visible, hidden: hidden, timeout: timeout)
75
64
  end
76
65
 
77
66
  define_async_method :async_wait_for_selector
@@ -125,28 +114,14 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
125
114
  # Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default
126
115
  # value can be changed by using the {@link Page.setDefaultTimeout} method.
127
116
  def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
128
- frame = @context.frame
129
-
130
- secondary_world = frame.secondary_world
131
- adopted_root = secondary_world.execution_context.adopt_element_handle(self)
132
117
  param_xpath =
133
118
  if xpath.start_with?('//')
134
119
  ".#{xpath}"
135
120
  else
136
121
  xpath
137
122
  end
138
- unless param_xpath.start_with?('.//')
139
- adopted_root.dispose
140
- raise ArgumentError.new("Unsupported xpath expression: #{xpath}")
141
- end
142
- handle = secondary_world.wait_for_xpath(param_xpath, visible: visible, hidden: hidden, timeout: timeout, root: adopted_root)
143
- adopted_root.dispose
144
- return nil unless handle
145
123
 
146
- main_world = frame.main_world
147
- result = main_world.execution_context.adopt_element_handle(handle)
148
- handle.dispose
149
- result
124
+ wait_for_selector("xpath/#{param_xpath}", visible: visible, hidden: hidden, timeout: timeout)
150
125
  end
151
126
 
152
127
  define_async_method :async_wait_for_xpath
@@ -623,21 +598,14 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
623
598
  # @param expression [String]
624
599
  # @return [Array<ElementHandle>]
625
600
  def Sx(expression)
626
- fn = <<~JAVASCRIPT
627
- (element, expression) => {
628
- const document = element.ownerDocument || element;
629
- const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
630
- const array = [];
631
- let item;
632
- while ((item = iterator.iterateNext()))
633
- array.push(item);
634
- return array;
635
- }
636
- JAVASCRIPT
637
- handles = evaluate_handle(fn, expression)
638
- properties = handles.properties
639
- handles.dispose
640
- properties.values.map(&:as_element).compact
601
+ param_xpath =
602
+ if expression.start_with?('//')
603
+ ".#{expression}"
604
+ else
605
+ expression
606
+ end
607
+
608
+ query_selector_all("xpath/#{param_xpath}")
641
609
  end
642
610
 
643
611
  define_async_method :async_Sx
@@ -674,6 +642,6 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
674
642
  # used in AriaQueryHandler
675
643
  def query_ax_tree(accessible_name: nil, role: nil)
676
644
  @remote_object.query_ax_tree(@client,
677
- accessible_name: accessible_name, role: role)
645
+ accessible_name: accessible_name, role: role)
678
646
  end
679
647
  end
@@ -7,7 +7,7 @@ class Puppeteer::ExecutionContext
7
7
 
8
8
  # @param client [Puppeteer::CDPSession]
9
9
  # @param context_payload [Hash]
10
- # @param world [Puppeteer::DOMWorld?]
10
+ # @param world [Puppeteer::IsolaatedWorld?]
11
11
  def initialize(client, context_payload, world)
12
12
  @client = client
13
13
  @world = world
@@ -17,23 +17,16 @@ class Puppeteer::ExecutionContext
17
17
 
18
18
  attr_reader :client, :world
19
19
 
20
- # only used in DOMWorld
20
+ # only used in IsolaatedWorld
21
21
  private def _context_id
22
22
  @context_id
23
23
  end
24
24
 
25
- # only used in DOMWorld::BindingFunction#add_binding_to_context
25
+ # only used in IsolaatedWorld::BindingFunction#add_binding_to_context
26
26
  private def _context_name
27
27
  @context_name
28
28
  end
29
29
 
30
- # @return [Puppeteer::Frame]
31
- def frame
32
- if_present(@world) do |world|
33
- world.frame
34
- end
35
- end
36
-
37
30
  # @param page_function [String]
38
31
  # @return [Object]
39
32
  def evaluate(page_function, *args)
@@ -208,54 +201,4 @@ class Puppeteer::ExecutionContext
208
201
  context_id: @context_id,
209
202
  )
210
203
  end
211
-
212
- # /**
213
- # * @param {!JSHandle} prototypeHandle
214
- # * @return {!Promise<!JSHandle>}
215
- # */
216
- # async queryObjects(prototypeHandle) {
217
- # assert(!prototypeHandle._disposed, 'Prototype JSHandle is disposed!');
218
- # assert(prototypeHandle._remoteObject.objectId, 'Prototype JSHandle must not be referencing primitive value');
219
- # const response = await this._client.send('Runtime.queryObjects', {
220
- # prototypeObjectId: prototypeHandle._remoteObject.objectId
221
- # });
222
- # return createJSHandle(this, response.objects);
223
- # }
224
-
225
- # @param backend_node_id [Integer]
226
- # @return [Puppeteer::ElementHandle]
227
- def adopt_backend_node_id(backend_node_id)
228
- response = @client.send_message('DOM.resolveNode',
229
- backendNodeId: backend_node_id,
230
- executionContextId: @context_id,
231
- )
232
- Puppeteer::JSHandle.create(
233
- context: self,
234
- remote_object: Puppeteer::RemoteObject.new(response["object"]),
235
- )
236
- end
237
- private define_async_method :async_adopt_backend_node_id
238
-
239
- # @param element_handle [Puppeteer::ElementHandle]
240
- # @return [Puppeteer::ElementHandle]
241
- def adopt_element_handle(element_handle)
242
- if element_handle.execution_context == self
243
- raise ArgumentError.new('Cannot adopt handle that already belongs to this execution context')
244
- end
245
-
246
- unless @world
247
- raise 'Cannot adopt handle without DOMWorld'
248
- end
249
-
250
- node_info = element_handle.remote_object.node_info(@client)
251
- response = @client.send_message('DOM.resolveNode',
252
- backendNodeId: node_info["node"]["backendNodeId"],
253
- executionContextId: @context_id,
254
- )
255
-
256
- Puppeteer::JSHandle.create(
257
- context: self,
258
- remote_object: Puppeteer::RemoteObject.new(response["object"]),
259
- )
260
- end
261
204
  end
@@ -37,8 +37,8 @@ class Puppeteer::Frame
37
37
  # @param client [Puppeteer::CDPSession]
38
38
  private def update_client(client)
39
39
  @client = client
40
- @main_world = Puppeteer::DOMWorld.new(@client, @frame_manager, self, @frame_manager.timeout_settings)
41
- @secondary_world = Puppeteer::DOMWorld.new(@client, @frame_manager, self, @frame_manager.timeout_settings)
40
+ @main_world = Puppeteer::IsolaatedWorld.new(@client, @frame_manager, self, @frame_manager.timeout_settings)
41
+ @puppeteer_world = Puppeteer::IsolaatedWorld.new(@client, @frame_manager, self, @frame_manager.timeout_settings)
42
42
  end
43
43
 
44
44
  def page
@@ -49,7 +49,7 @@ class Puppeteer::Frame
49
49
  @client != @frame_manager.client
50
50
  end
51
51
 
52
- attr_accessor :frame_manager, :id, :loader_id, :lifecycle_events, :main_world, :secondary_world
52
+ attr_accessor :frame_manager, :id, :loader_id, :lifecycle_events, :main_world, :puppeteer_world
53
53
 
54
54
  def has_started_loading?
55
55
  @has_started_loading
@@ -106,7 +106,14 @@ class Puppeteer::Frame
106
106
  # @param {string} expression
107
107
  # @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
108
108
  def Sx(expression)
109
- @main_world.Sx(expression)
109
+ param_xpath =
110
+ if expression.start_with?('//')
111
+ ".#{expression}"
112
+ else
113
+ expression
114
+ end
115
+
116
+ query_selector_all("xpath/#{param_xpath}")
110
117
  end
111
118
 
112
119
  define_async_method :async_Sx
@@ -147,14 +154,14 @@ class Puppeteer::Frame
147
154
 
148
155
  # @return [String]
149
156
  def content
150
- @secondary_world.content
157
+ @puppeteer_world.content
151
158
  end
152
159
 
153
160
  # @param html [String]
154
161
  # @param timeout [Integer]
155
162
  # @param wait_until [String|Array<String>]
156
163
  def set_content(html, timeout: nil, wait_until: nil)
157
- @secondary_world.set_content(html, timeout: timeout, wait_until: wait_until)
164
+ @puppeteer_world.set_content(html, timeout: timeout, wait_until: wait_until)
158
165
  end
159
166
 
160
167
  # @return [String]
@@ -205,35 +212,35 @@ class Puppeteer::Frame
205
212
  # @param button [String] "left"|"right"|"middle"
206
213
  # @param click_count [Number]
207
214
  def click(selector, delay: nil, button: nil, click_count: nil)
208
- @secondary_world.click(selector, delay: delay, button: button, click_count: click_count)
215
+ @puppeteer_world.click(selector, delay: delay, button: button, click_count: click_count)
209
216
  end
210
217
 
211
218
  define_async_method :async_click
212
219
 
213
220
  # @param {string} selector
214
221
  def focus(selector)
215
- @secondary_world.focus(selector)
222
+ @puppeteer_world.focus(selector)
216
223
  end
217
224
 
218
225
  define_async_method :async_focus
219
226
 
220
227
  # @param {string} selector
221
228
  def hover(selector)
222
- @secondary_world.hover(selector)
229
+ @puppeteer_world.hover(selector)
223
230
  end
224
231
 
225
232
  # @param {string} selector
226
233
  # @param {!Array<string>} values
227
234
  # @return {!Promise<!Array<string>>}
228
235
  def select(selector, *values)
229
- @secondary_world.select(selector, *values)
236
+ @puppeteer_world.select(selector, *values)
230
237
  end
231
238
 
232
239
  define_async_method :async_select
233
240
 
234
241
  # @param {string} selector
235
242
  def tap(selector)
236
- @secondary_world.tap(selector)
243
+ @puppeteer_world.tap(selector)
237
244
  end
238
245
 
239
246
  define_async_method :async_tap
@@ -252,14 +259,8 @@ class Puppeteer::Frame
252
259
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
253
260
  # @param timeout [Integer]
254
261
  def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
255
- handle = @secondary_world.wait_for_selector(selector, visible: visible, hidden: hidden, timeout: timeout)
256
- if !handle
257
- return nil
258
- end
259
- main_execution_context = @main_world.execution_context
260
- result = main_execution_context.adopt_element_handle(handle)
261
- handle.dispose
262
- result
262
+ query_handler_manager = Puppeteer::QueryHandlerManager.instance
263
+ query_handler_manager.detect_query_handler(selector).wait_for(self, visible: visible, hidden: hidden, timeout: timeout)
263
264
  end
264
265
 
265
266
  define_async_method :async_wait_for_selector
@@ -274,14 +275,14 @@ class Puppeteer::Frame
274
275
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
275
276
  # @param timeout [Integer]
276
277
  def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
277
- handle = @secondary_world.wait_for_xpath(xpath, visible: visible, hidden: hidden, timeout: timeout)
278
- if !handle
279
- return nil
280
- end
281
- main_execution_context = @main_world.execution_context
282
- result = main_execution_context.adopt_element_handle(handle)
283
- handle.dispose
284
- result
278
+ param_xpath =
279
+ if xpath.start_with?('//')
280
+ ".#{xpath}"
281
+ else
282
+ xpath
283
+ end
284
+
285
+ wait_for_selector("xpath/#{param_xpath}", visible: visible, hidden: hidden, timeout: timeout)
285
286
  end
286
287
 
287
288
  define_async_method :async_wait_for_xpath
@@ -299,7 +300,7 @@ class Puppeteer::Frame
299
300
 
300
301
  # @return [String]
301
302
  def title
302
- @secondary_world.title
303
+ @puppeteer_world.title
303
304
  end
304
305
 
305
306
  # @param frame_payload [Hash]
@@ -337,7 +338,7 @@ class Puppeteer::Frame
337
338
  def detach
338
339
  @detached = true
339
340
  @main_world.detach
340
- @secondary_world.detach
341
+ @puppeteer_world.detach
341
342
  if @parent_frame
342
343
  @parent_frame._child_frames.delete(self)
343
344
  end
@@ -144,13 +144,13 @@ class Puppeteer::FrameManager
144
144
  watcher.same_document_navigation_promise
145
145
  end,
146
146
  )
147
+
148
+ watcher.navigation_response
147
149
  rescue Puppeteer::TimeoutError => err
148
150
  raise NavigationError.new(err)
149
151
  ensure
150
152
  watcher.dispose
151
153
  end
152
-
153
- watcher.navigation_response
154
154
  end
155
155
 
156
156
  # @param timeout [number|nil]
@@ -168,13 +168,13 @@ class Puppeteer::FrameManager
168
168
  watcher.same_document_navigation_promise,
169
169
  watcher.new_document_navigation_promise,
170
170
  )
171
+
172
+ watcher.navigation_response
171
173
  rescue Puppeteer::TimeoutError => err
172
174
  raise NavigationError.new(err)
173
175
  ensure
174
176
  watcher.dispose
175
177
  end
176
-
177
- watcher.navigation_response
178
178
  end
179
179
 
180
180
  # @param event [Hash]
@@ -413,11 +413,11 @@ class Puppeteer::FrameManager
413
413
 
414
414
  if context_payload.dig('auxData', 'isDefault')
415
415
  world = frame.main_world
416
- elsif context_payload['name'] == UTILITY_WORLD_NAME && !frame.secondary_world.has_context?
416
+ elsif context_payload['name'] == UTILITY_WORLD_NAME && !frame.puppeteer_world.has_context?
417
417
  # In case of multiple sessions to the same target, there's a race between
418
418
  # connections so we might end up creating multiple isolated worlds.
419
419
  # We can use either.
420
- world = frame.secondary_world
420
+ world = frame.puppeteer_world
421
421
  end
422
422
  end
423
423
 
@@ -1,7 +1,7 @@
1
1
  require 'thread'
2
2
 
3
- # https://github.com/puppeteer/puppeteer/blob/master/src/DOMWorld.js
4
- class Puppeteer::DOMWorld
3
+ # https://github.com/puppeteer/puppeteer/blob/master/src/IsolaatedWorld.js
4
+ class Puppeteer::IsolaatedWorld
5
5
  using Puppeteer::DefineAsyncMethod
6
6
 
7
7
  class BindingFunction
@@ -152,7 +152,7 @@ class Puppeteer::DOMWorld
152
152
  raise 'Bug of puppeteer-ruby...'
153
153
  end
154
154
 
155
- private def document
155
+ def document
156
156
  @document ||= evaluate_document.as_element
157
157
  end
158
158
 
@@ -416,16 +416,6 @@ class Puppeteer::DOMWorld
416
416
  handle.dispose
417
417
  end
418
418
 
419
- # @param selector [String]
420
- # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
421
- # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
422
- # @param timeout [Integer]
423
- def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil, root: nil)
424
- # call wait_for_selector_in_page with custom query selector.
425
- query_selector_manager = Puppeteer::QueryHandlerManager.instance
426
- query_selector_manager.detect_query_handler(selector).wait_for(self, visible: visible, hidden: hidden, timeout: timeout, root: root)
427
- end
428
-
429
419
  private def binding_identifier(name, context)
430
420
  "#{name}_#{context.send(:_context_id)}"
431
421
  end
@@ -497,7 +487,7 @@ class Puppeteer::DOMWorld
497
487
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
498
488
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
499
489
  # @param timeout [Integer]
500
- private def wait_for_selector_in_page(query_one, selector, visible: nil, hidden: nil, timeout: nil, root: nil, binding_function: nil)
490
+ private def wait_for_selector_in_page(query_one, root, selector, visible: nil, hidden: nil, timeout: nil, binding_function: nil)
501
491
  option_wait_for_visible = visible || false
502
492
  option_wait_for_hidden = hidden || false
503
493
  option_timeout = timeout || @timeout_settings.timeout
@@ -531,55 +521,7 @@ class Puppeteer::DOMWorld
531
521
  root: option_root,
532
522
  binding_function: binding_function,
533
523
  )
534
- handle = wait_task.await_promise
535
- unless handle.as_element
536
- handle.dispose
537
- return nil
538
- end
539
- handle.as_element
540
- end
541
-
542
- # @param xpath [String]
543
- # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
544
- # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
545
- # @param timeout [Integer]
546
- def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil, root: nil)
547
- option_wait_for_visible = visible || false
548
- option_wait_for_hidden = hidden || false
549
- option_timeout = timeout || @timeout_settings.timeout
550
- option_root = root
551
-
552
- polling =
553
- if option_wait_for_visible || option_wait_for_hidden
554
- 'raf'
555
- else
556
- 'mutation'
557
- end
558
- title = "XPath #{xpath}#{option_wait_for_hidden ? 'to be hidden' : ''}"
559
-
560
- xpath_predicate = make_predicate_string(
561
- predicate_arg_def: '(root, selector, waitForVisible, waitForHidden)',
562
- predicate_body: <<~JAVASCRIPT
563
- const node = document.evaluate(selector, root, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
564
- return checkWaitForOptions(node, waitForVisible, waitForHidden);
565
- JAVASCRIPT
566
- )
567
-
568
- wait_task = Puppeteer::WaitTask.new(
569
- dom_world: self,
570
- predicate_body: xpath_predicate,
571
- title: title,
572
- polling: polling,
573
- timeout: option_timeout,
574
- args: [xpath, option_wait_for_visible, option_wait_for_hidden],
575
- root: option_root,
576
- )
577
- handle = wait_task.await_promise
578
- unless handle.as_element
579
- handle.dispose
580
- return nil
581
- end
582
- handle.as_element
524
+ wait_task.await_promise
583
525
  end
584
526
 
585
527
  # @param page_function [String]
@@ -644,4 +586,35 @@ class Puppeteer::DOMWorld
644
586
  }
645
587
  JAVASCRIPT
646
588
  end
589
+
590
+ # @param backend_node_id [Integer]
591
+ # @return [Puppeteer::ElementHandle]
592
+ def adopt_backend_node(backend_node_id)
593
+ response = @client.send_message('DOM.resolveNode',
594
+ backendNodeId: backend_node_id,
595
+ executionContextId: execution_context.send(:_context_id),
596
+ )
597
+ Puppeteer::JSHandle.create(
598
+ context: execution_context,
599
+ remote_object: Puppeteer::RemoteObject.new(response["object"]),
600
+ )
601
+ end
602
+ private define_async_method :async_adopt_backend_node
603
+
604
+ # @param element_handle [Puppeteer::ElementHandle]
605
+ # @return [Puppeteer::ElementHandle]
606
+ def adopt_handle(element_handle)
607
+ if element_handle.execution_context == execution_context
608
+ raise ArgumentError.new('Cannot adopt handle that already belongs to this execution context')
609
+ end
610
+
611
+ node_info = element_handle.remote_object.node_info(@client)
612
+ adopt_backend_node(node_info["node"]["backendNodeId"])
613
+ end
614
+
615
+ def transfer_handle(element_handle)
616
+ result = adopt_handle(element_handle)
617
+ element_handle.dispose
618
+ result
619
+ end
647
620
  end
@@ -5,16 +5,12 @@ class Puppeteer::JSHandle
5
5
  # @param context [Puppeteer::ExecutionContext]
6
6
  # @param remote_object [Puppeteer::RemoteObject]
7
7
  def self.create(context:, remote_object:)
8
- frame = context.frame
9
- if remote_object.sub_type == 'node' && frame
10
- frame_manager = frame.frame_manager
8
+ if remote_object.sub_type == 'node' && context.world
11
9
  Puppeteer::ElementHandle.new(
12
10
  context: context,
13
11
  client: context.client,
14
12
  remote_object: remote_object,
15
- frame: frame,
16
- page: frame_manager.page,
17
- frame_manager: frame_manager,
13
+ frame: context.world.frame,
18
14
  )
19
15
  else
20
16
  Puppeteer::JSHandle.new(
@@ -81,7 +81,10 @@ class Puppeteer::LifecycleWatcher
81
81
  @frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameSwapped, &method(:handle_frame_swapped)),
82
82
  @frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameDetached, &method(:handle_frame_detached)),
83
83
  ]
84
- @listener_ids['network_manager'] = @frame_manager.network_manager.add_event_listener(NetworkManagerEmittedEvents::Request, &method(:handle_request))
84
+ @listener_ids['network_manager'] = [
85
+ @frame_manager.network_manager.add_event_listener(NetworkManagerEmittedEvents::Request, &method(:handle_request)),
86
+ @frame_manager.network_manager.add_event_listener(NetworkManagerEmittedEvents::Response, &method(:handle_response)),
87
+ ]
85
88
 
86
89
  @same_document_navigation_promise = resolvable_future
87
90
  @lifecycle_promise = resolvable_future
@@ -94,6 +97,21 @@ class Puppeteer::LifecycleWatcher
94
97
  def handle_request(request)
95
98
  return if request.frame != @frame || !request.navigation_request?
96
99
  @navigation_request = request
100
+ # Resolve previous navigation response in case there are multiple
101
+ # navigation requests reported by the backend. This generally should not
102
+ # happen by it looks like it's possible.
103
+ @navigation_response_received&.fulfill(nil)
104
+ @navigation_response_received = resolvable_future
105
+ if request.response && !@navigation_response_received.resolved?
106
+ @navigation_response_received.fulfill(nil)
107
+ end
108
+ end
109
+
110
+ # @param [Puppeteer::HTTPResponse] response
111
+ def handle_response(response)
112
+ return if @navigation_request&.internal&.request_id != response.request.internal.request_id
113
+
114
+ @navigation_response_received.fulfill(nil) unless @navigation_response_received.resolved?
97
115
  end
98
116
 
99
117
  # @param frame [Puppeteer::Frame]
@@ -107,6 +125,8 @@ class Puppeteer::LifecycleWatcher
107
125
 
108
126
  # @return [Puppeteer::HTTPResponse]
109
127
  def navigation_response
128
+ # Continue with a possibly null response.
129
+ @navigation_response_received.value! rescue nil
110
130
  if_present(@navigation_request) do |request|
111
131
  request.response
112
132
  end
@@ -175,8 +195,8 @@ class Puppeteer::LifecycleWatcher
175
195
  if_present(@listener_ids['frame_manager']) do |ids|
176
196
  @frame_manager.remove_event_listener(*ids)
177
197
  end
178
- if_present(@listener_ids['network_manager']) do |id|
179
- @frame_manager.network_manager.remove_event_listener(id)
198
+ if_present(@listener_ids['network_manager']) do |ids|
199
+ @frame_manager.network_manager.remove_event_listener(*ids)
180
200
  end
181
201
  end
182
202
  end
@@ -191,8 +191,7 @@ class Puppeteer::Page
191
191
  return if @file_chooser_interceptors.empty?
192
192
 
193
193
  frame = @frame_manager.frame(event['frameId'])
194
- context = frame.execution_context
195
- element = context.adopt_backend_node_id(event['backendNodeId'])
194
+ element = frame.main_world.adopt_backend_node(event['backendNodeId'])
196
195
  interceptors = @file_chooser_interceptors.to_a
197
196
  @file_chooser_interceptors.clear
198
197
  file_chooser = Puppeteer::FileChooser.new(element, event)
@@ -1070,7 +1069,8 @@ class Puppeteer::Page
1070
1069
  clip = if_present(screenshot_options.clip) do |rect|
1071
1070
  x = rect[:x].round
1072
1071
  y = rect[:y].round
1073
- { x: x, y: y, width: rect[:width] + rect[:x] - x, height: rect[:height] + rect[:y] - y, scale: 1 }
1072
+ scale = rect[:scale] || 1
1073
+ { x: x, y: y, width: rect[:width] + rect[:x] - x, height: rect[:height] + rect[:y] - y, scale: scale }
1074
1074
  end
1075
1075
 
1076
1076
  if screenshot_options.full_page?
@@ -6,6 +6,7 @@ class Puppeteer::QueryHandlerManager
6
6
  def query_handlers
7
7
  @query_handlers ||= {
8
8
  aria: Puppeteer::AriaQueryHandler.new,
9
+ xpath: xpath_handler,
9
10
  }
10
11
  end
11
12
 
@@ -16,6 +17,40 @@ class Puppeteer::QueryHandlerManager
16
17
  )
17
18
  end
18
19
 
20
+ private def xpath_handler
21
+ @xpath_handler ||= Puppeteer::CustomQueryHandler.new(
22
+ query_one: <<~JAVASCRIPT,
23
+ (element, selector) => {
24
+ const doc = element.ownerDocument || document;
25
+ const result = doc.evaluate(
26
+ selector,
27
+ element,
28
+ null,
29
+ XPathResult.FIRST_ORDERED_NODE_TYPE
30
+ );
31
+ return result.singleNodeValue;
32
+ }
33
+ JAVASCRIPT
34
+ query_all: <<~JAVASCRIPT,
35
+ (element, selector) => {
36
+ const doc = element.ownerDocument || document;
37
+ const iterator = doc.evaluate(
38
+ selector,
39
+ element,
40
+ null,
41
+ XPathResult.ORDERED_NODE_ITERATOR_TYPE
42
+ );
43
+ const array = [];
44
+ let item;
45
+ while ((item = iterator.iterateNext())) {
46
+ array.push(item);
47
+ }
48
+ return array;
49
+ }
50
+ JAVASCRIPT
51
+ )
52
+ end
53
+
19
54
  class Result
20
55
  def initialize(query_handler:, selector:)
21
56
  @query_handler = query_handler
@@ -26,8 +61,8 @@ class Puppeteer::QueryHandlerManager
26
61
  @query_handler.query_one(element_handle, @selector)
27
62
  end
28
63
 
29
- def wait_for(dom_world, visible:, hidden:, timeout:, root:)
30
- @query_handler.wait_for(dom_world, @selector, visible: visible, hidden: hidden, timeout: timeout, root: root)
64
+ def wait_for(element_or_frame, visible:, hidden:, timeout:)
65
+ @query_handler.wait_for(element_or_frame, @selector, visible: visible, hidden: hidden, timeout: timeout)
31
66
  end
32
67
 
33
68
  def query_all(element_handle)
@@ -105,8 +105,8 @@ class Puppeteer::RemoteObject
105
105
  role: role,
106
106
  }.compact)
107
107
 
108
- result['nodes'].reject do |node|
109
- node['role']['value'] == 'text'
108
+ result['nodes'].select do |node|
109
+ node['role'] && node['role']['value'] != 'StaticText'
110
110
  end
111
111
  end
112
112
 
@@ -93,7 +93,7 @@ class Puppeteer::Target
93
93
  end
94
94
 
95
95
  def create_cdp_session
96
- @session_factory.call
96
+ @session_factory.call(false)
97
97
  end
98
98
 
99
99
  def target_manager
@@ -102,7 +102,7 @@ class Puppeteer::Target
102
102
 
103
103
  def page
104
104
  if @is_page_target_callback.call(@target_info) && @page.nil?
105
- client = @session || @session_factory.call
105
+ client = @session || @session_factory.call(true)
106
106
  @page = Puppeteer::Page.create(client, self, @ignore_https_errors, @default_viewport)
107
107
  end
108
108
  @page
@@ -1,3 +1,3 @@
1
1
  module Puppeteer
2
- VERSION = '0.43.0'
2
+ VERSION = '0.44.0'
3
3
  end
data/lib/puppeteer.rb CHANGED
@@ -32,7 +32,6 @@ require 'puppeteer/css_coverage'
32
32
  require 'puppeteer/custom_query_handler'
33
33
  require 'puppeteer/devices'
34
34
  require 'puppeteer/dialog'
35
- require 'puppeteer/dom_world'
36
35
  require 'puppeteer/emulation_manager'
37
36
  require 'puppeteer/exception_details'
38
37
  require 'puppeteer/executable_path_finder'
@@ -43,6 +42,7 @@ require 'puppeteer/frame'
43
42
  require 'puppeteer/frame_manager'
44
43
  require 'puppeteer/http_request'
45
44
  require 'puppeteer/http_response'
45
+ require 'puppeteer/isolated_world'
46
46
  require 'puppeteer/js_coverage'
47
47
  require 'puppeteer/js_handle'
48
48
  require 'puppeteer/keyboard'
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.43.0
4
+ version: 0.44.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - YusukeIwaki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-09-13 00:00:00.000000000 Z
11
+ date: 2022-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -272,7 +272,6 @@ files:
272
272
  - lib/puppeteer/device.rb
273
273
  - lib/puppeteer/devices.rb
274
274
  - lib/puppeteer/dialog.rb
275
- - lib/puppeteer/dom_world.rb
276
275
  - lib/puppeteer/element_handle.rb
277
276
  - lib/puppeteer/element_handle/bounding_box.rb
278
277
  - lib/puppeteer/element_handle/box_model.rb
@@ -294,6 +293,7 @@ files:
294
293
  - lib/puppeteer/http_request.rb
295
294
  - lib/puppeteer/http_response.rb
296
295
  - lib/puppeteer/if_present.rb
296
+ - lib/puppeteer/isolated_world.rb
297
297
  - lib/puppeteer/js_coverage.rb
298
298
  - lib/puppeteer/js_handle.rb
299
299
  - lib/puppeteer/keyboard.rb