akephalos 0.2.3 → 0.2.4
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/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
|