puppeteer-ruby 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +36 -0
  5. data/.travis.yml +7 -0
  6. data/Dockerfile +6 -0
  7. data/Gemfile +6 -0
  8. data/README.md +41 -0
  9. data/Rakefile +1 -0
  10. data/bin/console +11 -0
  11. data/bin/setup +8 -0
  12. data/docker-compose.yml +15 -0
  13. data/example.rb +7 -0
  14. data/lib/puppeteer.rb +192 -0
  15. data/lib/puppeteer/async_await_behavior.rb +34 -0
  16. data/lib/puppeteer/browser.rb +240 -0
  17. data/lib/puppeteer/browser_context.rb +90 -0
  18. data/lib/puppeteer/browser_fetcher.rb +6 -0
  19. data/lib/puppeteer/browser_runner.rb +142 -0
  20. data/lib/puppeteer/cdp_session.rb +78 -0
  21. data/lib/puppeteer/concurrent_ruby_utils.rb +37 -0
  22. data/lib/puppeteer/connection.rb +254 -0
  23. data/lib/puppeteer/console_message.rb +24 -0
  24. data/lib/puppeteer/debug_print.rb +20 -0
  25. data/lib/puppeteer/device.rb +12 -0
  26. data/lib/puppeteer/devices.rb +885 -0
  27. data/lib/puppeteer/dom_world.rb +447 -0
  28. data/lib/puppeteer/element_handle.rb +433 -0
  29. data/lib/puppeteer/emulation_manager.rb +46 -0
  30. data/lib/puppeteer/errors.rb +4 -0
  31. data/lib/puppeteer/event_callbackable.rb +88 -0
  32. data/lib/puppeteer/execution_context.rb +230 -0
  33. data/lib/puppeteer/frame.rb +278 -0
  34. data/lib/puppeteer/frame_manager.rb +380 -0
  35. data/lib/puppeteer/if_present.rb +18 -0
  36. data/lib/puppeteer/js_handle.rb +142 -0
  37. data/lib/puppeteer/keyboard.rb +183 -0
  38. data/lib/puppeteer/keyboard/key_description.rb +19 -0
  39. data/lib/puppeteer/keyboard/us_keyboard_layout.rb +283 -0
  40. data/lib/puppeteer/launcher.rb +26 -0
  41. data/lib/puppeteer/launcher/base.rb +48 -0
  42. data/lib/puppeteer/launcher/browser_options.rb +41 -0
  43. data/lib/puppeteer/launcher/chrome.rb +165 -0
  44. data/lib/puppeteer/launcher/chrome_arg_options.rb +49 -0
  45. data/lib/puppeteer/launcher/launch_options.rb +68 -0
  46. data/lib/puppeteer/lifecycle_watcher.rb +168 -0
  47. data/lib/puppeteer/mouse.rb +120 -0
  48. data/lib/puppeteer/network_manager.rb +122 -0
  49. data/lib/puppeteer/page.rb +1001 -0
  50. data/lib/puppeteer/page/screenshot_options.rb +78 -0
  51. data/lib/puppeteer/remote_object.rb +124 -0
  52. data/lib/puppeteer/target.rb +150 -0
  53. data/lib/puppeteer/timeout_settings.rb +15 -0
  54. data/lib/puppeteer/touch_screen.rb +43 -0
  55. data/lib/puppeteer/version.rb +3 -0
  56. data/lib/puppeteer/viewport.rb +36 -0
  57. data/lib/puppeteer/wait_task.rb +6 -0
  58. data/lib/puppeteer/web_socket.rb +117 -0
  59. data/lib/puppeteer/web_socket_transport.rb +49 -0
  60. data/puppeteer-ruby.gemspec +29 -0
  61. metadata +213 -0
