poltergeist-cj 1.5.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.
- data/LICENSE +20 -0
- data/README.md +434 -0
- data/lib/capybara/poltergeist.rb +27 -0
- data/lib/capybara/poltergeist/browser.rb +339 -0
- data/lib/capybara/poltergeist/client.rb +151 -0
- data/lib/capybara/poltergeist/client/agent.coffee +374 -0
- data/lib/capybara/poltergeist/client/browser.coffee +393 -0
- data/lib/capybara/poltergeist/client/compiled/agent.js +535 -0
- data/lib/capybara/poltergeist/client/compiled/browser.js +526 -0
- data/lib/capybara/poltergeist/client/compiled/connection.js +25 -0
- data/lib/capybara/poltergeist/client/compiled/main.js +204 -0
- data/lib/capybara/poltergeist/client/compiled/node.js +77 -0
- data/lib/capybara/poltergeist/client/compiled/web_page.js +421 -0
- data/lib/capybara/poltergeist/client/connection.coffee +11 -0
- data/lib/capybara/poltergeist/client/main.coffee +89 -0
- data/lib/capybara/poltergeist/client/node.coffee +57 -0
- data/lib/capybara/poltergeist/client/web_page.coffee +297 -0
- data/lib/capybara/poltergeist/cookie.rb +35 -0
- data/lib/capybara/poltergeist/driver.rb +278 -0
- data/lib/capybara/poltergeist/errors.rb +178 -0
- data/lib/capybara/poltergeist/inspector.rb +46 -0
- data/lib/capybara/poltergeist/json.rb +25 -0
- data/lib/capybara/poltergeist/network_traffic.rb +6 -0
- data/lib/capybara/poltergeist/network_traffic/request.rb +26 -0
- data/lib/capybara/poltergeist/network_traffic/response.rb +40 -0
- data/lib/capybara/poltergeist/node.rb +154 -0
- data/lib/capybara/poltergeist/server.rb +36 -0
- data/lib/capybara/poltergeist/utility.rb +9 -0
- data/lib/capybara/poltergeist/version.rb +5 -0
- data/lib/capybara/poltergeist/web_socket_server.rb +96 -0
- metadata +285 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Capybara::Poltergeist
|
|
2
|
+
class Cookie
|
|
3
|
+
def initialize(attributes)
|
|
4
|
+
@attributes = attributes
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def name
|
|
8
|
+
@attributes['name']
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def value
|
|
12
|
+
@attributes['value']
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def domain
|
|
16
|
+
@attributes['domain']
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def path
|
|
20
|
+
@attributes['path']
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def secure?
|
|
24
|
+
@attributes['secure']
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def httponly?
|
|
28
|
+
@attributes['httponly']
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def expires
|
|
32
|
+
Time.at @attributes['expiry'] if @attributes['expiry']
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
|
|
3
|
+
module Capybara::Poltergeist
|
|
4
|
+
class Driver < Capybara::Driver::Base
|
|
5
|
+
DEFAULT_TIMEOUT = 30
|
|
6
|
+
|
|
7
|
+
attr_reader :app, :server, :client, :browser, :options
|
|
8
|
+
|
|
9
|
+
def initialize(app, options = {})
|
|
10
|
+
@app = app
|
|
11
|
+
@options = options
|
|
12
|
+
@browser = nil
|
|
13
|
+
@inspector = nil
|
|
14
|
+
@server = nil
|
|
15
|
+
@client = nil
|
|
16
|
+
@started = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def needs_server?
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def browser
|
|
24
|
+
@browser ||= begin
|
|
25
|
+
browser = Browser.new(server, client, logger)
|
|
26
|
+
browser.js_errors = options[:js_errors] if options.key?(:js_errors)
|
|
27
|
+
browser.extensions = options.fetch(:extensions, [])
|
|
28
|
+
browser.debug = true if options[:debug]
|
|
29
|
+
browser
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def inspector
|
|
34
|
+
@inspector ||= options[:inspector] && Inspector.new(options[:inspector])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def server
|
|
38
|
+
@server ||= Server.new(options[:port], options.fetch(:timeout) { DEFAULT_TIMEOUT })
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def client
|
|
42
|
+
@client ||= Client.start(server,
|
|
43
|
+
:path => options[:phantomjs],
|
|
44
|
+
:window_size => options[:window_size],
|
|
45
|
+
:phantomjs_options => phantomjs_options,
|
|
46
|
+
:phantomjs_logger => phantomjs_logger
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def phantomjs_options
|
|
51
|
+
list = options[:phantomjs_options] || []
|
|
52
|
+
list += ["--remote-debugger-port=#{inspector.port}", "--remote-debugger-autorun=yes"] if inspector
|
|
53
|
+
list
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def client_pid
|
|
57
|
+
client.pid
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def timeout
|
|
61
|
+
server.timeout
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def timeout=(sec)
|
|
65
|
+
server.timeout = sec
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def restart
|
|
69
|
+
browser.restart
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def quit
|
|
73
|
+
server.stop
|
|
74
|
+
client.stop
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# logger should be an object that responds to puts, or nil
|
|
78
|
+
def logger
|
|
79
|
+
options[:logger] || (options[:debug] && STDERR)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# logger should be an object that behaves like IO or nil
|
|
83
|
+
def phantomjs_logger
|
|
84
|
+
options.fetch(:phantomjs_logger, nil)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def visit(url)
|
|
88
|
+
@started = true
|
|
89
|
+
browser.visit(url)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def current_url
|
|
93
|
+
browser.current_url
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def status_code
|
|
97
|
+
browser.status_code
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def html
|
|
101
|
+
browser.body
|
|
102
|
+
end
|
|
103
|
+
alias_method :body, :html
|
|
104
|
+
|
|
105
|
+
def source
|
|
106
|
+
browser.source.to_s
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def title
|
|
110
|
+
browser.title
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def find(method, selector)
|
|
114
|
+
browser.find(method, selector).map { |page_id, id| Capybara::Poltergeist::Node.new(self, page_id, id) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def find_xpath(selector)
|
|
118
|
+
find :xpath, selector
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def find_css(selector)
|
|
122
|
+
find :css, selector
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def click(x, y)
|
|
126
|
+
browser.click_coordinates(x, y)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def evaluate_script(script)
|
|
130
|
+
browser.evaluate(script)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def execute_script(script)
|
|
134
|
+
browser.execute(script)
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def within_frame(name, &block)
|
|
139
|
+
browser.within_frame(name, &block)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def within_window(name, &block)
|
|
143
|
+
browser.within_window(name, &block)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def window_handles
|
|
147
|
+
browser.window_handles
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def reset!
|
|
151
|
+
browser.reset
|
|
152
|
+
@started = false
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def save_screenshot(path, options = {})
|
|
156
|
+
browser.render(path, options)
|
|
157
|
+
end
|
|
158
|
+
alias_method :render, :save_screenshot
|
|
159
|
+
|
|
160
|
+
def render_base64(format = :png, options = {})
|
|
161
|
+
browser.render_base64(format, options)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def paper_size=(size = {})
|
|
165
|
+
browser.set_paper_size(size)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def zoom_factor=(zoom_factor)
|
|
169
|
+
browser.set_zoom_factor(zoom_factor)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def resize(width, height)
|
|
173
|
+
browser.resize(width, height)
|
|
174
|
+
end
|
|
175
|
+
alias_method :resize_window, :resize
|
|
176
|
+
|
|
177
|
+
def scroll_to(left, top)
|
|
178
|
+
browser.scroll_to(left, top)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def network_traffic
|
|
182
|
+
browser.network_traffic
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def clear_network_traffic
|
|
186
|
+
browser.clear_network_traffic
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def headers
|
|
190
|
+
browser.get_headers
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def headers=(headers)
|
|
194
|
+
browser.set_headers(headers)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def add_headers(headers)
|
|
198
|
+
browser.add_headers(headers)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def add_header(name, value, options = {})
|
|
202
|
+
permanent = options.fetch(:permanent, true)
|
|
203
|
+
browser.add_header({ name => value }, permanent)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def response_headers
|
|
207
|
+
browser.response_headers
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def cookies
|
|
211
|
+
browser.cookies
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def set_cookie(name, value, options = {})
|
|
215
|
+
options[:name] ||= name
|
|
216
|
+
options[:value] ||= value
|
|
217
|
+
options[:domain] ||= begin
|
|
218
|
+
if @started
|
|
219
|
+
URI.parse(browser.current_url).host
|
|
220
|
+
else
|
|
221
|
+
Capybara.app_host || "127.0.0.1"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
browser.set_cookie(options)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def remove_cookie(name)
|
|
229
|
+
browser.remove_cookie(name)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def cookies_enabled=(flag)
|
|
233
|
+
browser.cookies_enabled = flag
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# * PhantomJS with set settings doesn't send `Authorize` on POST request
|
|
237
|
+
# * With manually set header PhantomJS makes next request with
|
|
238
|
+
# `Authorization: Basic Og==` header when settings are empty and the
|
|
239
|
+
# response was `401 Unauthorized` (which means Base64.encode64(':')).
|
|
240
|
+
# Combining both methods to reach proper behavior.
|
|
241
|
+
def basic_authorize(user, password)
|
|
242
|
+
browser.set_http_auth(user, password)
|
|
243
|
+
credentials = ["#{user}:#{password}"].pack('m*').strip
|
|
244
|
+
add_header('Authorization', "Basic #{credentials}")
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def debug
|
|
248
|
+
if @options[:inspector]
|
|
249
|
+
inspector.open
|
|
250
|
+
pause
|
|
251
|
+
else
|
|
252
|
+
raise Error, "To use the remote debugging, you have to launch the driver " \
|
|
253
|
+
"with `:inspector => true` configuration option"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def pause
|
|
258
|
+
STDERR.puts "Poltergeist execution paused. Press enter to continue."
|
|
259
|
+
STDIN.gets
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def wait?
|
|
263
|
+
true
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def invalid_element_errors
|
|
267
|
+
[Capybara::Poltergeist::ObsoleteNode, Capybara::Poltergeist::MouseEventFailed]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def go_back
|
|
271
|
+
browser.go_back
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def go_forward
|
|
275
|
+
browser.go_forward
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
module Capybara
|
|
2
|
+
module Poltergeist
|
|
3
|
+
class Error < StandardError
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
class ClientError < Error
|
|
7
|
+
attr_reader :response
|
|
8
|
+
|
|
9
|
+
def initialize(response)
|
|
10
|
+
@response = response
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class JSErrorItem
|
|
15
|
+
attr_reader :message, :stack
|
|
16
|
+
|
|
17
|
+
def initialize(message, stack)
|
|
18
|
+
@message = message
|
|
19
|
+
@stack = stack
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
[message, stack].join("\n")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class BrowserError < ClientError
|
|
28
|
+
def name
|
|
29
|
+
response['name']
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def javascript_error
|
|
33
|
+
JSErrorItem.new(*response['args'])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def message
|
|
37
|
+
"There was an error inside the PhantomJS portion of Poltergeist. " \
|
|
38
|
+
"This is probably a bug, so please report it. " \
|
|
39
|
+
"\n\n#{javascript_error}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class JavascriptError < ClientError
|
|
44
|
+
def javascript_errors
|
|
45
|
+
response['args'].first.map { |data| JSErrorItem.new(data['message'], data['stack']) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def message
|
|
49
|
+
"One or more errors were raised in the Javascript code on the page. " \
|
|
50
|
+
"If you don't care about these errors, you can ignore them by " \
|
|
51
|
+
"setting js_errors: false in your Poltergeist configuration (see " \
|
|
52
|
+
"documentation for details)." \
|
|
53
|
+
"\n\n#{javascript_errors.map(&:to_s).join("\n")}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class StatusFailError < ClientError
|
|
58
|
+
def message
|
|
59
|
+
"Request failed to reach server, check DNS and/or server status"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class FrameNotFound < ClientError
|
|
64
|
+
def name
|
|
65
|
+
response['args'].first
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def message
|
|
69
|
+
"The frame '#{name}' was not found."
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class InvalidSelector < ClientError
|
|
74
|
+
def method
|
|
75
|
+
response['args'][0]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def selector
|
|
79
|
+
response['args'][1]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def message
|
|
83
|
+
"The browser raised a syntax error while trying to evaluate " \
|
|
84
|
+
"#{method} selector #{selector.inspect}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class NodeError < ClientError
|
|
89
|
+
attr_reader :node
|
|
90
|
+
|
|
91
|
+
def initialize(node, response)
|
|
92
|
+
@node = node
|
|
93
|
+
super(response)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class ObsoleteNode < NodeError
|
|
98
|
+
def message
|
|
99
|
+
"The element you are trying to interact with is either not part of the DOM, or is " \
|
|
100
|
+
"not currently visible on the page (perhaps display: none is set). " \
|
|
101
|
+
"It's possible the element has been replaced by another element and you meant to interact with " \
|
|
102
|
+
"the new element. If so you need to do a new 'find' in order to get a reference to the " \
|
|
103
|
+
"new element."
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class MouseEventFailed < NodeError
|
|
108
|
+
def name
|
|
109
|
+
response['args'][0]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def selector
|
|
113
|
+
response['args'][1]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def position
|
|
117
|
+
[response['args'][2]['x'], response['args'][2]['y']]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def message
|
|
121
|
+
"Firing a #{name} at co-ordinates [#{position.join(', ')}] failed. Poltergeist detected " \
|
|
122
|
+
"another element with CSS selector '#{selector}' at this position. " \
|
|
123
|
+
"It may be overlapping the element you are trying to interact with. " \
|
|
124
|
+
"If you don't care about overlapping elements, try using node.trigger('#{name}')."
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class TimeoutError < Error
|
|
129
|
+
def initialize(message)
|
|
130
|
+
@message = message
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def message
|
|
134
|
+
"Timed out waiting for response to #{@message}. It's possible that this happened " \
|
|
135
|
+
"because something took a very long time (for example a page load was slow). " \
|
|
136
|
+
"If so, setting the Poltergeist :timeout option to a higher value will help " \
|
|
137
|
+
"(see the docs for details). If increasing the timeout does not help, this is " \
|
|
138
|
+
"probably a bug in Poltergeist - please report it to the issue tracker."
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
class DeadClient < Error
|
|
143
|
+
def initialize(message)
|
|
144
|
+
@message = message
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def message
|
|
148
|
+
"PhantomJS client died while processing #{@message}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
class PhantomJSTooOld < Error
|
|
153
|
+
def self.===(other)
|
|
154
|
+
if Cliver::Dependency::VersionMismatch === other
|
|
155
|
+
warn "#{name} exception has been deprecated in favor of using the " +
|
|
156
|
+
"cliver gem for command-line dependency detection. Please " +
|
|
157
|
+
"handle Cliver::Dependency::VersionMismatch instead."
|
|
158
|
+
true
|
|
159
|
+
else
|
|
160
|
+
super
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class PhantomJSFailed < Error
|
|
166
|
+
def self.===(other)
|
|
167
|
+
if Cliver::Dependency::NotMet === other
|
|
168
|
+
warn "#{name} exception has been deprecated in favor of using the " +
|
|
169
|
+
"cliver gem for command-line dependency detection. Please " +
|
|
170
|
+
"handle Cliver::Dependency::NotMet instead."
|
|
171
|
+
true
|
|
172
|
+
else
|
|
173
|
+
super
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|