apparition 0.0.1 → 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.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/lib/capybara/apparition/browser.rb +41 -48
- data/lib/capybara/apparition/chrome_client.rb +52 -55
- data/lib/capybara/apparition/dev_tools_protocol/session.rb +2 -2
- data/lib/capybara/apparition/dev_tools_protocol/target.rb +1 -1
- data/lib/capybara/apparition/driver.rb +29 -135
- data/lib/capybara/apparition/keyboard.rb +44 -43
- data/lib/capybara/apparition/launcher.rb +35 -36
- data/lib/capybara/apparition/mouse.rb +25 -21
- data/lib/capybara/apparition/node.rb +279 -309
- data/lib/capybara/apparition/node/drag.rb +148 -0
- data/lib/capybara/apparition/page.rb +64 -30
- data/lib/capybara/apparition/response.rb +41 -0
- data/lib/capybara/apparition/version.rb +1 -1
- metadata +5 -4
- data/lib/capybara/apparition/command.rb +0 -21
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara::Apparition
|
4
|
+
module Drag
|
5
|
+
def drag_to(other, delay: 0.1)
|
6
|
+
return html5_drag_to(other) if html5_draggable?
|
7
|
+
|
8
|
+
pos = visible_center
|
9
|
+
raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['drag_to']) if pos.nil?
|
10
|
+
|
11
|
+
test = mouse_event_test(pos)
|
12
|
+
raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['drag', test.selector, pos]) unless test.success
|
13
|
+
|
14
|
+
begin
|
15
|
+
@page.mouse.move_to(pos).down
|
16
|
+
sleep delay
|
17
|
+
|
18
|
+
other_pos = other.visible_center
|
19
|
+
raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['drag_to']) if other_pos.nil?
|
20
|
+
|
21
|
+
@page.mouse.move_to(other_pos.merge(button: 'left'))
|
22
|
+
sleep delay
|
23
|
+
ensure
|
24
|
+
@page.mouse.up
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def drag_by(x, y, delay: 0.1)
|
29
|
+
pos = visible_center
|
30
|
+
raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['hover']) if pos.nil?
|
31
|
+
|
32
|
+
other_pos = { x: pos[:x] + x, y: pos[:y] + y }
|
33
|
+
raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['drag', test['selector'], pos]) unless mouse_event_test?(pos)
|
34
|
+
|
35
|
+
@page.mouse.move_to(pos).down
|
36
|
+
sleep delay
|
37
|
+
@page.mouse.move_to(other_pos.merge(button: 'left'))
|
38
|
+
sleep delay
|
39
|
+
@page.mouse.up
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def html5_drag_to(element)
|
45
|
+
driver.execute_script MOUSEDOWN_TRACKER
|
46
|
+
scroll_if_needed
|
47
|
+
@page.mouse.move_to(element.visible_center).down
|
48
|
+
if driver.evaluate_script('window.capybara_mousedown_prevented')
|
49
|
+
element.scroll_if_needed
|
50
|
+
@page.mouse.move_to(element.visible_center).up
|
51
|
+
else
|
52
|
+
driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element
|
53
|
+
@page.mouse.up(element.visible_center)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def html5_draggable?
|
58
|
+
native.property('draggable')
|
59
|
+
end
|
60
|
+
|
61
|
+
MOUSEDOWN_TRACKER = <<~JS
|
62
|
+
document.addEventListener('mousedown', ev => {
|
63
|
+
window.capybara_mousedown_prevented = ev.defaultPrevented;
|
64
|
+
}, { once: true, passive: true })
|
65
|
+
JS
|
66
|
+
|
67
|
+
HTML5_DRAG_DROP_SCRIPT = <<~JS
|
68
|
+
var source = arguments[0];
|
69
|
+
var target = arguments[1];
|
70
|
+
|
71
|
+
function rectCenter(rect){
|
72
|
+
return new DOMPoint(
|
73
|
+
(rect.left + rect.right)/2,
|
74
|
+
(rect.top + rect.bottom)/2
|
75
|
+
);
|
76
|
+
}
|
77
|
+
|
78
|
+
function pointOnRect(pt, rect) {
|
79
|
+
var rectPt = rectCenter(rect);
|
80
|
+
var slope = (rectPt.y - pt.y) / (rectPt.x - pt.x);
|
81
|
+
|
82
|
+
if (pt.x <= rectPt.x) { // left side
|
83
|
+
var minXy = slope * (rect.left - pt.x) + pt.y;
|
84
|
+
if (rect.top <= minXy && minXy <= rect.bottom)
|
85
|
+
return new DOMPoint(rect.left, minXy);
|
86
|
+
}
|
87
|
+
|
88
|
+
if (pt.x >= rectPt.x) { // right side
|
89
|
+
var maxXy = slope * (rect.right - pt.x) + pt.y;
|
90
|
+
if (rect.top <= maxXy && maxXy <= rect.bottom)
|
91
|
+
return new DOMPoint(rect.right, maxXy);
|
92
|
+
}
|
93
|
+
|
94
|
+
if (pt.y <= rectPt.y) { // top side
|
95
|
+
var minYx = (rectPt.top - pt.y) / slope + pt.x;
|
96
|
+
if (rect.left <= minYx && minYx <= rect.right)
|
97
|
+
return new DOMPoint(minYx, rect.top);
|
98
|
+
}
|
99
|
+
|
100
|
+
if (pt.y >= rectPt.y) { // bottom side
|
101
|
+
var maxYx = (rect.bottom - pt.y) / slope + pt.x;
|
102
|
+
if (rect.left <= maxYx && maxYx <= rect.right)
|
103
|
+
return new DOMPoint(maxYx, rect.bottom);
|
104
|
+
}
|
105
|
+
|
106
|
+
return new DOMPoint(pt.x,pt.y);
|
107
|
+
}
|
108
|
+
|
109
|
+
var dt = new DataTransfer();
|
110
|
+
var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
111
|
+
|
112
|
+
if (source.tagName == 'A'){
|
113
|
+
dt.setData('text/uri-list', source.href);
|
114
|
+
dt.setData('text', source.href);
|
115
|
+
}
|
116
|
+
if (source.tagName == 'IMG'){
|
117
|
+
dt.setData('text/uri-list', source.src);
|
118
|
+
dt.setData('text', source.src);
|
119
|
+
}
|
120
|
+
|
121
|
+
var dragEvent = new DragEvent('dragstart', opts);
|
122
|
+
source.dispatchEvent(dragEvent);
|
123
|
+
target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
|
124
|
+
var targetRect = target.getBoundingClientRect();
|
125
|
+
var sourceCenter = rectCenter(source.getBoundingClientRect());
|
126
|
+
|
127
|
+
// fire 2 dragover events to simulate dragging with a direction
|
128
|
+
var entryPoint = pointOnRect(sourceCenter, targetRect)
|
129
|
+
var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
|
130
|
+
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
131
|
+
target.dispatchEvent(dragOverEvent);
|
132
|
+
|
133
|
+
var targetCenter = rectCenter(targetRect);
|
134
|
+
dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
|
135
|
+
dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
136
|
+
target.dispatchEvent(dragOverEvent);
|
137
|
+
|
138
|
+
var dragLeaveEvent = new DragEvent('dragleave', opts);
|
139
|
+
target.dispatchEvent(dragLeaveEvent);
|
140
|
+
if (dragOverEvent.defaultPrevented) {
|
141
|
+
var dropEvent = new DragEvent('drop', opts);
|
142
|
+
target.dispatchEvent(dropEvent);
|
143
|
+
}
|
144
|
+
var dragEndEvent = new DragEvent('dragend', opts);
|
145
|
+
source.dispatchEvent(dragEndEvent);
|
146
|
+
JS
|
147
|
+
end
|
148
|
+
end
|
@@ -12,20 +12,24 @@ module Capybara::Apparition
|
|
12
12
|
attr_accessor :perm_headers, :temp_headers, :temp_no_redirect_headers
|
13
13
|
attr_reader :network_traffic
|
14
14
|
|
15
|
-
def self.create(browser, session, id, ignore_https_errors:
|
15
|
+
def self.create(browser, session, id, ignore_https_errors: false, screenshot_task_queue: nil, js_errors: false)
|
16
16
|
session.command 'Page.enable'
|
17
|
-
|
18
|
-
|
17
|
+
|
18
|
+
# Provides a lot of info - but huge overhead
|
19
|
+
# session.command 'Page.setLifecycleEventsEnabled', enabled: true
|
19
20
|
|
20
21
|
page = Page.new(browser, session, id, ignore_https_errors, screenshot_task_queue, js_errors)
|
21
22
|
|
22
23
|
session.command 'Network.enable'
|
23
24
|
session.command 'Runtime.enable'
|
24
25
|
session.command 'Security.enable'
|
25
|
-
session.command 'Security.setOverrideCertificateErrors', override: true if ignore_https_errors
|
26
|
+
# session.command 'Security.setOverrideCertificateErrors', override: true if ignore_https_errors
|
27
|
+
session.command 'Security.setIgnoreCertificateErrors', ignore: !!ignore_https_errors
|
26
28
|
session.command 'DOM.enable'
|
27
29
|
# session.command 'Log.enable'
|
28
|
-
|
30
|
+
if Capybara.save_path
|
31
|
+
session.command 'Page.setDownloadBehavior', behavior: 'allow', downloadPath: Capybara.save_path
|
32
|
+
end
|
29
33
|
page
|
30
34
|
end
|
31
35
|
|
@@ -121,17 +125,23 @@ module Capybara::Apparition
|
|
121
125
|
params = {}
|
122
126
|
params[:paperWidth] = @browser.paper_size[:width].to_f if @browser.paper_size
|
123
127
|
params[:paperHeight] = @browser.paper_size[:height].to_f if @browser.paper_size
|
128
|
+
params[:scale] = @browser.zoom_factor if @browser.zoom_factor
|
124
129
|
command('Page.printToPDF', params)
|
125
130
|
else
|
126
|
-
if options[:selector]
|
131
|
+
clip_options = if options[:selector]
|
127
132
|
pos = evaluate("document.querySelector('#{options.delete(:selector)}').getBoundingClientRect().toJSON();")
|
128
|
-
|
133
|
+
%w[x y width height].each_with_object({}) { |key, hash| hash[key] = pos[key] }
|
129
134
|
elsif options[:full]
|
130
|
-
|
135
|
+
evaluate <<~JS
|
131
136
|
{ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight}
|
132
137
|
JS
|
133
|
-
|
138
|
+
else
|
139
|
+
evaluate <<~JS
|
140
|
+
{ width: window.innerWidth, height: window.innerHeight }
|
141
|
+
JS
|
134
142
|
end
|
143
|
+
options[:clip] = { x: 0, y: 0, scale: 1 }.merge(clip_options)
|
144
|
+
options[:clip][:scale] = @browser.zoom_factor || 1
|
135
145
|
command('Page.captureScreenshot', options)
|
136
146
|
end['data']
|
137
147
|
end
|
@@ -139,10 +149,11 @@ module Capybara::Apparition
|
|
139
149
|
def push_frame(frame_el)
|
140
150
|
node = command('DOM.describeNode', objectId: frame_el.base.id)
|
141
151
|
frame_id = node['node']['frameId']
|
142
|
-
|
152
|
+
|
153
|
+
timer = Capybara::Helpers.timer(expire_in: 10)
|
143
154
|
while (frame = @frames.get(frame_id)).nil? || frame.loading?
|
144
155
|
# Wait for the frame creation messages to be processed
|
145
|
-
if
|
156
|
+
if timer.expired?
|
146
157
|
puts 'Timed out waiting from frame to be ready'
|
147
158
|
# byebug
|
148
159
|
raise TimeoutError.new('push_frame')
|
@@ -217,10 +228,10 @@ module Capybara::Apparition
|
|
217
228
|
attr_reader :status_code
|
218
229
|
|
219
230
|
def wait_for_loaded(allow_obsolete: false)
|
220
|
-
|
231
|
+
timer = Capybara::Helpers.timer(expire_in: 10)
|
221
232
|
cf = current_frame
|
222
233
|
until cf.usable? || (allow_obsolete && cf.obsolete?) || @js_error
|
223
|
-
if
|
234
|
+
if timer.expired?
|
224
235
|
puts 'Timedout waiting for page to be loaded'
|
225
236
|
# byebug
|
226
237
|
raise TimeoutError.new('wait_for_loaded')
|
@@ -248,6 +259,9 @@ module Capybara::Apparition
|
|
248
259
|
navigate_opts = { url: url, transitionType: 'reload' }
|
249
260
|
navigate_opts[:referrer] = extra_headers['Referer'] if extra_headers['Referer']
|
250
261
|
response = command('Page.navigate', navigate_opts)
|
262
|
+
|
263
|
+
raise StatusFailError, 'args' => [url, response['errorText']] if response['errorText']
|
264
|
+
|
251
265
|
main_frame.loading(response['loaderId'])
|
252
266
|
wait_for_loaded
|
253
267
|
rescue TimeoutError
|
@@ -306,11 +320,11 @@ module Capybara::Apparition
|
|
306
320
|
end
|
307
321
|
|
308
322
|
def command(name, **params)
|
309
|
-
@browser.command_for_session(@session.session_id, name, params)
|
323
|
+
@browser.command_for_session(@session.session_id, name, params).result
|
310
324
|
end
|
311
325
|
|
312
326
|
def async_command(name, **params)
|
313
|
-
@browser.command_for_session(@session.session_id, name, params
|
327
|
+
@browser.command_for_session(@session.session_id, name, params).discard_result
|
314
328
|
end
|
315
329
|
|
316
330
|
def extra_headers
|
@@ -378,6 +392,8 @@ module Capybara::Apparition
|
|
378
392
|
|
379
393
|
@session.on 'Page.windowOpen' do |params|
|
380
394
|
puts "**** windowOpen was called with: #{params}" if ENV['DEBUG']
|
395
|
+
# TODO: find a better way to handle this
|
396
|
+
sleep 0.4 # wait a bit so the window has time to start loading
|
381
397
|
end
|
382
398
|
|
383
399
|
@session.on 'Page.frameAttached' do |params|
|
@@ -399,16 +415,31 @@ module Capybara::Apparition
|
|
399
415
|
end
|
400
416
|
end
|
401
417
|
|
402
|
-
@session.on 'Page.
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
418
|
+
@session.on 'Page.frameStartedLoading' do |params|
|
419
|
+
@frames.get(params['frameId'])&.loading(-1)
|
420
|
+
end
|
421
|
+
|
422
|
+
@session.on 'Page.frameStoppedLoading' do |params|
|
423
|
+
@frames.get(params['frameId'])&.loaded!
|
424
|
+
end
|
425
|
+
|
426
|
+
# @session.on 'Page.lifecycleEvent' do |params|
|
427
|
+
# # Provides a lot of useful info - but lots of overhead
|
428
|
+
# puts "Lifecycle: #{params['name']} - frame: #{params['frameId']} - loader: #{params['loaderId']}" if ENV['DEBUG']
|
429
|
+
# case params['name']
|
430
|
+
# when 'init'
|
431
|
+
# @frames.get(params['frameId'])&.loading(params['loaderId'])
|
432
|
+
# when 'firstMeaningfulPaint',
|
433
|
+
# 'networkIdle'
|
434
|
+
# @frames.get(params['frameId']).tap do |frame|
|
435
|
+
# frame.loaded! if frame.loader_id == params['loaderId']
|
436
|
+
# end
|
437
|
+
# end
|
438
|
+
# end
|
439
|
+
|
440
|
+
@session.on('Page.domContentEventFired') do |params|
|
441
|
+
# TODO: Really need something better than this
|
442
|
+
main_frame.loaded! if @status_code != 200
|
412
443
|
end
|
413
444
|
|
414
445
|
@session.on 'Page.navigatedWithinDocument' do |params|
|
@@ -466,7 +497,7 @@ module Capybara::Apparition
|
|
466
497
|
@session.on 'Network.loadingFailed' do |params|
|
467
498
|
req = @network_traffic.find { |request| request.request_id == params['requestId'] }
|
468
499
|
req&.blocked_params = params if params['blockedReason']
|
469
|
-
puts "Loading Failed - request: #{params['requestId']}: #{params['errorText']}" if params['type'] == 'Document'
|
500
|
+
puts "Loading Failed - request: #{params['requestId']} : #{params['errorText']}" if params['type'] == 'Document'
|
470
501
|
end
|
471
502
|
|
472
503
|
@session.on 'Network.requestIntercepted' do |params|
|
@@ -484,6 +515,10 @@ module Capybara::Apparition
|
|
484
515
|
)
|
485
516
|
end
|
486
517
|
|
518
|
+
# @session.on 'Security.certificateError' do |params|
|
519
|
+
# async_command 'Network.continueInterceptedRequest', interceptionId: id, **params
|
520
|
+
# end
|
521
|
+
|
487
522
|
# @session.on 'Log.entryAdded' do |params|
|
488
523
|
# log_entry = params['entry']
|
489
524
|
# if params.values_at('source', 'level') == ['javascript', 'error']
|
@@ -634,7 +669,6 @@ module Capybara::Apparition
|
|
634
669
|
def decode_result(result, object_cache = {})
|
635
670
|
if result['type'] == 'object'
|
636
671
|
if result['subtype'] == 'array'
|
637
|
-
# remoteObject = @browser.command('Runtime.getProperties',
|
638
672
|
remote_object = command('Runtime.getProperties',
|
639
673
|
objectId: result['objectId'],
|
640
674
|
ownProperties: true)
|
@@ -655,7 +689,7 @@ module Capybara::Apparition
|
|
655
689
|
|
656
690
|
# releasePromises = [helper.releaseObject(@element._client, remoteObject)]
|
657
691
|
end
|
658
|
-
|
692
|
+
command('Runtime.releaseObject', objectId: result['objectId'])
|
659
693
|
return results
|
660
694
|
elsif result['subtype'] == 'node'
|
661
695
|
return result
|
@@ -667,10 +701,10 @@ module Capybara::Apparition
|
|
667
701
|
.find { |prop| prop['name'] == '[[StableObjectId]]' }
|
668
702
|
.dig('value', 'value')
|
669
703
|
# We could actually return cyclic objects here but Capybara would need to be updated to support
|
670
|
-
return '(cyclic structure)' if object_cache.key?(stable_id)
|
671
|
-
|
672
704
|
# return object_cache[stable_id] if object_cache.key?(stable_id)
|
673
705
|
|
706
|
+
return '(cyclic structure)' if object_cache.key?(stable_id)
|
707
|
+
|
674
708
|
object_cache[stable_id] = {}
|
675
709
|
properties = remote_object['result']
|
676
710
|
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara::Apparition
|
4
|
+
class ChromeClient
|
5
|
+
class Response
|
6
|
+
def initialize(client, *msg_ids, send_time: nil)
|
7
|
+
@send_time = send_time
|
8
|
+
@msg_ids = msg_ids
|
9
|
+
@client = client
|
10
|
+
end
|
11
|
+
|
12
|
+
def result
|
13
|
+
response = @msg_ids.map do |id|
|
14
|
+
resp = @client.send(:wait_for_msg_response, id)
|
15
|
+
handle_error(resp['error']) if resp['error']
|
16
|
+
resp
|
17
|
+
end.last
|
18
|
+
puts "Processed msg: #{@msg_ids.last} in #{Time.now - @send_time} seconds" if ENV['DEBUG']
|
19
|
+
|
20
|
+
response['result']
|
21
|
+
end
|
22
|
+
|
23
|
+
def discard_result
|
24
|
+
@msg_ids.each { |id| @client.add_async_id id }
|
25
|
+
@result_time = Time.now
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def handle_error(error)
|
32
|
+
case error['code']
|
33
|
+
when -32_000
|
34
|
+
raise WrongWorld.new(nil, error)
|
35
|
+
else
|
36
|
+
raise CDPError.new(error)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: apparition
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Walpole
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-01-
|
11
|
+
date: 2019-01-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: capybara
|
@@ -197,7 +197,6 @@ files:
|
|
197
197
|
- lib/capybara/apparition.rb
|
198
198
|
- lib/capybara/apparition/browser.rb
|
199
199
|
- lib/capybara/apparition/chrome_client.rb
|
200
|
-
- lib/capybara/apparition/command.rb
|
201
200
|
- lib/capybara/apparition/cookie.rb
|
202
201
|
- lib/capybara/apparition/dev_tools_protocol/session.rb
|
203
202
|
- lib/capybara/apparition/dev_tools_protocol/target.rb
|
@@ -215,7 +214,9 @@ files:
|
|
215
214
|
- lib/capybara/apparition/network_traffic/request.rb
|
216
215
|
- lib/capybara/apparition/network_traffic/response.rb
|
217
216
|
- lib/capybara/apparition/node.rb
|
217
|
+
- lib/capybara/apparition/node/drag.rb
|
218
218
|
- lib/capybara/apparition/page.rb
|
219
|
+
- lib/capybara/apparition/response.rb
|
219
220
|
- lib/capybara/apparition/utility.rb
|
220
221
|
- lib/capybara/apparition/version.rb
|
221
222
|
- lib/capybara/apparition/web_socket_client.rb
|
@@ -238,7 +239,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
238
239
|
- !ruby/object:Gem::Version
|
239
240
|
version: '0'
|
240
241
|
requirements: []
|
241
|
-
rubygems_version: 3.0.
|
242
|
+
rubygems_version: 3.0.1
|
242
243
|
signing_key:
|
243
244
|
specification_version: 4
|
244
245
|
summary: Chrome driver using CDP for Capybara
|
@@ -1,21 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'securerandom'
|
4
|
-
|
5
|
-
module Capybara::Apparition
|
6
|
-
class Command
|
7
|
-
attr_reader :id
|
8
|
-
attr_reader :name
|
9
|
-
attr_accessor :args
|
10
|
-
|
11
|
-
def initialize(name, params = {})
|
12
|
-
@id = SecureRandom.uuid
|
13
|
-
@name = name
|
14
|
-
@params = params
|
15
|
-
end
|
16
|
-
|
17
|
-
def message
|
18
|
-
JSON.dump('id' => @id, 'name' => @name, 'params' => @params)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|