akephalos 0.2.3 → 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +2 -1
- data/lib/akephalos/capybara.rb +97 -1
- data/lib/akephalos/client.rb +34 -2
- data/lib/akephalos/client/filter.rb +42 -2
- data/lib/akephalos/configuration.rb +32 -5
- data/lib/akephalos/console.rb +5 -0
- data/lib/akephalos/htmlunit/ext/http_method.rb +10 -0
- data/lib/akephalos/node.rb +47 -0
- data/lib/akephalos/page.rb +80 -6
- data/lib/akephalos/remote_client.rb +6 -6
- data/lib/akephalos/server.rb +11 -2
- data/lib/akephalos/version.rb +1 -1
- metadata +3 -4
- data/lib/akephalos/client/listener.rb +0 -18
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Akephalos
|
2
2
|
Akephalos is a full-stack headless browser for integration testing with
|
3
|
-
Capybara. It is built on top of [HtmlUnit](http://htmlunit.sourceforge.
|
3
|
+
Capybara. It is built on top of [HtmlUnit](http://htmlunit.sourceforge.net),
|
4
4
|
a GUI-less browser for the Java platform, but can be run on both JRuby and
|
5
5
|
MRI with no need for JRuby to be installed on the system.
|
6
6
|
|
@@ -50,6 +50,7 @@ Here's some sample RSpec code:
|
|
50
50
|
|
51
51
|
## Resources
|
52
52
|
|
53
|
+
* [API Documentation](http://bernerdschaefer.github.com/akephalos/api)
|
53
54
|
* [Source code](http://github.com/bernerdschaefer/akephalos) and
|
54
55
|
[issues](http://github.com/bernerdschaefer/akephalos/issues) are hosted on
|
55
56
|
github.
|
data/lib/akephalos/capybara.rb
CHANGED
@@ -1,7 +1,17 @@
|
|
1
|
+
# Driver class exposed to Capybara. It implements Capybara's full driver API,
|
2
|
+
# and is the entry point for interaction between the test suites and HtmlUnit.
|
3
|
+
#
|
4
|
+
# This class and +Capybara::Driver::Akephalos::Node+ are written to run on both
|
5
|
+
# MRI and JRuby, and is agnostic whether the Akephalos::Client instance is used
|
6
|
+
# directly or over DRb.
|
1
7
|
class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
2
8
|
|
9
|
+
# Akephalos-specific implementation for Capybara's Node class.
|
3
10
|
class Node < Capybara::Node
|
4
11
|
|
12
|
+
# @api capybara
|
13
|
+
# @param [String] name attribute name
|
14
|
+
# @return [String] the attribute value
|
5
15
|
def [](name)
|
6
16
|
name = name.to_s
|
7
17
|
case name
|
@@ -12,14 +22,22 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
12
22
|
end
|
13
23
|
end
|
14
24
|
|
25
|
+
# @api capybara
|
26
|
+
# @return [String] the inner text of the node
|
15
27
|
def text
|
16
28
|
node.text
|
17
29
|
end
|
18
30
|
|
31
|
+
# @api capybara
|
32
|
+
# @return [String] the form element's value
|
19
33
|
def value
|
20
34
|
node.value
|
21
35
|
end
|
22
36
|
|
37
|
+
# Set the form element's value.
|
38
|
+
#
|
39
|
+
# @api capybara
|
40
|
+
# @param [String] value the form element's new value
|
23
41
|
def set(value)
|
24
42
|
if tag_name == 'textarea'
|
25
43
|
node.value = value.to_s
|
@@ -34,6 +52,10 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
34
52
|
end
|
35
53
|
end
|
36
54
|
|
55
|
+
# Select an option from a select box.
|
56
|
+
#
|
57
|
+
# @api capybara
|
58
|
+
# @param [String] option the option to select
|
37
59
|
def select(option)
|
38
60
|
result = node.select_option(option)
|
39
61
|
|
@@ -45,6 +67,10 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
45
67
|
end
|
46
68
|
end
|
47
69
|
|
70
|
+
# Unselect an option from a select box.
|
71
|
+
#
|
72
|
+
# @api capybara
|
73
|
+
# @param [String] option the option to unselect
|
48
74
|
def unselect(option)
|
49
75
|
unless self[:multiple]
|
50
76
|
raise Capybara::UnselectNotAllowed, "Cannot unselect option '#{option}' from single select box."
|
@@ -60,36 +86,54 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
60
86
|
end
|
61
87
|
end
|
62
88
|
|
89
|
+
# Trigger an event on the element.
|
90
|
+
#
|
91
|
+
# @api capybara
|
92
|
+
# @param [String] event the event to trigger
|
63
93
|
def trigger(event)
|
64
94
|
node.fire_event(event.to_s)
|
65
95
|
end
|
66
96
|
|
97
|
+
# @api capybara
|
98
|
+
# @return [String] the element's tag name
|
67
99
|
def tag_name
|
68
100
|
node.tag_name
|
69
101
|
end
|
70
102
|
|
103
|
+
# @api capybara
|
104
|
+
# @return [true, false] the element's visiblity
|
71
105
|
def visible?
|
72
106
|
node.visible?
|
73
107
|
end
|
74
108
|
|
109
|
+
# Drag the element on top of the target element.
|
110
|
+
#
|
111
|
+
# @api capybara
|
112
|
+
# @param [Node] element the target element
|
75
113
|
def drag_to(element)
|
76
114
|
trigger('mousedown')
|
77
115
|
element.trigger('mousemove')
|
78
116
|
element.trigger('mouseup')
|
79
117
|
end
|
80
118
|
|
119
|
+
# Click the element.
|
81
120
|
def click
|
82
121
|
node.click
|
83
122
|
end
|
84
123
|
|
85
124
|
private
|
86
125
|
|
126
|
+
# Return all child nodes which match the selector criteria.
|
127
|
+
#
|
128
|
+
# @api capybara
|
129
|
+
# @return [Array<Node>] the matched nodes
|
87
130
|
def all_unfiltered(selector)
|
88
131
|
nodes = []
|
89
132
|
node.find(selector).each { |node| nodes << Node.new(driver, node) }
|
90
133
|
nodes
|
91
134
|
end
|
92
135
|
|
136
|
+
# @return [String] the node's type attribute
|
93
137
|
def type
|
94
138
|
node[:type]
|
95
139
|
end
|
@@ -97,6 +141,7 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
97
141
|
|
98
142
|
attr_reader :app, :rack_server
|
99
143
|
|
144
|
+
# @return [Client] an instance of Akephalos::Client
|
100
145
|
def self.driver
|
101
146
|
@driver ||= Akephalos::Client.new
|
102
147
|
end
|
@@ -107,50 +152,101 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
107
152
|
@rack_server.boot if Capybara.run_server
|
108
153
|
end
|
109
154
|
|
155
|
+
# Visit the given path in the browser.
|
156
|
+
#
|
157
|
+
# @param [String] path relative path to visit
|
110
158
|
def visit(path)
|
111
159
|
browser.visit(url(path))
|
112
160
|
end
|
113
161
|
|
162
|
+
# @return [String] the page's original source
|
114
163
|
def source
|
115
164
|
page.source
|
116
165
|
end
|
117
166
|
|
167
|
+
# @return [String] the page's modified source
|
118
168
|
def body
|
119
169
|
page.modified_source
|
120
170
|
end
|
121
171
|
|
172
|
+
# @return [Hash{String => String}] the page's response headers
|
173
|
+
def response_headers
|
174
|
+
page.response_headers
|
175
|
+
end
|
176
|
+
|
177
|
+
# @return [Integer] the response's status code
|
178
|
+
def status_code
|
179
|
+
page.status_code
|
180
|
+
end
|
181
|
+
|
182
|
+
# Execute the given block within the context of a specified frame.
|
183
|
+
#
|
184
|
+
# @param [String] frame_id the frame's id
|
185
|
+
# @raise [Capybara::ElementNotFound] if the frame is not found
|
186
|
+
def within_frame(frame_id, &block)
|
187
|
+
unless page.within_frame(frame_id, &block)
|
188
|
+
raise Capybara::ElementNotFound, "Unable to find frame with id '#{frame_id}'"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Clear all cookie session data.
|
193
|
+
def cleanup!
|
194
|
+
browser.clear_cookies
|
195
|
+
end
|
196
|
+
|
197
|
+
# @return [String] the page's current URL
|
122
198
|
def current_url
|
123
199
|
page.current_url
|
124
200
|
end
|
125
201
|
|
202
|
+
# Search for nodes which match the given XPath selector.
|
203
|
+
#
|
204
|
+
# @param [String] selector XPath query
|
205
|
+
# @return [Array<Node>] the matched nodes
|
126
206
|
def find(selector)
|
127
207
|
nodes = []
|
128
208
|
page.find(selector).each { |node| nodes << Node.new(self, node) }
|
129
209
|
nodes
|
130
210
|
end
|
131
211
|
|
212
|
+
# Execute JavaScript against the current page, discarding any return value.
|
213
|
+
#
|
214
|
+
# @param [String] script the JavaScript to be executed
|
215
|
+
# @return [nil]
|
132
216
|
def execute_script(script)
|
133
217
|
page.execute_script script
|
134
218
|
end
|
135
219
|
|
220
|
+
# Execute JavaScript against the current page and return the results.
|
221
|
+
#
|
222
|
+
# @param [String] script the JavaScript to be executed
|
223
|
+
# @return the result of the JavaScript
|
136
224
|
def evaluate_script(script)
|
137
225
|
page.evaluate_script script
|
138
226
|
end
|
139
227
|
|
228
|
+
# @return the current page
|
140
229
|
def page
|
141
230
|
browser.page
|
142
231
|
end
|
143
232
|
|
233
|
+
# @return the browser
|
144
234
|
def browser
|
145
235
|
self.class.driver
|
146
236
|
end
|
147
237
|
|
238
|
+
# Disable waiting in Capybara, since waiting is handled directly by
|
239
|
+
# Akephalos.
|
240
|
+
#
|
241
|
+
# @return [false]
|
148
242
|
def wait
|
149
243
|
false
|
150
244
|
end
|
151
245
|
|
152
|
-
private
|
246
|
+
private
|
153
247
|
|
248
|
+
# @param [String] path
|
249
|
+
# @return [String] the absolute URL for the given path
|
154
250
|
def url(path)
|
155
251
|
rack_server.url(path)
|
156
252
|
end
|
data/lib/akephalos/client.rb
CHANGED
@@ -11,9 +11,12 @@ else
|
|
11
11
|
require 'akephalos/node'
|
12
12
|
|
13
13
|
require 'akephalos/client/filter'
|
14
|
-
require 'akephalos/client/listener'
|
15
14
|
|
16
15
|
module Akephalos
|
16
|
+
|
17
|
+
# Akephalos::Client wraps HtmlUnit's WebClient class. It is the main entry
|
18
|
+
# point for all interaction with the browser, exposing its current page and
|
19
|
+
# allowing navigation.
|
17
20
|
class Client
|
18
21
|
java_import 'com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController'
|
19
22
|
java_import 'com.gargoylesoftware.htmlunit.SilentCssErrorHandler'
|
@@ -25,7 +28,6 @@ else
|
|
25
28
|
client = WebClient.new
|
26
29
|
|
27
30
|
Filter.new(client)
|
28
|
-
client.addWebWindowListener(Listener.new(self))
|
29
31
|
client.setAjaxController(NicelyResynchronizingAjaxController.new)
|
30
32
|
client.setCssErrorHandler(SilentCssErrorHandler.new)
|
31
33
|
|
@@ -34,15 +36,40 @@ else
|
|
34
36
|
Thread.new { @_client.run }
|
35
37
|
end
|
36
38
|
|
39
|
+
# Set the global configuration settings for Akephalos.
|
40
|
+
#
|
41
|
+
# @note This is only used when communicating over DRb, since just a
|
42
|
+
# single client instance is exposed.
|
43
|
+
# @param [Hash] config the configuration settings
|
44
|
+
# @return [Hash] the configuration
|
37
45
|
def configuration=(config)
|
38
46
|
Akephalos.configuration = config
|
39
47
|
end
|
40
48
|
|
49
|
+
# Visit the requested URL and return the page.
|
50
|
+
#
|
51
|
+
# @param [String] url the URL to load
|
52
|
+
# @return [Page] the loaded page
|
41
53
|
def visit(url)
|
42
54
|
client.getPage(url)
|
43
55
|
page
|
44
56
|
end
|
45
57
|
|
58
|
+
# Clear all cookies for this browser session.
|
59
|
+
def clear_cookies
|
60
|
+
client.getCookieManager.clearCookies
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Page] the current page
|
64
|
+
def page
|
65
|
+
self.page = client.getCurrentWindow.getTopWindow.getEnclosedPage
|
66
|
+
@page
|
67
|
+
end
|
68
|
+
|
69
|
+
# Update the current page.
|
70
|
+
#
|
71
|
+
# @param [HtmlUnit::HtmlPage] _page the new page
|
72
|
+
# @return [Page] the new page
|
46
73
|
def page=(_page)
|
47
74
|
if @page != _page
|
48
75
|
@page = Page.new(_page)
|
@@ -51,6 +78,11 @@ else
|
|
51
78
|
end
|
52
79
|
|
53
80
|
private
|
81
|
+
|
82
|
+
# Call the future set up in #initialize and return the WebCLient
|
83
|
+
# instance.
|
84
|
+
#
|
85
|
+
# @return [HtmlUnit::WebClient] the WebClient instance
|
54
86
|
def client
|
55
87
|
@client ||= @_client.get.tap do |client|
|
56
88
|
client.getCurrentWindow.getHistory.ignoreNewPages_.set(true)
|
@@ -1,23 +1,62 @@
|
|
1
1
|
module Akephalos
|
2
2
|
class Client
|
3
|
+
|
4
|
+
# Akephalos::Client::Filter extends HtmlUnit's WebConnectionWrapper to
|
5
|
+
# enable filtering outgoing requests generated interally by HtmlUnit.
|
6
|
+
#
|
7
|
+
# When a request comes through, it will be tested against the filters
|
8
|
+
# defined in Akephalos.filters and return a mock response if a match is
|
9
|
+
# found. If no filters are defined, or no filters match the request, then
|
10
|
+
# the response will bubble up to HtmlUnit for the normal request/response
|
11
|
+
# cycle.
|
3
12
|
class Filter < WebConnectionWrapper
|
4
13
|
java_import 'com.gargoylesoftware.htmlunit.util.NameValuePair'
|
5
14
|
java_import 'com.gargoylesoftware.htmlunit.WebResponseData'
|
6
15
|
java_import 'com.gargoylesoftware.htmlunit.WebResponseImpl'
|
7
16
|
|
17
|
+
# Filters an outgoing request, and if a match is found, returns the mock
|
18
|
+
# response.
|
19
|
+
#
|
20
|
+
# @param [WebRequest] request the pending HTTP request
|
21
|
+
# @return [WebResponseImpl] when the request matches a defined filter
|
22
|
+
# @return [nil] when no filters match the request
|
8
23
|
def filter(request)
|
9
|
-
if filter =
|
24
|
+
if filter = find_filter(request)
|
10
25
|
start_time = Time.now
|
11
26
|
headers = filter[:headers].map { |name, value| NameValuePair.new(name.to_s, value.to_s) }
|
12
|
-
response = WebResponseData.new(
|
27
|
+
response = WebResponseData.new(
|
28
|
+
filter[:body].to_s.to_java_bytes,
|
29
|
+
filter[:status],
|
30
|
+
HTTP_STATUS_CODES.fetch(filter[:status], "Unknown"),
|
31
|
+
headers
|
32
|
+
)
|
13
33
|
WebResponseImpl.new(response, request, Time.now - start_time)
|
14
34
|
end
|
15
35
|
end
|
16
36
|
|
37
|
+
# Searches for a filter which matches the request's HTTP method and url.
|
38
|
+
#
|
39
|
+
# @param [WebRequest] request the pending HTTP request
|
40
|
+
# @return [Hash] when a filter matches the request
|
41
|
+
# @return [nil] when no filters match the request
|
42
|
+
def find_filter(request)
|
43
|
+
Akephalos.filters.find do |filter|
|
44
|
+
request.http_method === filter[:method] && request.url.to_s =~ filter[:filter]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# This method is called by WebClient when a page is requested, and will
|
49
|
+
# return a mock response if the request matches a defined filter or else
|
50
|
+
# return the actual response.
|
51
|
+
#
|
52
|
+
# @api htmlunit
|
53
|
+
# @param [WebRequest] request the pending HTTP request
|
54
|
+
# @return [WebResponseImpl]
|
17
55
|
def getResponse(request)
|
18
56
|
filter(request) || super
|
19
57
|
end
|
20
58
|
|
59
|
+
# Map of status codes to their English descriptions.
|
21
60
|
HTTP_STATUS_CODES = {
|
22
61
|
100 => "Continue",
|
23
62
|
101 => "Switching Protocols",
|
@@ -78,5 +117,6 @@ module Akephalos
|
|
78
117
|
530 => "User access denied"
|
79
118
|
}.freeze
|
80
119
|
end
|
120
|
+
|
81
121
|
end
|
82
122
|
end
|
@@ -1,17 +1,44 @@
|
|
1
1
|
module Akephalos
|
2
|
-
def self.configuration
|
3
|
-
@configuration ||= {}
|
4
|
-
end
|
5
2
|
|
6
|
-
|
7
|
-
|
3
|
+
@configuration = {}
|
4
|
+
|
5
|
+
class << self
|
6
|
+
# @return [Hash] the configuration
|
7
|
+
attr_accessor :configuration
|
8
8
|
end
|
9
9
|
|
10
10
|
module Filters
|
11
|
+
# @return [Array] all defined filters
|
11
12
|
def filters
|
12
13
|
configuration[:filters] ||= []
|
13
14
|
end
|
14
15
|
|
16
|
+
# Defines a new filter to be tested by Akephalos::Filter when executing
|
17
|
+
# page requests. An HTTP method and a regex or string to match against the
|
18
|
+
# URL are required for defining a filter.
|
19
|
+
#
|
20
|
+
# You can additionally pass the following options to define how the
|
21
|
+
# filtered request should respond:
|
22
|
+
#
|
23
|
+
# :status (defaults to 200)
|
24
|
+
# :body (defaults to "")
|
25
|
+
# :headers (defaults to {})
|
26
|
+
#
|
27
|
+
# If we define a filter with no additional options, then, we will get an
|
28
|
+
# empty HTML response:
|
29
|
+
#
|
30
|
+
# Akephalos.filter :post, "http://example.com"
|
31
|
+
# Akephalos.filter :any, %r{http://.*\.com}
|
32
|
+
#
|
33
|
+
# If you instead, say, wanted to simulate a failure in an external system,
|
34
|
+
# you could do this:
|
35
|
+
#
|
36
|
+
# Akephalos.filter :post, "http://example.com",
|
37
|
+
# :status => 500, :body => "Something went wrong"
|
38
|
+
#
|
39
|
+
# @param [Symbol] method the HTTP method to match
|
40
|
+
# @param [RegExp, String] regex URL matcher
|
41
|
+
# @param [Hash] options response values
|
15
42
|
def filter(method, regex, options = {})
|
16
43
|
regex = Regexp.new(Regexp.escape(regex)) if regex.is_a?(String)
|
17
44
|
filters << {:method => method, :filter => regex, :status => 200, :body => "", :headers => {}}.merge!(options)
|
data/lib/akephalos/console.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# Begin a new Capybara session, by default connecting to localhost on port
|
2
|
+
# 3000.
|
1
3
|
def session
|
2
4
|
Capybara.app_host = "http://localhost:3000"
|
3
5
|
@session ||= Capybara::Session.new(:Akephalos)
|
@@ -5,8 +7,11 @@ end
|
|
5
7
|
alias page session
|
6
8
|
|
7
9
|
module Akephalos
|
10
|
+
# Simple class for starting an IRB session.
|
8
11
|
class Console
|
9
12
|
|
13
|
+
# Start an IRB session. Tries to load irb/completion, and also loads a
|
14
|
+
# .irbrc file if it exists.
|
10
15
|
def self.start
|
11
16
|
require 'irb'
|
12
17
|
|
@@ -1,4 +1,13 @@
|
|
1
|
+
# Reopen com.gargoylesoftware.htmlunit.HttpMethod to add convenience methods.
|
1
2
|
class HttpMethod
|
3
|
+
|
4
|
+
# Loosely compare HttpMethod with another object, accepting either an
|
5
|
+
# HttpMethod instance or a symbol describing the method. Note that :any is a
|
6
|
+
# special symbol which will always return true.
|
7
|
+
#
|
8
|
+
# @param [HttpMethod] other an HtmlUnit HttpMethod object
|
9
|
+
# @param [Symbol] other a symbolized representation of an http method
|
10
|
+
# @return [true/false]
|
2
11
|
def ===(other)
|
3
12
|
case other
|
4
13
|
when HttpMethod
|
@@ -15,4 +24,5 @@ class HttpMethod
|
|
15
24
|
self == self.class::DELETE
|
16
25
|
end
|
17
26
|
end
|
27
|
+
|
18
28
|
end
|
data/lib/akephalos/node.rb
CHANGED
@@ -1,22 +1,38 @@
|
|
1
1
|
module Akephalos
|
2
|
+
|
3
|
+
# Akephalos::Node wraps HtmlUnit's DOMNode class, providing a simple API for
|
4
|
+
# interacting with an element on the page.
|
2
5
|
class Node
|
6
|
+
# @param [HtmlUnit::DOMNode] node
|
3
7
|
def initialize(node)
|
4
8
|
@nodes = []
|
5
9
|
@_node = node
|
6
10
|
end
|
7
11
|
|
12
|
+
# @return [true, false] whether the element is checked
|
8
13
|
def checked?
|
9
14
|
@_node.isChecked
|
10
15
|
end
|
11
16
|
|
17
|
+
# @return [String] inner text of the node
|
12
18
|
def text
|
13
19
|
@_node.asText
|
14
20
|
end
|
15
21
|
|
22
|
+
# Return the value of the node's attribute.
|
23
|
+
#
|
24
|
+
# @param [String] name attribute on node
|
25
|
+
# @return [String] the value of the named attribute
|
26
|
+
# @return [nil] when the node does not have the named attribute
|
16
27
|
def [](name)
|
17
28
|
@_node.hasAttribute(name.to_s) ? @_node.getAttribute(name.to_s) : nil
|
18
29
|
end
|
19
30
|
|
31
|
+
# Return the value of a form element. If the element is a select box and
|
32
|
+
# has "multiple" declared as an attribute, then all selected options will
|
33
|
+
# be returned as an array.
|
34
|
+
#
|
35
|
+
# @return [String, Array<String>] the node's value
|
20
36
|
def value
|
21
37
|
case tag_name
|
22
38
|
when "select"
|
@@ -33,6 +49,9 @@ module Akephalos
|
|
33
49
|
end
|
34
50
|
end
|
35
51
|
|
52
|
+
# Set the value of the form input.
|
53
|
+
#
|
54
|
+
# @param [String] value
|
36
55
|
def value=(value)
|
37
56
|
case tag_name
|
38
57
|
when "textarea"
|
@@ -42,48 +61,76 @@ module Akephalos
|
|
42
61
|
end
|
43
62
|
end
|
44
63
|
|
64
|
+
# Select an option from a select box by its value.
|
65
|
+
#
|
66
|
+
# @return [true, false] whether the selection was successful
|
45
67
|
def select_option(option)
|
46
68
|
opt = @_node.getOptions.detect { |o| o.asText == option }
|
47
69
|
|
48
70
|
opt && opt.setSelected(true)
|
49
71
|
end
|
50
72
|
|
73
|
+
# Unselect an option from a select box by its value.
|
74
|
+
#
|
75
|
+
# @return [true, false] whether the unselection was successful
|
51
76
|
def unselect_option(option)
|
52
77
|
opt = @_node.getOptions.detect { |o| o.asText == option }
|
53
78
|
|
54
79
|
opt && opt.setSelected(false)
|
55
80
|
end
|
56
81
|
|
82
|
+
# Return the option elements for a select box.
|
83
|
+
#
|
84
|
+
# @return [Array<Node>] the options
|
57
85
|
def options
|
58
86
|
@_node.getOptions.map { |node| Node.new(node) }
|
59
87
|
end
|
60
88
|
|
89
|
+
# Return the selected option elements for a select box.
|
90
|
+
#
|
91
|
+
# @return [Array<Node>] the selected options
|
61
92
|
def selected_options
|
62
93
|
@_node.getSelectedOptions.map { |node| Node.new(node) }
|
63
94
|
end
|
64
95
|
|
96
|
+
# Fire a JavaScript event on the current node. Note that you should not
|
97
|
+
# prefix event names with "on", so:
|
98
|
+
#
|
99
|
+
# link.fire_event('mousedown')
|
100
|
+
#
|
101
|
+
# @param [String] JavaScript event name
|
65
102
|
def fire_event(name)
|
66
103
|
@_node.fireEvent(name)
|
67
104
|
end
|
68
105
|
|
106
|
+
# @return [String] the node's tag name
|
69
107
|
def tag_name
|
70
108
|
@_node.getNodeName
|
71
109
|
end
|
72
110
|
|
111
|
+
# @return [true, false] whether the node is visible to the user accounting
|
112
|
+
# for CSS.
|
73
113
|
def visible?
|
74
114
|
@_node.isDisplayed
|
75
115
|
end
|
76
116
|
|
117
|
+
# Click the node and then wait for any triggered JavaScript callbacks to
|
118
|
+
# fire.
|
77
119
|
def click
|
78
120
|
@_node.click
|
79
121
|
@_node.getPage.getEnclosingWindow.getJobManager.waitForJobs(1000)
|
80
122
|
@_node.getPage.getEnclosingWindow.getJobManager.waitForJobsStartingBefore(1000)
|
81
123
|
end
|
82
124
|
|
125
|
+
# Search for child nodes which match the given XPath selector.
|
126
|
+
#
|
127
|
+
# @param [String] selector an XPath selector
|
128
|
+
# @return [Array<Node>] the matched nodes
|
83
129
|
def find(selector)
|
84
130
|
nodes = @_node.getByXPath(selector).map { |node| Node.new(node) }
|
85
131
|
@nodes << nodes
|
86
132
|
nodes
|
87
133
|
end
|
88
134
|
end
|
135
|
+
|
89
136
|
end
|
data/lib/akephalos/page.rb
CHANGED
@@ -1,39 +1,113 @@
|
|
1
1
|
module Akephalos
|
2
|
+
|
3
|
+
# Akephalos::Page wraps HtmlUnit's HtmlPage class, exposing an API for
|
4
|
+
# interacting with a page in the browser.
|
2
5
|
class Page
|
6
|
+
# @param [HtmlUnit::HtmlPage] page
|
3
7
|
def initialize(page)
|
4
8
|
@nodes = []
|
5
9
|
@_page = page
|
6
10
|
end
|
7
11
|
|
12
|
+
# Search for nodes which match the given XPath selector.
|
13
|
+
#
|
14
|
+
# @param [String] selector an XPath selector
|
15
|
+
# @return [Array<Node>] the matched nodes
|
8
16
|
def find(selector)
|
9
|
-
nodes =
|
17
|
+
nodes = current_frame.getByXPath(selector).map { |node| Node.new(node) }
|
10
18
|
@nodes << nodes
|
11
19
|
nodes
|
12
20
|
end
|
13
21
|
|
22
|
+
# Return the page's source, including any JavaScript-triggered DOM changes.
|
23
|
+
#
|
24
|
+
# @return [String] the page's modified source
|
14
25
|
def modified_source
|
15
|
-
|
26
|
+
current_frame.asXml
|
16
27
|
end
|
17
28
|
|
29
|
+
# Return the page's source as returned by the web server.
|
30
|
+
#
|
31
|
+
# @return [String] the page's original source
|
18
32
|
def source
|
19
|
-
|
33
|
+
current_frame.getWebResponse.getContentAsString
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Hash{String => String}] the page's response headers
|
37
|
+
def response_headers
|
38
|
+
headers = current_frame.getWebResponse.getResponseHeaders.map do |header|
|
39
|
+
[header.getName, header.getValue]
|
40
|
+
end
|
41
|
+
Hash[*headers.flatten]
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Integer] the response's status code
|
45
|
+
def status_code
|
46
|
+
current_frame.getWebResponse.getStatusCode
|
47
|
+
end
|
48
|
+
|
49
|
+
# Execute the given block in the context of the frame specified.
|
50
|
+
#
|
51
|
+
# @param [String] frame_id the frame's id
|
52
|
+
# @return [true] if the frame is found
|
53
|
+
# @return [nil] if the frame is not found
|
54
|
+
def within_frame(frame_id)
|
55
|
+
return unless @current_frame = find_frame(frame_id)
|
56
|
+
yield
|
57
|
+
true
|
58
|
+
ensure
|
59
|
+
@current_frame = nil
|
20
60
|
end
|
21
61
|
|
62
|
+
# @return [String] the current page's URL.
|
22
63
|
def current_url
|
23
|
-
|
64
|
+
current_frame.getWebResponse.getRequestSettings.getUrl.toString
|
24
65
|
end
|
25
66
|
|
67
|
+
# Execute JavaScript against the current page, discarding any return value.
|
68
|
+
#
|
69
|
+
# @param [String] script the JavaScript to be executed
|
70
|
+
# @return [nil]
|
26
71
|
def execute_script(script)
|
27
|
-
|
72
|
+
current_frame.executeJavaScript(script)
|
28
73
|
nil
|
29
74
|
end
|
30
75
|
|
76
|
+
# Execute JavaScript against the current page and return the results.
|
77
|
+
#
|
78
|
+
# @param [String] script the JavaScript to be executed
|
79
|
+
# @return the result of the JavaScript
|
31
80
|
def evaluate_script(script)
|
32
|
-
|
81
|
+
current_frame.executeJavaScript(script).getJavaScriptResult
|
33
82
|
end
|
34
83
|
|
84
|
+
# Compare this page with an HtmlUnit page.
|
85
|
+
#
|
86
|
+
# @param [HtmlUnit::HtmlPage] other an HtmlUnit page
|
87
|
+
# @return [true, false]
|
35
88
|
def ==(other)
|
36
89
|
@_page == other
|
37
90
|
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# Return the current frame. Usually just @_page, except when inside of the
|
95
|
+
# within_frame block.
|
96
|
+
#
|
97
|
+
# @return [HtmlUnit::HtmlPage] the current frame
|
98
|
+
def current_frame
|
99
|
+
@current_frame || @_page
|
100
|
+
end
|
101
|
+
|
102
|
+
# @param [String] id the frame's id
|
103
|
+
# @return [HtmlUnit::HtmlPage] the specified frame
|
104
|
+
# @return [nil] if no frame is found
|
105
|
+
def find_frame(id)
|
106
|
+
frame = @_page.getFrames.find do |frame|
|
107
|
+
frame.getFrameElement.getAttribute("id") == id
|
108
|
+
end
|
109
|
+
frame.getEnclosedPage if frame
|
110
|
+
end
|
38
111
|
end
|
112
|
+
|
39
113
|
end
|
@@ -2,10 +2,10 @@ require 'drb/drb'
|
|
2
2
|
|
3
3
|
# We need to define our own NativeException class for the cases when a native
|
4
4
|
# exception is raised by the JRuby DRb server.
|
5
|
-
class NativeException < StandardError; end
|
5
|
+
class NativeException < StandardError; end
|
6
6
|
|
7
7
|
module Akephalos
|
8
|
-
|
8
|
+
|
9
9
|
# The +RemoteClient+ class provides an interface to an +Akephalos::Client+
|
10
10
|
# isntance on a remote DRb server.
|
11
11
|
#
|
@@ -16,9 +16,10 @@ module Akephalos
|
|
16
16
|
class RemoteClient
|
17
17
|
@socket_file = "/tmp/akephalos.#{Process.pid}.sock"
|
18
18
|
|
19
|
-
|
20
|
-
# Starts a remote akephalos server and returns the remote Akephalos::Client
|
19
|
+
# Start a remote akephalos server and return the remote Akephalos::Client
|
21
20
|
# instance.
|
21
|
+
#
|
22
|
+
# @return [DRbObject] the remote client instance
|
22
23
|
def self.new
|
23
24
|
start!
|
24
25
|
DRb.start_service
|
@@ -31,8 +32,7 @@ module Akephalos
|
|
31
32
|
client
|
32
33
|
end
|
33
34
|
|
34
|
-
|
35
|
-
# Start a remote server process, returning when it is available for use.
|
35
|
+
# Start a remote server process and return when it is available for use.
|
36
36
|
def self.start!
|
37
37
|
remote_client = fork do
|
38
38
|
exec("#{Akephalos::BIN_DIR + 'akephalos'} #{@socket_file}")
|
data/lib/akephalos/server.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
|
-
# This file runs a JRuby DRb server, and is run by `akephalos --server`.
|
2
1
|
require "pathname"
|
3
2
|
require "drb/drb"
|
4
3
|
require "akephalos/client"
|
5
4
|
|
6
5
|
# In ruby-1.8.7 and later, the message for a NameError exception is lazily
|
7
6
|
# evaluated. There are, however, different implementations of this between ruby
|
8
|
-
# and
|
7
|
+
# and jruby, so we realize these messages when sending over DRb.
|
9
8
|
class NameError::Message
|
9
|
+
# @note This method is called by DRb before sending the error to the remote
|
10
|
+
# connection.
|
11
|
+
# @return [String] the inner message.
|
10
12
|
def _dump
|
11
13
|
to_s
|
12
14
|
end
|
@@ -15,11 +17,18 @@ end
|
|
15
17
|
[Akephalos::Page, Akephalos::Node].each { |klass| klass.send(:include, DRbUndumped) }
|
16
18
|
|
17
19
|
module Akephalos
|
20
|
+
|
21
|
+
# Akephalos::Server is used by `akephalos --server` to start a DRb server
|
22
|
+
# serving an instance of Akephalos::Client.
|
18
23
|
class Server
|
24
|
+
# Start DRb service for an Akephalos::Client.
|
25
|
+
#
|
26
|
+
# @param [String] socket_file path to socket file to start
|
19
27
|
def self.start!(socket_file)
|
20
28
|
client = Client.new
|
21
29
|
DRb.start_service("drbunix://#{socket_file}", client)
|
22
30
|
DRb.thread.join
|
23
31
|
end
|
24
32
|
end
|
33
|
+
|
25
34
|
end
|
data/lib/akephalos/version.rb
CHANGED
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 2
|
8
|
-
-
|
9
|
-
version: 0.2.
|
8
|
+
- 4
|
9
|
+
version: 0.2.4
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Bernerd Schaefer
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-09-14 00:00:00 -05:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -80,7 +80,6 @@ extra_rdoc_files: []
|
|
80
80
|
files:
|
81
81
|
- lib/akephalos/capybara.rb
|
82
82
|
- lib/akephalos/client/filter.rb
|
83
|
-
- lib/akephalos/client/listener.rb
|
84
83
|
- lib/akephalos/client.rb
|
85
84
|
- lib/akephalos/configuration.rb
|
86
85
|
- lib/akephalos/console.rb
|
@@ -1,18 +0,0 @@
|
|
1
|
-
module Akephalos
|
2
|
-
class Client
|
3
|
-
class Listener
|
4
|
-
include com.gargoylesoftware.htmlunit.WebWindowListener
|
5
|
-
|
6
|
-
def initialize(client)
|
7
|
-
@client = client
|
8
|
-
end
|
9
|
-
|
10
|
-
def webWindowClosed(event)
|
11
|
-
end
|
12
|
-
|
13
|
-
def webWindowContentChanged(event)
|
14
|
-
@client.page = event.getNewPage
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|