poltergeistFork 0.0.1
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 +7 -0
- data/LICENSE +22 -0
- data/README.md +425 -0
- data/lib/capybara/poltergeist/browser.rb +426 -0
- data/lib/capybara/poltergeist/client.rb +151 -0
- data/lib/capybara/poltergeist/client/agent.coffee +423 -0
- data/lib/capybara/poltergeist/client/browser.coffee +497 -0
- data/lib/capybara/poltergeist/client/cmd.coffee +17 -0
- data/lib/capybara/poltergeist/client/compiled/agent.js +587 -0
- data/lib/capybara/poltergeist/client/compiled/browser.js +687 -0
- data/lib/capybara/poltergeist/client/compiled/cmd.js +31 -0
- data/lib/capybara/poltergeist/client/compiled/connection.js +25 -0
- data/lib/capybara/poltergeist/client/compiled/main.js +228 -0
- data/lib/capybara/poltergeist/client/compiled/node.js +88 -0
- data/lib/capybara/poltergeist/client/compiled/web_page.js +539 -0
- data/lib/capybara/poltergeist/client/connection.coffee +11 -0
- data/lib/capybara/poltergeist/client/main.coffee +99 -0
- data/lib/capybara/poltergeist/client/node.coffee +70 -0
- data/lib/capybara/poltergeist/client/pre/agent.js +587 -0
- data/lib/capybara/poltergeist/client/pre/browser.js +688 -0
- data/lib/capybara/poltergeist/client/pre/cmd.js +31 -0
- data/lib/capybara/poltergeist/client/pre/connection.js +25 -0
- data/lib/capybara/poltergeist/client/pre/main.js +228 -0
- data/lib/capybara/poltergeist/client/pre/node.js +88 -0
- data/lib/capybara/poltergeist/client/pre/web_page.js +540 -0
- data/lib/capybara/poltergeist/client/web_page.coffee +372 -0
- data/lib/capybara/poltergeist/command.rb +17 -0
- data/lib/capybara/poltergeist/cookie.rb +35 -0
- data/lib/capybara/poltergeist/driver.rb +394 -0
- data/lib/capybara/poltergeist/errors.rb +183 -0
- data/lib/capybara/poltergeist/inspector.rb +46 -0
- data/lib/capybara/poltergeist/json.rb +25 -0
- data/lib/capybara/poltergeist/network_traffic.rb +7 -0
- data/lib/capybara/poltergeist/network_traffic/error.rb +19 -0
- data/lib/capybara/poltergeist/network_traffic/request.rb +27 -0
- data/lib/capybara/poltergeist/network_traffic/response.rb +40 -0
- data/lib/capybara/poltergeist/node.rb +177 -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 +107 -0
- data/lib/capybara/poltergeistFork.rb +27 -0
- metadata +268 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Poltergeist
|
3
|
+
class Error < StandardError; end
|
4
|
+
class NoSuchWindowError < Error; 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 error_parameters
|
33
|
+
response['args'].join("\n")
|
34
|
+
end
|
35
|
+
|
36
|
+
def message
|
37
|
+
"There was an error inside the PhantomJS portion of Poltergeist. " \
|
38
|
+
"If this is the error returned, and not the cause of a more detailed error response, " \
|
39
|
+
"this is probably a bug, so please report it. " \
|
40
|
+
"\n\n#{name}: #{error_parameters}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class JavascriptError < ClientError
|
45
|
+
def javascript_errors
|
46
|
+
response['args'].first.map { |data| JSErrorItem.new(data['message'], data['stack']) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def message
|
50
|
+
"One or more errors were raised in the Javascript code on the page. " \
|
51
|
+
"If you don't care about these errors, you can ignore them by " \
|
52
|
+
"setting js_errors: false in your Poltergeist configuration (see " \
|
53
|
+
"documentation for details)." \
|
54
|
+
"\n\n#{javascript_errors.map(&:to_s).join("\n")}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class StatusFailError < ClientError
|
59
|
+
def url
|
60
|
+
response['args'].first
|
61
|
+
end
|
62
|
+
|
63
|
+
def message
|
64
|
+
"Request to '#{url}' failed to reach server, check DNS and/or server status"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class FrameNotFound < ClientError
|
69
|
+
def name
|
70
|
+
response['args'].first
|
71
|
+
end
|
72
|
+
|
73
|
+
def message
|
74
|
+
"The frame '#{name}' was not found."
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class InvalidSelector < ClientError
|
79
|
+
def method
|
80
|
+
response['args'][0]
|
81
|
+
end
|
82
|
+
|
83
|
+
def selector
|
84
|
+
response['args'][1]
|
85
|
+
end
|
86
|
+
|
87
|
+
def message
|
88
|
+
"The browser raised a syntax error while trying to evaluate " \
|
89
|
+
"#{method} selector #{selector.inspect}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class NodeError < ClientError
|
94
|
+
attr_reader :node
|
95
|
+
|
96
|
+
def initialize(node, response)
|
97
|
+
@node = node
|
98
|
+
super(response)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class ObsoleteNode < NodeError
|
103
|
+
def message
|
104
|
+
"The element you are trying to interact with is either not part of the DOM, or is " \
|
105
|
+
"not currently visible on the page (perhaps display: none is set). " \
|
106
|
+
"It's possible the element has been replaced by another element and you meant to interact with " \
|
107
|
+
"the new element. If so you need to do a new 'find' in order to get a reference to the " \
|
108
|
+
"new element."
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class MouseEventFailed < NodeError
|
113
|
+
def name
|
114
|
+
response['args'][0]
|
115
|
+
end
|
116
|
+
|
117
|
+
def selector
|
118
|
+
response['args'][1]
|
119
|
+
end
|
120
|
+
|
121
|
+
def position
|
122
|
+
[response['args'][2]['x'], response['args'][2]['y']]
|
123
|
+
end
|
124
|
+
|
125
|
+
def message
|
126
|
+
"Firing a #{name} at co-ordinates [#{position.join(', ')}] failed. Poltergeist detected " \
|
127
|
+
"another element with CSS selector '#{selector}' at this position. " \
|
128
|
+
"It may be overlapping the element you are trying to interact with. " \
|
129
|
+
"If you don't care about overlapping elements, try using node.trigger('#{name}')."
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class TimeoutError < Error
|
134
|
+
def initialize(message)
|
135
|
+
@message = message
|
136
|
+
end
|
137
|
+
|
138
|
+
def message
|
139
|
+
"Timed out waiting for response to #{@message}. It's possible that this happened " \
|
140
|
+
"because something took a very long time (for example a page load was slow). " \
|
141
|
+
"If so, setting the Poltergeist :timeout option to a higher value will help " \
|
142
|
+
"(see the docs for details). If increasing the timeout does not help, this is " \
|
143
|
+
"probably a bug in Poltergeist - please report it to the issue tracker."
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class DeadClient < Error
|
148
|
+
def initialize(message)
|
149
|
+
@message = message
|
150
|
+
end
|
151
|
+
|
152
|
+
def message
|
153
|
+
"PhantomJS client died while processing #{@message}"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class PhantomJSTooOld < Error
|
158
|
+
def self.===(other)
|
159
|
+
if Cliver::Dependency::VersionMismatch === other
|
160
|
+
warn "#{name} exception has been deprecated in favor of using the " +
|
161
|
+
"cliver gem for command-line dependency detection. Please " +
|
162
|
+
"handle Cliver::Dependency::VersionMismatch instead."
|
163
|
+
true
|
164
|
+
else
|
165
|
+
super
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class PhantomJSFailed < Error
|
171
|
+
def self.===(other)
|
172
|
+
if Cliver::Dependency::NotMet === other
|
173
|
+
warn "#{name} exception has been deprecated in favor of using the " +
|
174
|
+
"cliver gem for command-line dependency detection. Please " +
|
175
|
+
"handle Cliver::Dependency::NotMet instead."
|
176
|
+
true
|
177
|
+
else
|
178
|
+
super
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Capybara::Poltergeist
|
2
|
+
class Inspector
|
3
|
+
BROWSERS = %w(chromium chromium-browser google-chrome open)
|
4
|
+
DEFAULT_PORT = 9664
|
5
|
+
|
6
|
+
def self.detect_browser
|
7
|
+
@browser ||= BROWSERS.find { |name| browser_binary_exists?(name) }
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :port
|
11
|
+
|
12
|
+
def initialize(browser = nil, port = DEFAULT_PORT)
|
13
|
+
@browser = browser.respond_to?(:to_str) ? browser : nil
|
14
|
+
@port = port
|
15
|
+
end
|
16
|
+
|
17
|
+
def browser
|
18
|
+
@browser ||= self.class.detect_browser
|
19
|
+
end
|
20
|
+
|
21
|
+
def url(scheme)
|
22
|
+
"#{scheme}://localhost:#{port}/"
|
23
|
+
end
|
24
|
+
|
25
|
+
def open(scheme)
|
26
|
+
if browser
|
27
|
+
Process.spawn(browser, url(scheme))
|
28
|
+
else
|
29
|
+
raise Error, "Could not find a browser executable to open #{url(scheme)}. " \
|
30
|
+
"You can specify one manually using e.g. `:inspector => 'chromium'` " \
|
31
|
+
"as a configuration option for Poltergeist."
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.browser_binary_exists?(browser)
|
36
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
37
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
38
|
+
exts.each { |ext|
|
39
|
+
exe = "#{path}#{File::SEPARATOR}#{browser}#{ext}"
|
40
|
+
return exe if File.executable? exe
|
41
|
+
}
|
42
|
+
end
|
43
|
+
return nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Capybara::Poltergeist
|
2
|
+
module JSON
|
3
|
+
def self.load(message)
|
4
|
+
if dumpy_multi_json?
|
5
|
+
MultiJson.load(message)
|
6
|
+
else
|
7
|
+
MultiJson.decode(message)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.dump(message)
|
12
|
+
if dumpy_multi_json?
|
13
|
+
MultiJson.dump(message)
|
14
|
+
else
|
15
|
+
MultiJson.encode(message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def self.dumpy_multi_json?
|
22
|
+
MultiJson.respond_to?(:dump) && MultiJson.respond_to?(:load)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Capybara::Poltergeist::NetworkTraffic
|
2
|
+
class Error
|
3
|
+
def initialize(data)
|
4
|
+
@data = data
|
5
|
+
end
|
6
|
+
|
7
|
+
def url
|
8
|
+
@data['url']
|
9
|
+
end
|
10
|
+
|
11
|
+
def code
|
12
|
+
@data['errorCode']
|
13
|
+
end
|
14
|
+
|
15
|
+
def description
|
16
|
+
@data['errorString']
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Capybara::Poltergeist::NetworkTraffic
|
2
|
+
class Request
|
3
|
+
attr_reader :response_parts, :error
|
4
|
+
|
5
|
+
def initialize(data, response_parts = [], error = nil)
|
6
|
+
@data = data
|
7
|
+
@response_parts = response_parts
|
8
|
+
@error = error
|
9
|
+
end
|
10
|
+
|
11
|
+
def url
|
12
|
+
@data['url']
|
13
|
+
end
|
14
|
+
|
15
|
+
def method
|
16
|
+
@data['method']
|
17
|
+
end
|
18
|
+
|
19
|
+
def headers
|
20
|
+
@data['headers']
|
21
|
+
end
|
22
|
+
|
23
|
+
def time
|
24
|
+
@data['time'] && Time.parse(@data['time'])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Capybara::Poltergeist::NetworkTraffic
|
2
|
+
class Response
|
3
|
+
def initialize(data)
|
4
|
+
@data = data
|
5
|
+
end
|
6
|
+
|
7
|
+
def url
|
8
|
+
@data['url']
|
9
|
+
end
|
10
|
+
|
11
|
+
def status
|
12
|
+
@data['status']
|
13
|
+
end
|
14
|
+
|
15
|
+
def status_text
|
16
|
+
@data['statusText']
|
17
|
+
end
|
18
|
+
|
19
|
+
def headers
|
20
|
+
@data['headers']
|
21
|
+
end
|
22
|
+
|
23
|
+
def redirect_url
|
24
|
+
@data['redirectURL']
|
25
|
+
end
|
26
|
+
|
27
|
+
def body_size
|
28
|
+
@data['bodySize']
|
29
|
+
end
|
30
|
+
|
31
|
+
def content_type
|
32
|
+
@data['contentType']
|
33
|
+
end
|
34
|
+
|
35
|
+
def time
|
36
|
+
@data['time'] && Time.parse(@data['time'])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module Capybara::Poltergeist
|
2
|
+
class Node < Capybara::Driver::Node
|
3
|
+
attr_reader :page_id, :id
|
4
|
+
|
5
|
+
def initialize(driver, page_id, id)
|
6
|
+
super(driver, self)
|
7
|
+
|
8
|
+
@page_id = page_id
|
9
|
+
@id = id
|
10
|
+
end
|
11
|
+
|
12
|
+
def browser
|
13
|
+
driver.browser
|
14
|
+
end
|
15
|
+
|
16
|
+
def command(name, *args)
|
17
|
+
browser.send(name, page_id, id, *args)
|
18
|
+
rescue BrowserError => error
|
19
|
+
case error.name
|
20
|
+
when 'Poltergeist.ObsoleteNode'
|
21
|
+
raise ObsoleteNode.new(self, error.response)
|
22
|
+
when 'Poltergeist.MouseEventFailed'
|
23
|
+
raise MouseEventFailed.new(self, error.response)
|
24
|
+
else
|
25
|
+
raise
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def parents
|
30
|
+
command(:parents).map { |parent_id| self.class.new(driver, page_id, parent_id) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def find(method, selector)
|
34
|
+
command(:find_within, method, selector).map { |id| self.class.new(driver, page_id, id) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_xpath(selector)
|
38
|
+
find :xpath, selector
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_css(selector)
|
42
|
+
find :css, selector
|
43
|
+
end
|
44
|
+
|
45
|
+
def all_text
|
46
|
+
filter_text command(:all_text)
|
47
|
+
end
|
48
|
+
|
49
|
+
def visible_text
|
50
|
+
filter_text command(:visible_text)
|
51
|
+
end
|
52
|
+
|
53
|
+
def property(name)
|
54
|
+
command :property, name
|
55
|
+
end
|
56
|
+
|
57
|
+
def [](name)
|
58
|
+
# Although the attribute matters, the property is consistent. Return that in
|
59
|
+
# preference to the attribute for links and images.
|
60
|
+
if (tag_name == 'img' and name == 'src') or (tag_name == 'a' and name == 'href' )
|
61
|
+
#if attribute exists get the property
|
62
|
+
value = command(:attribute, name) && command(:property, name)
|
63
|
+
return value
|
64
|
+
end
|
65
|
+
|
66
|
+
value = property(name)
|
67
|
+
value = command(:attribute, name) if value.nil? || value.is_a?(Hash)
|
68
|
+
|
69
|
+
value
|
70
|
+
end
|
71
|
+
|
72
|
+
def attributes
|
73
|
+
command :attributes
|
74
|
+
end
|
75
|
+
|
76
|
+
def value
|
77
|
+
command :value
|
78
|
+
end
|
79
|
+
|
80
|
+
def set(value)
|
81
|
+
if tag_name == 'input'
|
82
|
+
case self[:type]
|
83
|
+
when 'radio'
|
84
|
+
click
|
85
|
+
when 'checkbox'
|
86
|
+
click if value != checked?
|
87
|
+
when 'file'
|
88
|
+
files = value.respond_to?(:to_ary) ? value.to_ary.map(&:to_s) : value.to_s
|
89
|
+
command :select_file, files
|
90
|
+
else
|
91
|
+
command :set, value.to_s
|
92
|
+
end
|
93
|
+
elsif tag_name == 'textarea'
|
94
|
+
command :set, value.to_s
|
95
|
+
elsif self[:contenteditable] == 'true'
|
96
|
+
command :delete_text
|
97
|
+
send_keys(value.to_s)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def select_option
|
102
|
+
command :select, true
|
103
|
+
end
|
104
|
+
|
105
|
+
def unselect_option
|
106
|
+
command(:select, false) or
|
107
|
+
raise(Capybara::UnselectNotAllowed, "Cannot unselect option from single select box.")
|
108
|
+
end
|
109
|
+
|
110
|
+
def tag_name
|
111
|
+
@tag_name ||= command(:tag_name)
|
112
|
+
end
|
113
|
+
|
114
|
+
def visible?
|
115
|
+
command :visible?
|
116
|
+
end
|
117
|
+
|
118
|
+
def checked?
|
119
|
+
self[:checked]
|
120
|
+
end
|
121
|
+
|
122
|
+
def selected?
|
123
|
+
self[:selected]
|
124
|
+
end
|
125
|
+
|
126
|
+
def disabled?
|
127
|
+
command :disabled?
|
128
|
+
end
|
129
|
+
|
130
|
+
def click
|
131
|
+
command :click
|
132
|
+
end
|
133
|
+
|
134
|
+
def right_click
|
135
|
+
command :right_click
|
136
|
+
end
|
137
|
+
|
138
|
+
def double_click
|
139
|
+
command :double_click
|
140
|
+
end
|
141
|
+
|
142
|
+
def hover
|
143
|
+
command :hover
|
144
|
+
end
|
145
|
+
|
146
|
+
def drag_to(other)
|
147
|
+
command :drag, other.id
|
148
|
+
end
|
149
|
+
|
150
|
+
def drag_by(x, y)
|
151
|
+
command :drag_by, x, y
|
152
|
+
end
|
153
|
+
|
154
|
+
def trigger(event)
|
155
|
+
command :trigger, event
|
156
|
+
end
|
157
|
+
|
158
|
+
def ==(other)
|
159
|
+
command :equals, other.id
|
160
|
+
end
|
161
|
+
|
162
|
+
def send_keys(*keys)
|
163
|
+
command :send_keys, keys
|
164
|
+
end
|
165
|
+
alias_method :send_key, :send_keys
|
166
|
+
|
167
|
+
def path
|
168
|
+
command :path
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def filter_text(text)
|
174
|
+
Capybara::Helpers.normalize_whitespace(text.to_s)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|