poltergeist 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +0 -0
- data/LICENSE +20 -0
- data/README.md +85 -0
- data/lib/capybara/poltergeist/browser.rb +130 -0
- data/lib/capybara/poltergeist/client/agent.coffee +175 -0
- data/lib/capybara/poltergeist/client/browser.coffee +121 -0
- data/lib/capybara/poltergeist/client/compiled/agent.js +198 -0
- data/lib/capybara/poltergeist/client/compiled/browser.js +117 -0
- data/lib/capybara/poltergeist/client/compiled/connection.js +20 -0
- data/lib/capybara/poltergeist/client/compiled/main.js +38 -0
- data/lib/capybara/poltergeist/client/compiled/node.js +74 -0
- data/lib/capybara/poltergeist/client/compiled/web_page.js +136 -0
- data/lib/capybara/poltergeist/client/connection.coffee +11 -0
- data/lib/capybara/poltergeist/client/main.coffee +27 -0
- data/lib/capybara/poltergeist/client/node.coffee +56 -0
- data/lib/capybara/poltergeist/client/web_page.coffee +102 -0
- data/lib/capybara/poltergeist/client.rb +57 -0
- data/lib/capybara/poltergeist/driver.rb +85 -0
- data/lib/capybara/poltergeist/errors.rb +26 -0
- data/lib/capybara/poltergeist/node.rb +92 -0
- data/lib/capybara/poltergeist/server.rb +35 -0
- data/lib/capybara/poltergeist/server_manager.rb +118 -0
- data/lib/capybara/poltergeist/version.rb +5 -0
- data/lib/capybara/poltergeist.rb +20 -0
- metadata +102 -0
data/CHANGELOG.md
ADDED
File without changes
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Jonathan Leighton
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# Poltergeist - A PhantomJS driver for Capybara #
|
2
|
+
|
3
|
+
Version: 0.1.0
|
4
|
+
|
5
|
+
Poltergeist is a driver for [Capybara](https://github.com/jnicklas/capybara). It allows you to
|
6
|
+
run your Capybara tests on a headless [WebKit](http://webkit.org) browser,
|
7
|
+
provided by [PhantomJS](http://www.phantomjs.org/).
|
8
|
+
|
9
|
+
## Installation ##
|
10
|
+
|
11
|
+
Add `poltergeist` to your Gemfile, and add `Capybara.javascript_driver = :poltergeist`
|
12
|
+
in your test setup.
|
13
|
+
|
14
|
+
You will also need PhantomJS 1.3+ on your system.
|
15
|
+
[Here's how to do that](http://code.google.com/p/phantomjs/wiki/BuildInstructions).
|
16
|
+
|
17
|
+
Currently PhantomJS is not 'truly headless', so to run it on a continuous integration
|
18
|
+
server you will need to use [Xvfb](http://en.wikipedia.org/wiki/Xvfb). You can either use the
|
19
|
+
[headless gem](https://github.com/leonid-shevtsov/headless) for this,
|
20
|
+
or make sure that Xvfb is running and the `DISPLAY` environment variable is set.
|
21
|
+
|
22
|
+
## What's supported? ##
|
23
|
+
|
24
|
+
Poltergeist supports basically everything that is supported by the stock Selenium driver,
|
25
|
+
including Javascript, drag-and-drop, etc.
|
26
|
+
|
27
|
+
Additionally, you can grab screenshots of the page at any point by calling
|
28
|
+
`page.driver.render('/path/to/file.png')` (this works the same way as the PhantomJS
|
29
|
+
render feature, so you can specify other extensions like `.pdf`, `.gif`, etc.)
|
30
|
+
|
31
|
+
## Customisation ##
|
32
|
+
|
33
|
+
You can customise the way that Capybara sets up Poltegeist via the following code in your
|
34
|
+
test setup:
|
35
|
+
|
36
|
+
Capybara.register_driver :poltergeist do |app|
|
37
|
+
Capybara::Poltergeist::Driver.new(app, options)
|
38
|
+
end
|
39
|
+
|
40
|
+
`options` is a hash of options. The following options are supported:
|
41
|
+
|
42
|
+
* `:phantomjs` (String) - A custom path to the phantomjs executable
|
43
|
+
* `:debug` (Boolean) - When true, debug output is logged to `STDERR`
|
44
|
+
* `:logger` (Object responding to `puts`) - When present, debug output is written to this object
|
45
|
+
|
46
|
+
## Bugs ##
|
47
|
+
|
48
|
+
Please file bug reports on Github and include example code to reproduce the problem wherever
|
49
|
+
possible. (Tests are even better.)
|
50
|
+
|
51
|
+
## Why not use [capybara-webkit](https://github.com/thoughtbot/capybara-webkit)? ##
|
52
|
+
|
53
|
+
If capybara-webkit works for you, then by all means carry on using it.
|
54
|
+
|
55
|
+
However, I have had some trouble with it, and Poltergeist basically started
|
56
|
+
as an experiment to see whether a PhantomJS driver was possible. (It turned out it
|
57
|
+
was, but only thanks to some new features in the recent 1.3.0 release.)
|
58
|
+
|
59
|
+
In the long term, I think having a PhantomJS driver makes sense, because that allows
|
60
|
+
PhantomJS to concentrate on being an awesome headless browser, while the capybara driver
|
61
|
+
(Poltergeist) is able to be the minimal amount of glue code necessary to drive the
|
62
|
+
browser.
|
63
|
+
|
64
|
+
## License ##
|
65
|
+
|
66
|
+
Copyright (c) 2011 Jonathan Leighton
|
67
|
+
|
68
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
69
|
+
a copy of this software and associated documentation files (the
|
70
|
+
"Software"), to deal in the Software without restriction, including
|
71
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
72
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
73
|
+
permit persons to whom the Software is furnished to do so, subject to
|
74
|
+
the following conditions:
|
75
|
+
|
76
|
+
The above copyright notice and this permission notice shall be
|
77
|
+
included in all copies or substantial portions of the Software.
|
78
|
+
|
79
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
80
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
81
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
82
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
83
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
84
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
85
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Capybara::Poltergeist
|
4
|
+
class Browser
|
5
|
+
attr_reader :options, :server, :client
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@options = options
|
9
|
+
@server = Server.new
|
10
|
+
@client = Client.new(server.port, options[:phantomjs])
|
11
|
+
end
|
12
|
+
|
13
|
+
def restart
|
14
|
+
server.restart
|
15
|
+
client.restart
|
16
|
+
end
|
17
|
+
|
18
|
+
def visit(url, attributes = {})
|
19
|
+
command 'visit', url
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_url
|
23
|
+
command 'current_url'
|
24
|
+
end
|
25
|
+
|
26
|
+
def body
|
27
|
+
command 'body'
|
28
|
+
end
|
29
|
+
|
30
|
+
def source
|
31
|
+
command 'source'
|
32
|
+
end
|
33
|
+
|
34
|
+
def find(selector, id = nil)
|
35
|
+
command 'find', selector, id
|
36
|
+
end
|
37
|
+
|
38
|
+
def text(id)
|
39
|
+
command 'text', id
|
40
|
+
end
|
41
|
+
|
42
|
+
def attribute(id, name)
|
43
|
+
command 'attribute', id, name
|
44
|
+
end
|
45
|
+
|
46
|
+
def value(id)
|
47
|
+
command 'value', id
|
48
|
+
end
|
49
|
+
|
50
|
+
def set(id, value)
|
51
|
+
command 'set', id, value
|
52
|
+
end
|
53
|
+
|
54
|
+
def select_file(id, value)
|
55
|
+
command 'select_file', id, value
|
56
|
+
end
|
57
|
+
|
58
|
+
def tag_name(id)
|
59
|
+
command('tag_name', id).downcase
|
60
|
+
end
|
61
|
+
|
62
|
+
def visible?(id)
|
63
|
+
command 'visible', id
|
64
|
+
end
|
65
|
+
|
66
|
+
def evaluate(script)
|
67
|
+
command 'evaluate', script
|
68
|
+
end
|
69
|
+
|
70
|
+
def execute(script)
|
71
|
+
command 'execute', script
|
72
|
+
end
|
73
|
+
|
74
|
+
def within_frame(id, &block)
|
75
|
+
command 'push_frame', id
|
76
|
+
yield
|
77
|
+
command 'pop_frame'
|
78
|
+
end
|
79
|
+
|
80
|
+
def reset
|
81
|
+
visit('about:blank')
|
82
|
+
end
|
83
|
+
|
84
|
+
def click(id)
|
85
|
+
command 'click', id
|
86
|
+
end
|
87
|
+
|
88
|
+
def drag(id, other_id)
|
89
|
+
command 'drag', id, other_id
|
90
|
+
end
|
91
|
+
|
92
|
+
def select(id, value)
|
93
|
+
command 'select', id, value
|
94
|
+
end
|
95
|
+
|
96
|
+
def trigger(id, event)
|
97
|
+
command 'trigger', id, event
|
98
|
+
end
|
99
|
+
|
100
|
+
def reset
|
101
|
+
command 'reset'
|
102
|
+
end
|
103
|
+
|
104
|
+
def render(path)
|
105
|
+
command 'render', path
|
106
|
+
end
|
107
|
+
|
108
|
+
def logger
|
109
|
+
options[:logger]
|
110
|
+
end
|
111
|
+
|
112
|
+
def log(message)
|
113
|
+
logger.puts message if logger
|
114
|
+
end
|
115
|
+
|
116
|
+
def command(name, *args)
|
117
|
+
message = { 'name' => name, 'args' => args }
|
118
|
+
log message.inspect
|
119
|
+
|
120
|
+
json = JSON.parse(server.send(JSON.generate(message)))
|
121
|
+
log json.inspect
|
122
|
+
|
123
|
+
if json['error']
|
124
|
+
raise BrowserError.new(json['error'])
|
125
|
+
else
|
126
|
+
json['response']
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# This is injected into each page that is loaded
|
2
|
+
|
3
|
+
class PoltergeistAgent
|
4
|
+
constructor: ->
|
5
|
+
@elements = []
|
6
|
+
@nodes = {}
|
7
|
+
@windows = []
|
8
|
+
this.pushWindow(window)
|
9
|
+
|
10
|
+
pushWindow: (new_window) ->
|
11
|
+
@windows.push(new_window)
|
12
|
+
|
13
|
+
@window = new_window
|
14
|
+
@document = @window.document
|
15
|
+
|
16
|
+
popWindow: ->
|
17
|
+
@windows.pop()
|
18
|
+
|
19
|
+
@window = @windows[@windows.length - 1]
|
20
|
+
@document = @window.document
|
21
|
+
|
22
|
+
pushFrame: (id) ->
|
23
|
+
this.pushWindow @document.getElementById(id).contentWindow
|
24
|
+
|
25
|
+
popFrame: ->
|
26
|
+
this.popWindow()
|
27
|
+
|
28
|
+
currentUrl: ->
|
29
|
+
window.location.toString()
|
30
|
+
|
31
|
+
find: (selector, id) ->
|
32
|
+
context = if id? then @elements[id] else @document
|
33
|
+
results = @document.evaluate(selector, context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
|
34
|
+
ids = []
|
35
|
+
|
36
|
+
for i in [0...results.snapshotLength]
|
37
|
+
ids.push(this.register(results.snapshotItem(i)))
|
38
|
+
|
39
|
+
ids
|
40
|
+
|
41
|
+
register: (element) ->
|
42
|
+
@elements.push(element)
|
43
|
+
@elements.length - 1
|
44
|
+
|
45
|
+
documentSize: ->
|
46
|
+
height: @document.documentElement.scrollHeight,
|
47
|
+
width: @document.documentElement.scrollWidth
|
48
|
+
|
49
|
+
get: (id) ->
|
50
|
+
@nodes[id] or= new PoltergeistAgent.Node(this, @elements[id])
|
51
|
+
|
52
|
+
nodeCall: (id, name, arguments) ->
|
53
|
+
node = this.get(id)
|
54
|
+
node[name].apply(node, arguments)
|
55
|
+
|
56
|
+
class PoltergeistAgent.Node
|
57
|
+
@EVENTS = {
|
58
|
+
FOCUS: ['blur', 'focus', 'focusin', 'focusout'],
|
59
|
+
MOUSE: ['click', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove',
|
60
|
+
'mouseover', 'mouseout', 'mouseup']
|
61
|
+
}
|
62
|
+
|
63
|
+
constructor: (@agent, @element) ->
|
64
|
+
|
65
|
+
parentId: ->
|
66
|
+
@agent.register(@element.parentNode)
|
67
|
+
|
68
|
+
isObsolete: ->
|
69
|
+
obsolete = (element) =>
|
70
|
+
if element.parentNode?
|
71
|
+
if element.parentNode == @agent.document
|
72
|
+
false
|
73
|
+
else
|
74
|
+
obsolete element.parentNode
|
75
|
+
else
|
76
|
+
true
|
77
|
+
obsolete @element
|
78
|
+
|
79
|
+
changed: ->
|
80
|
+
event = document.createEvent('HTMLEvents')
|
81
|
+
event.initEvent("change", true, false)
|
82
|
+
@element.dispatchEvent(event)
|
83
|
+
|
84
|
+
text: ->
|
85
|
+
@element.textContent
|
86
|
+
|
87
|
+
getAttribute: (name) ->
|
88
|
+
if name == 'checked' || name == 'selected'
|
89
|
+
@element[name]
|
90
|
+
else
|
91
|
+
@element.getAttribute(name)
|
92
|
+
|
93
|
+
value: ->
|
94
|
+
if @element.tagName == 'SELECT' && @element.multiple
|
95
|
+
option.value for option in @element.children when option.selected
|
96
|
+
else
|
97
|
+
@element.value
|
98
|
+
|
99
|
+
set: (value) ->
|
100
|
+
if (@element.maxLength >= 0)
|
101
|
+
value = value.substr(0, @element.maxLength)
|
102
|
+
|
103
|
+
@element.value = value
|
104
|
+
this.changed()
|
105
|
+
|
106
|
+
isMultiple: ->
|
107
|
+
@element.multiple
|
108
|
+
|
109
|
+
setAttribute: (name, value) ->
|
110
|
+
@element.setAttribute(name, value)
|
111
|
+
|
112
|
+
removeAttribute: (name) ->
|
113
|
+
@element.removeAttribute(name)
|
114
|
+
|
115
|
+
select: (value) ->
|
116
|
+
if value == false && !@element.parentNode.multiple
|
117
|
+
false
|
118
|
+
else
|
119
|
+
@element.selected = value
|
120
|
+
this.changed()
|
121
|
+
true
|
122
|
+
|
123
|
+
tagName: ->
|
124
|
+
@element.tagName
|
125
|
+
|
126
|
+
elementVisible: (element) ->
|
127
|
+
|
128
|
+
isVisible: (id) ->
|
129
|
+
visible = (element) ->
|
130
|
+
if @window.getComputedStyle(element).display == 'none'
|
131
|
+
false
|
132
|
+
else if element.parentElement
|
133
|
+
visible element.parentElement
|
134
|
+
else
|
135
|
+
true
|
136
|
+
visible @element
|
137
|
+
|
138
|
+
position: (id) ->
|
139
|
+
pos = (element) ->
|
140
|
+
x = element.offsetLeft
|
141
|
+
y = element.offsetTop
|
142
|
+
|
143
|
+
if element.offsetParent
|
144
|
+
parentPos = pos(element.offsetParent)
|
145
|
+
|
146
|
+
x += parentPos.x
|
147
|
+
y += parentPos.y
|
148
|
+
|
149
|
+
{ x: x, y: y }
|
150
|
+
pos @element
|
151
|
+
|
152
|
+
trigger: (name) ->
|
153
|
+
if Node.EVENTS.MOUSE.indexOf(name) != -1
|
154
|
+
event = document.createEvent('MouseEvent')
|
155
|
+
event.initMouseEvent(
|
156
|
+
name, true, true, @agent.window, 0, 0, 0, 0, 0,
|
157
|
+
false, false, false, false, 0, null
|
158
|
+
)
|
159
|
+
else if Node.EVENTS.FOCUS.indexOf(name) != -1
|
160
|
+
event = document.createEvent('HTMLEvents')
|
161
|
+
event.initEvent(name, true, true)
|
162
|
+
else
|
163
|
+
throw "Unknown event"
|
164
|
+
|
165
|
+
@element.dispatchEvent(event)
|
166
|
+
|
167
|
+
window.__poltergeist = new PoltergeistAgent
|
168
|
+
|
169
|
+
document.addEventListener(
|
170
|
+
'DOMContentLoaded',
|
171
|
+
-> console.log('__DOMContentLoaded')
|
172
|
+
)
|
173
|
+
|
174
|
+
# Important to return true here - Phantom seems to choke otherwise
|
175
|
+
true
|
@@ -0,0 +1,121 @@
|
|
1
|
+
class Poltergeist.Browser
|
2
|
+
constructor: (@owner) ->
|
3
|
+
@awaiting_response = false
|
4
|
+
this.resetPage()
|
5
|
+
|
6
|
+
resetPage: ->
|
7
|
+
@page.release() if @page?
|
8
|
+
|
9
|
+
@page = new Poltergeist.WebPage
|
10
|
+
@page.onLoadFinished = (status) =>
|
11
|
+
if @awaiting_response
|
12
|
+
@owner.sendResponse(status)
|
13
|
+
@awaiting_response = false
|
14
|
+
|
15
|
+
visit: (url) ->
|
16
|
+
@awaiting_response = true
|
17
|
+
@page.open(url)
|
18
|
+
|
19
|
+
current_url: ->
|
20
|
+
@owner.sendResponse @page.currentUrl()
|
21
|
+
|
22
|
+
body: ->
|
23
|
+
@owner.sendResponse @page.content()
|
24
|
+
|
25
|
+
source: ->
|
26
|
+
@owner.sendResponse @page.source()
|
27
|
+
|
28
|
+
find: (selector, id) ->
|
29
|
+
@owner.sendResponse @page.find(selector, id)
|
30
|
+
|
31
|
+
text: (id) ->
|
32
|
+
@owner.sendResponse @page.get(id).text()
|
33
|
+
|
34
|
+
attribute: (id, name) ->
|
35
|
+
@owner.sendResponse @page.get(id).getAttribute(name)
|
36
|
+
|
37
|
+
value: (id) ->
|
38
|
+
@owner.sendResponse @page.get(id).value()
|
39
|
+
|
40
|
+
set: (id, value) ->
|
41
|
+
@page.get(id).set(value)
|
42
|
+
@owner.sendResponse(true)
|
43
|
+
|
44
|
+
# PhantomJS only allows us to reference the element by CSS selector, not XPath,
|
45
|
+
# so we have to add an attribute to the element to identify it, then remove it
|
46
|
+
# afterwards.
|
47
|
+
#
|
48
|
+
# PhantomJS does not support multiple-file inputs, so we have to blatently cheat
|
49
|
+
# by temporarily changing it to a single-file input. This obviously could break
|
50
|
+
# things in various ways, which is not ideal, but it works in the simplest case.
|
51
|
+
select_file: (id, value) ->
|
52
|
+
element = @page.get(id)
|
53
|
+
|
54
|
+
multiple = element.isMultiple()
|
55
|
+
|
56
|
+
element.removeAttribute('multiple') if multiple
|
57
|
+
element.setAttribute('_poltergeist_selected', '')
|
58
|
+
|
59
|
+
@page.uploadFile('[_poltergeist_selected]', value)
|
60
|
+
|
61
|
+
element.removeAttribute('_poltergeist_selected')
|
62
|
+
element.setAttribute('multiple', 'multiple') if multiple
|
63
|
+
|
64
|
+
@owner.sendResponse(true)
|
65
|
+
|
66
|
+
select: (id, value) ->
|
67
|
+
@owner.sendResponse @page.get(id).select(value)
|
68
|
+
|
69
|
+
tag_name: (id) ->
|
70
|
+
@owner.sendResponse @page.get(id).tagName()
|
71
|
+
|
72
|
+
visible: (id) ->
|
73
|
+
@owner.sendResponse @page.get(id).isVisible()
|
74
|
+
|
75
|
+
evaluate: (script) ->
|
76
|
+
@owner.sendResponse @page.evaluate("function() { return #{script} }")
|
77
|
+
|
78
|
+
execute: (script) ->
|
79
|
+
@page.execute("function() { return #{script} }")
|
80
|
+
@owner.sendResponse(true)
|
81
|
+
|
82
|
+
push_frame: (id) ->
|
83
|
+
@page.pushFrame(id)
|
84
|
+
@owner.sendResponse(true)
|
85
|
+
|
86
|
+
pop_frame: ->
|
87
|
+
@page.popFrame()
|
88
|
+
@owner.sendResponse(true)
|
89
|
+
|
90
|
+
click: (id) ->
|
91
|
+
# Detect if the click event triggers a page load. If it does, don't send
|
92
|
+
# a response here, because the response will be sent once the page has loaded.
|
93
|
+
@page.onLoadStarted = => @awaiting_response = true
|
94
|
+
|
95
|
+
@page.get(id).click()
|
96
|
+
|
97
|
+
# Use a timeout in order to let the stack clear, so that the @page.onLoadStarted
|
98
|
+
# callback can (possibly) fire, before we decide whether to send a response.
|
99
|
+
setTimeout(
|
100
|
+
=>
|
101
|
+
@page.onLoadStarted = null
|
102
|
+
@owner.sendResponse(true) unless @awaiting_response
|
103
|
+
,
|
104
|
+
0
|
105
|
+
)
|
106
|
+
|
107
|
+
drag: (id, other_id) ->
|
108
|
+
@page.get(id).dragTo(@page.get(other_id))
|
109
|
+
@owner.sendResponse(true)
|
110
|
+
|
111
|
+
trigger: (id, event) ->
|
112
|
+
@page.get(id).trigger(event)
|
113
|
+
@owner.sendResponse(event)
|
114
|
+
|
115
|
+
reset: ->
|
116
|
+
this.resetPage()
|
117
|
+
@owner.sendResponse(true)
|
118
|
+
|
119
|
+
render: (path) ->
|
120
|
+
@page.render(path)
|
121
|
+
@owner.sendResponse(true)
|