@@ -0,0 +1,380 @@
1
+ require 'timeout'
2
+
3
+ class Puppeteer::FrameManager
4
+ include Puppeteer::DebugPrint
5
+ include Puppeteer::IfPresent
6
+ include Puppeteer::EventCallbackable
7
+ using Puppeteer::AsyncAwaitBehavior
8
+
9
+ UTILITY_WORLD_NAME = '__puppeteer_utility_world__'
10
+
11
+ # @param {!Puppeteer.CDPSession} client
12
+ # @param {!Puppeteer.Page} page
13
+ # @param {boolean} ignoreHTTPSErrors
14
+ # @param {!Puppeteer.TimeoutSettings} timeoutSettings
15
+ def initialize(client, page, ignore_https_errors, timeout_settings)
16
+ @client = client
17
+ @page = page
18
+ @network_manager = Puppeteer::NetworkManager.new(client, ignore_https_errors, self)
19
+ @timeout_settings = timeout_settings
20
+
21
+ # @type {!Map<string, !Frame>}
22
+ @frames = {}
23
+
24
+ # @type {!Map<number, !ExecutionContext>}
25
+ @context_id_to_context = {}
26
+ @context_id_created = {}
27
+
28
+ # @type {!Set<string>}
29
+ @isolated_worlds = Set.new
30
+
31
+ @client.on_event 'Page.frameAttached' do |event|
32
+ handle_frame_attached(event['frameId'], event['parentFrameId'])
33
+ end
34
+ @client.on_event 'Page.frameNavigated' do |event|
35
+ handle_frame_navigated(event['frame'])
36
+ end
37
+ @client.on_event 'Page.navigatedWithinDocument' do |event|
38
+ handle_frame_navigated_within_document(event['frameId'], event['url'])
39
+ end
40
+ @client.on_event 'Page.frameDetached' do |event|
41
+ handle_frame_detached(event['frameId'])
42
+ end
43
+ @client.on_event 'Page.frameStoppedLoading' do |event|
44
+ handle_frame_stopped_loading(event['frameId'])
45
+ end
46
+ @client.on_event 'Runtime.executionContextCreated' do |event|
47
+ handle_execution_context_created(event['context'])
48
+ end
49
+ @client.on_event 'Runtime.executionContextDestroyed' do |event|
50
+ handle_execution_context_destroyed(event['executionContextId'])
51
+ end
52
+ @client.on_event 'Runtime.executionContextsCleared' do |event|
53
+ handle_execution_contexts_cleared
54
+ end
55
+ @client.on_event 'Page.lifecycleEvent' do |event|
56
+ handle_lifecycle_event(event)
57
+ end
58
+ end
59
+
60
+ attr_reader :client, :timeout_settings
61
+
62
+ private def init
63
+ results = await_all(
64
+ @client.async_send_message('Page.enable'),
65
+ @client.async_send_message('Page.getFrameTree'),
66
+ )
67
+ frame_tree = results.last['frameTree']
68
+ handle_frame_tree(frame_tree)
69
+ await_all(
70
+ @client.async_send_message('Page.setLifecycleEventsEnabled', enabled: true),
71
+ @client.async_send_message('Runtime.enable'),
72
+ )
73
+ ensure_isolated_world(UTILITY_WORLD_NAME)
74
+ @network_manager.init
75
+ end
76
+
77
+ async def async_init
78
+ init
79
+ end
80
+
81
+ attr_reader :network_manager
82
+
83
+ class NavigationError < StandardError ; end
84
+
85
+ # @param frame [Puppeteer::Frame]
86
+ # @param url [String]
87
+ # @param {!{referer?: string, timeout?: number, waitUntil?: string|!Array<string>}=} options
88
+ # @return [Puppeteer::Response]
89
+ def navigate_frame(frame, url, referer: nil, timeout: nil, wait_until: nil)
90
+ assert_no_legacy_navigation_options(wait_until: wait_until)
91
+
92
+ navigate_params = {
93
+ url: url,
94
+ referer: referer || @network_manager.extra_http_headers['referer'],
95
+ frameId: frame.id,
96
+ }.compact
97
+ option_wait_until = wait_until || ['load']
98
+ option_timeout = timeout || @timeout_settings.navigation_timeout
99
+
100
+ watcher = Puppeteer::LifecycleWatcher.new(self, frame, option_wait_until, option_timeout)
101
+ ensure_new_document_navigation = false
102
+
103
+ begin
104
+ navigate = future {
105
+ result = @client.send_message('Page.navigate', navigate_params)
106
+ loader_id = result['loaderId']
107
+ ensure_new_document_navigation = !!loader_id
108
+ if result['errorText']
109
+ raise NavigationError.new("#{result['errorText']} at #{url}")
110
+ end
111
+ }
112
+ await_any(
113
+ navigate,
114
+ watcher.timeout_or_termination_promise,
115
+ )
116
+
117
+ document_navigation_promise =
118
+ if ensure_new_document_navigation
119
+ watcher.new_document_navigation_promise
120
+ else
121
+ watcher.same_document_navigation_promise
122
+ end
123
+ await_any(
124
+ document_navigation_promise,
125
+ watcher.timeout_or_termination_promise,
126
+ )
127
+ ensure
128
+ watcher.dispose
129
+ end
130
+
131
+ watcher.navigation_response
132
+ end
133
+
134
+ # @param timeout [number|nil]
135
+ # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
136
+ # @return [Puppeteer::Response]
137
+ def wait_for_frame_navigation(frame, timeout: nil, wait_until: nil)
138
+ assert_no_legacy_navigation_options(wait_until: wait_until)
139
+
140
+ option_wait_until = wait_until || ['load']
141
+ option_timeout = timeout || @timeout_settings.navigation_timeout
142
+ watcher = Puppeteer::LifecycleWatcher.new(self, frame, option_wait_until, option_timeout)
143
+ begin
144
+ await_any(
145
+ watcher.timeout_or_termination_promise,
146
+ watcher.same_document_navigation_promise,
147
+ watcher.new_document_navigation_promise,
148
+ )
149
+ ensure
150
+ watcher.dispose
151
+ end
152
+
153
+ watcher.navigation_response
154
+ end
155
+
156
+ # @param event [Hash]
157
+ def handle_lifecycle_event(event)
158
+ frame = @frames[event['frameId']]
159
+ return if !frame
160
+ frame.handle_lifecycle_event(event['loaderId'], event['name'])
161
+ emit_event 'Events.FrameManager.LifecycleEvent', frame
162
+ end
163
+
164
+ # @param {string} frameId
165
+ def handle_frame_stopped_loading(frame_id)
166
+ frame = @frames[frame_id]
167
+ return if !frame
168
+ frame.handle_loading_stopped
169
+ emit_event 'Events.FrameManager.LifecycleEvent', frame
170
+ end
171
+
172
+ # @param frame_tree [Hash]
173
+ def handle_frame_tree(frame_tree)
174
+ if frame_tree['frame']['parentId']
175
+ handle_frame_attached(frame_tree['frame']['id'], frame_tree['frame']['parentId'])
176
+ end
177
+ handle_frame_navigated(frame_tree['frame'])
178
+ return if !frame_tree['childFrames']
179
+
180
+ frame_tree['childFrames'].each do |child|
181
+ handle_frame_tree(child)
182
+ end
183
+ end
184
+
185
+ # @return {!Puppeteer.Page}
186
+ def page
187
+ @page
188
+ end
189
+
190
+ # @return {!Frame}
191
+ def main_frame
192
+ @main_frame
193
+ end
194
+
195
+ # @return {!Array<!Frame>}
196
+ def frames
197
+ @frames.values
198
+ end
199
+
200
+ # @param {!string} frameId
201
+ # @return {?Frame}
202
+ def frame(frame_id)
203
+ @frames[frame_id]
204
+ end
205
+
206
+ # @param {string} frameId
207
+ # @param {?string} parentFrameId
208
+ def handle_frame_attached(frame_id, parent_frame_id)
209
+ return if @frames.has_key?[frame_id]
210
+ if !parent_frame_id
211
+ raise ArgymentError.new('parent_frame_id must not be nil')
212
+ end
213
+ parent_frame = @frames[parent_frame_id]
214
+ frame = Frame.new(self, @client, parent_frame, frame_id)
215
+ @frames[frame_id] = frame
216
+
217
+ emit_event 'Events.FrameManager.FrameAttached', frame
218
+ end
219
+
220
+ # @param frame_payload [Hash]
221
+ def handle_frame_navigated(frame_payload)
222
+ is_main_frame = !frame_payload['parent_id']
223
+ frame =
224
+ if is_main_frame
225
+ @main_frame
226
+ else
227
+ @frames[frame_payload['id']]
228
+ end
229
+
230
+ if !is_main_frame && !frame
231
+ raise ArgumentError.new('We either navigate top level or have old version of the navigated frame')
232
+ end
233
+
234
+ # Detach all child frames first.
235
+ if frame
236
+ frame.child_frames.each do |child|
237
+ remove_frame_recursively(child)
238
+ end
239
+ end
240
+
241
+ # Update or create main frame.
242
+ if is_main_frame
243
+ if frame
244
+ # Update frame id to retain frame identity on cross-process navigation.
245
+ @frames.delete(frame.id)
246
+ frame.id = frame_payload['id']
247
+ else
248
+ # Initial main frame navigation.
249
+ frame = Puppeteer::Frame.new(self, @client, nil, frame_payload['id'])
250
+ end
251
+ @frames[frame_payload['id']] = frame
252
+ @main_frame = frame
253
+ end
254
+
255
+ # Update frame payload.
256
+ frame.navigated(frame_payload);
257
+
258
+ emit_event 'Events.FrameManager.FrameNavigated', frame
259
+ end
260
+
261
+ # @param name [String]
262
+ def ensure_isolated_world(name)
263
+ return if @isolated_worlds.include?(name)
264
+ @isolated_worlds << name
265
+
266
+ @client.send_message('Page.addScriptToEvaluateOnNewDocument',
267
+ source: "//# sourceURL=#{Puppeteer::ExecutionContext::EVALUATION_SCRIPT_URL}",
268
+ worldName: name,
269
+ )
270
+ create_isolated_worlds_promises = frames.map do |frame|
271
+ @client.async_send_message('Page.createIsolatedWorld',
272
+ frameId: frame.id,
273
+ grantUniveralAccess: true,
274
+ worldName: name,
275
+ )
276
+ end
277
+ await_all(*create_isolated_worlds_promises)
278
+ end
279
+
280
+ # @param frame_id [String]
281
+ # @param url [String]
282
+ def handle_frame_navigated_within_document(frame_id, url)
283
+ frame = @frames[frame_id]
284
+ return if !frame
285
+ frame.navigated_within_document(url)
286
+ emit_event 'Events.FrameManager.FrameNavigatedWithinDocument', frame
287
+ emit_event 'Events.FrameManager.FrameNavigated', frame
288
+ handle_frame_manager_frame_navigated_within_document(frame)
289
+ handle_frame_manager_frame_navigated(frame)
290
+ end
291
+
292
+ # @param frame_id [String]
293
+ def handle_frame_detached(frame_id)
294
+ frame = @frames[frame_id]
295
+ if frame
296
+ remove_frame_recursively(frame)
297
+ end
298
+ end
299
+
300
+ # @param context_payload [Hash]
301
+ def handle_execution_context_created(context_payload)
302
+ frame = if_present(context_payload.dig('auxData', 'frameId')) { |frame_id| @frames[frame_id] }
303
+
304
+ world = nil
305
+ if frame
306
+ if context_payload.dig('auxData', 'isDefault')
307
+ world = frame.main_world
308
+ elsif context_payload['name'] == UTILITY_WORLD_NAME && !frame.secondary_world.has_context?
309
+ # In case of multiple sessions to the same target, there's a race between
310
+ # connections so we might end up creating multiple isolated worlds.
311
+ # We can use either.
312
+ world = frame.secondary_world
313
+ end
314
+ end
315
+
316
+ if context_payload.dig('auxData', 'type') == 'isolated'
317
+ @isolated_worlds << context_payload['name']
318
+ end
319
+
320
+ context = Puppeteer::ExecutionContext.new(@client, context_payload, world)
321
+ if world
322
+ world.context = context
323
+ end
324
+ @context_id_to_context[context_payload['id']] = context
325
+ @context_id_created[context_payload['id']] = Time.now
326
+ end
327
+
328
+ # @param {number} executionContextId
329
+ def handle_execution_context_destroyed(execution_context_id)
330
+ context = @context_id_to_context[execution_context_id]
331
+ return if !context
332
+ @context_id_to_context.delete(execution_context_id)
333
+ @context_id_created.delete(execution_context_id)
334
+ if context.world
335
+ context.world.context = nil
336
+ end
337
+ end
338
+
339
+ def handle_execution_contexts_cleared
340
+ # executionContextCleared is often notified after executionContextCreated.
341
+ # D, [2020-04-06T01:47:03.101227 #13823] DEBUG -- : RECV << {"method"=>"Runtime.executionContextCreated", "params"=>{"context"=>{"id"=>5, "origin"=>"https://github.com", "name"=>"", "auxData"=>{"isDefault"=>true, "type"=>"default", "frameId"=>"71C347B70848B89DDDEFAA8AB5B0BC92"}}}, "sessionId"=>"53F088EED260C28001D26A019F95D9E3"}
342
+ # D, [2020-04-06T01:47:03.101439 #13823] DEBUG -- : RECV << {"method"=>"Page.frameNavigated", "params"=>{"frame"=>{"id"=>"71C347B70848B89DDDEFAA8AB5B0BC92", "loaderId"=>"80338225D035AC96BAE8F6D4E81C7D51", "url"=>"https://github.com/search?q=puppeteer", "securityOrigin"=>"https://github.com", "mimeType"=>"text/html"}}, "sessionId"=>"53F088EED260C28001D26A019F95D9E3"}
343
+ # D, [2020-04-06T01:47:03.101325 #13823] DEBUG -- : RECV << {"method"=>"Target.targetInfoChanged", "params"=>{"targetInfo"=>{"targetId"=>"71C347B70848B89DDDEFAA8AB5B0BC92", "type"=>"page", "title"=>"https://github.com/search?q=puppeteer", "url"=>"https://github.com/search?q=puppeteer", "attached"=>true, "browserContextId"=>"AF37BC660284CE1552B4ECB147BE9305"}}}
344
+ # D, [2020-04-06T01:47:03.101269 #13823] DEBUG -- : RECV << {"method"=>"Runtime.executionContextsCleared", "params"=>{}, "sessionId"=>"53F088EED260C28001D26A019F95D9E3"}
345
+ # it unexpectedly clears the created execution context.
346
+ # To avoid the problem, just skip recent created ids.
347
+ now = Time.now
348
+ context_ids_to_skip = @context_id_created.select { |k, v| now - v < 1 }.keys
349
+ @context_id_to_context.reject{ |k, v| context_ids_to_skip.include?(k) }.values.each do |context|
350
+ if context.world
351
+ context.world.context = nil
352
+ end
353
+ end
354
+ @context_id_to_context.select!{ |k, v| context_ids_to_skip.include?(k) }
355
+ end
356
+
357
+ def execution_context_by_id(context_id)
358
+ context = @context_id_to_context[context_id]
359
+ if !context
360
+ raise "INTERNAL ERROR: missing context with id = #{context_id}"
361
+ end
362
+ return context
363
+ end
364
+
365
+ # @param {!Frame} frame
366
+ private def remove_frame_recursively(frame)
367
+ frame.child_frames.each do |child|
368
+ remove_frame_recursively(child)
369
+ end
370
+ frame.detach
371
+ @frames.delete(frame.id)
372
+ emit_event 'Events.FrameManager.FrameDetached', frame
373
+ end
374
+
375
+ private def assert_no_legacy_navigation_options(wait_until:)
376
+ if wait_until == 'networkidle'
377
+ raise ArgumentError.new('ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead')
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,18 @@
1
+ module Puppeteer::IfPresent
2
+ # Similar to #try in ActiveSupport::CoreExt.
3
+ #
4
+ # Evaluate block with the target, only if target is not nil.
5
+ # Returns nil if target is nil.
6
+ #
7
+ # --------
8
+ # if_present(params['target']) do |target|
9
+ # Point.new(target['x'], target['y'])
10
+ # end
11
+ # --------
12
+ def if_present(target, &block)
13
+ raise ArgumentError.new('block must be provided for #if_present') if block.nil?
14
+ return nil if target.nil?
15
+
16
+ block.call(target)
17
+ end
18
+ end
@@ -0,0 +1,142 @@
1
+ class Puppeteer::JSHandle
2
+ using Puppeteer::AsyncAwaitBehavior
3
+
4
+ # @param context [Puppeteer::ExecutionContext]
5
+ # @param remote_object [Puppeteer::RemoteObject]
6
+ def self.create(context:, remote_object:)
7
+ frame = context.frame
8
+ if remote_object.sub_type == 'node' && frame
9
+ frame_manager = frame.frame_manager
10
+ Puppeteer::ElementHandle.new(
11
+ context: context,
12
+ client: context.client,
13
+ remote_object: remote_object,
14
+ page: frame_manager.page,
15
+ frame_manager: frame_manager,
16
+ )
17
+ else
18
+ Puppeteer::JSHandle.new(
19
+ context: context,
20
+ client: context.client,
21
+ remote_object: remote_object,
22
+ )
23
+ end
24
+ end
25
+
26
+ # @param context [Puppeteer::ExecutionContext]
27
+ # @param client [Puppeteer::CDPSession]
28
+ # @param remote_object [Puppeteer::RemoteObject]
29
+ def initialize(context:, client:, remote_object:)
30
+ @context = context
31
+ @client = client
32
+ @remote_object = remote_object
33
+ @disposed = false
34
+ end
35
+
36
+ attr_reader :context, :remote_object
37
+
38
+ # @return [Puppeteer::ExecutionContext]
39
+ def execution_context
40
+ @context
41
+ end
42
+
43
+ # @param page_function [String]
44
+ # @return [Object]
45
+ def evaluate(page_function, *args)
46
+ execution_context.evaluate(page_function, self, *args)
47
+ end
48
+
49
+ # @param page_function [String]
50
+ # @return [Future<Object>]
51
+ async def async_evaluate(page_function, *args)
52
+ evaluate(page_function, *args)
53
+ end
54
+
55
+ # @param page_function [String]
56
+ # @param args {Array<*>}
57
+ # @return [Puppeteer::JSHandle]
58
+ def evaluate_handle(page_function, *args)
59
+ execution_context.evaluate_handle(page_function, self, *args)
60
+ end
61
+
62
+ # @param page_function [String]
63
+ # @param args {Array<*>}
64
+ # @return [Future<Puppeteer::JSHandle>]
65
+ async def async_evaluate_handle(page_function, *args)
66
+ evaluate_handle(page_function, *args)
67
+ end
68
+
69
+ # /**
70
+ # * @param {string} propertyName
71
+ # * @return {!Promise<?JSHandle>}
72
+ # */
73
+ # async getProperty(propertyName) {
74
+ # const objectHandle = await this.evaluateHandle((object, propertyName) => {
75
+ # const result = {__proto__: null};
76
+ # result[propertyName] = object[propertyName];
77
+ # return result;
78
+ # }, propertyName);
79
+ # const properties = await objectHandle.getProperties();
80
+ # const result = properties.get(propertyName) || null;
81
+ # await objectHandle.dispose();
82
+ # return result;
83
+ # }
84
+
85
+ # getProperties in JavaScript.
86
+ # @return [Hash<String, JSHandle>]
87
+ def properties
88
+ response = @remote_object.properties(@client)
89
+ response['result'].each_with_object({}) do |prop, h|
90
+ next unless prop['enumerable']
91
+ h[prop['name']] = Puppeteer::JSHandle.create(
92
+ context: @context,
93
+ remote_object: Puppeteer::RemoteObject.new(prop['value']))
94
+ end
95
+ end
96
+
97
+ def json_value
98
+ # original logic was:
99
+ # if (this._remoteObject.objectId) {
100
+ # const response = await this._client.send('Runtime.callFunctionOn', {
101
+ # functionDeclaration: 'function() { return this; }',
102
+ # objectId: this._remoteObject.objectId,
103
+ # returnByValue: true,
104
+ # awaitPromise: true,
105
+ # });
106
+ # return helper.valueFromRemoteObject(response.result);
107
+ # }
108
+ # return helper.valueFromRemoteObject(this._remoteObject);
109
+ #
110
+ # However it would be better that RemoteObject is responsible for
111
+ # the logic `if (this._remoteObject.objectId) { ... }`.
112
+ @remote_object.evaluate_self(@client) || @remote_object.value
113
+ end
114
+
115
+ def as_element
116
+ nil
117
+ end
118
+
119
+ # @return [Future]
120
+ def dispose
121
+ return if @disposed
122
+
123
+ @disposed = true
124
+ @remote_object.release(@client)
125
+ end
126
+
127
+ def disposed?
128
+ @disposed
129
+ end
130
+
131
+ # /**
132
+ # * @override
133
+ # * @return {string}
134
+ # */
135
+ # toString() {
136
+ # if (this._remoteObject.objectId) {
137
+ # const type = this._remoteObject.subtype || this._remoteObject.type;
138
+ # return 'JSHandle@' + type;
139
+ # }
140
+ # return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
141
+ # }
142
+ end