yellowlab-akephalos 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/MIT_LICENSE +20 -0
  2. data/README.md +109 -0
  3. data/bin/akephalos +88 -0
  4. data/lib/akephalos.rb +19 -0
  5. data/lib/akephalos/capybara.rb +343 -0
  6. data/lib/akephalos/client.rb +181 -0
  7. data/lib/akephalos/client/cookies.rb +73 -0
  8. data/lib/akephalos/client/filter.rb +120 -0
  9. data/lib/akephalos/configuration.rb +49 -0
  10. data/lib/akephalos/console.rb +32 -0
  11. data/lib/akephalos/cucumber.rb +6 -0
  12. data/lib/akephalos/htmlunit.rb +36 -0
  13. data/lib/akephalos/htmlunit/ext/confirm_handler.rb +18 -0
  14. data/lib/akephalos/htmlunit/ext/http_method.rb +30 -0
  15. data/lib/akephalos/node.rb +188 -0
  16. data/lib/akephalos/page.rb +113 -0
  17. data/lib/akephalos/remote_client.rb +92 -0
  18. data/lib/akephalos/server.rb +79 -0
  19. data/lib/akephalos/version.rb +3 -0
  20. data/src/htmlunit/apache-mime4j-0.6.jar +0 -0
  21. data/src/htmlunit/commons-codec-1.4.jar +0 -0
  22. data/src/htmlunit/commons-collections-3.2.1.jar +0 -0
  23. data/src/htmlunit/commons-io-1.4.jar +0 -0
  24. data/src/htmlunit/commons-lang-2.4.jar +0 -0
  25. data/src/htmlunit/commons-logging-1.1.1.jar +0 -0
  26. data/src/htmlunit/cssparser-0.9.5.jar +0 -0
  27. data/src/htmlunit/htmlunit-2.8.jar +0 -0
  28. data/src/htmlunit/htmlunit-core-js-2.8.jar +0 -0
  29. data/src/htmlunit/httpclient-4.0.1.jar +0 -0
  30. data/src/htmlunit/httpcore-4.0.1.jar +0 -0
  31. data/src/htmlunit/httpmime-4.0.1.jar +0 -0
  32. data/src/htmlunit/nekohtml-1.9.14.jar +0 -0
  33. data/src/htmlunit/sac-1.3.jar +0 -0
  34. data/src/htmlunit/serializer-2.7.1.jar +0 -0
  35. data/src/htmlunit/xalan-2.7.1.jar +0 -0
  36. data/src/htmlunit/xercesImpl-2.9.1.jar +0 -0
  37. data/src/htmlunit/xml-apis-1.3.04.jar +0 -0
  38. metadata +167 -0
@@ -0,0 +1,6 @@
1
+ require 'capybara/cucumber'
2
+
3
+ Before('@akephalos') do
4
+ Capybara.current_driver = :akephalos
5
+ end
6
+
@@ -0,0 +1,36 @@
1
+ require "pathname"
2
+ require "java"
3
+
4
+ dependency_directory = $:.detect { |path| Dir[File.join(path, 'htmlunit/htmlunit-*.jar')].any? }
5
+
6
+ raise "Could not find htmlunit/htmlunit-VERSION.jar in load path:\n [ #{$:.join(",\n ")}\n ]" unless dependency_directory
7
+
8
+ Dir[File.join(dependency_directory, "htmlunit/*.jar")].each do |jar|
9
+ require jar
10
+ end
11
+
12
+ java.lang.System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog")
13
+ java.lang.System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "fatal")
14
+ java.lang.System.setProperty("org.apache.commons.logging.simplelog.showdatetime", "true")
15
+
16
+ # Container module for com.gargoylesoftware.htmlunit namespace.
17
+ module HtmlUnit
18
+ java_import "com.gargoylesoftware.htmlunit.BrowserVersion"
19
+ java_import "com.gargoylesoftware.htmlunit.History"
20
+ java_import "com.gargoylesoftware.htmlunit.HttpMethod"
21
+ java_import 'com.gargoylesoftware.htmlunit.ConfirmHandler'
22
+ java_import "com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController"
23
+ java_import "com.gargoylesoftware.htmlunit.SilentCssErrorHandler"
24
+ java_import "com.gargoylesoftware.htmlunit.WebClient"
25
+ java_import "com.gargoylesoftware.htmlunit.WebResponseData"
26
+ java_import "com.gargoylesoftware.htmlunit.WebResponseImpl"
27
+
28
+ # Container module for com.gargoylesoftware.htmlunit.util namespace.
29
+ module Util
30
+ java_import "com.gargoylesoftware.htmlunit.util.NameValuePair"
31
+ java_import "com.gargoylesoftware.htmlunit.util.WebConnectionWrapper"
32
+ end
33
+
34
+ # Disable history tracking
35
+ History.field_reader :ignoreNewPages_
36
+ end
@@ -0,0 +1,18 @@
1
+ # Reopen com.gargoylesoftware.htmlunit.ConfirmHandler to provide an interface to
2
+ # confirm a dialog and capture its message
3
+ module HtmlUnit
4
+ module ConfirmHandler
5
+
6
+ # Boolean - true for ok, false for cancel
7
+ attr_accessor :handleConfirmValue
8
+
9
+ # last confirmation's message
10
+ attr_reader :text
11
+
12
+ # handleConfirm will be called by htmlunit on a confirm, so store the message.
13
+ def handleConfirm(page, message)
14
+ @text = message
15
+ return handleConfirmValue.nil? ? true : handleConfirmValue
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ module HtmlUnit
2
+ # Reopen HtmlUnit's HttpMethod class to add convenience methods.
3
+ class HttpMethod
4
+
5
+ # Loosely compare HttpMethod with another object, accepting either an
6
+ # HttpMethod instance or a symbol describing the method. Note that :any is a
7
+ # special symbol which will always return true.
8
+ #
9
+ # @param [HttpMethod] other an HtmlUnit HttpMethod object
10
+ # @param [Symbol] other a symbolized representation of an http method
11
+ # @return [true/false]
12
+ def ===(other)
13
+ case other
14
+ when HttpMethod
15
+ super
16
+ when :any
17
+ true
18
+ when :get
19
+ self == self.class::GET
20
+ when :post
21
+ self == self.class::POST
22
+ when :put
23
+ self == self.class::PUT
24
+ when :delete
25
+ self == self.class::DELETE
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,188 @@
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.
5
+ class Node
6
+ # @param [HtmlUnit::DOMNode] node
7
+ def initialize(node)
8
+ @nodes = []
9
+ @_node = node
10
+ end
11
+
12
+ # @return [true, false] whether the element is checked
13
+ def checked?
14
+ if @_node.respond_to?(:isChecked)
15
+ @_node.isChecked
16
+ else
17
+ !! self[:checked]
18
+ end
19
+ end
20
+
21
+ # @return [String] inner text of the node
22
+ # Returns a textual representation of this element that represents what would
23
+ # be visible to the user if this page was shown in a web browser.
24
+ # For example, a single-selection select element would return the currently
25
+ # selected value as text.
26
+ # Note: This will cleanup/reduce whitespace
27
+ def text
28
+ @_node.asText
29
+ end
30
+
31
+ # Returns the raw text content of this node and its descendants...
32
+ def text_content
33
+ @_node.getTextContent
34
+ end
35
+
36
+ # Returns a string representation of the XML document from this element and
37
+ # all it's children (recursively). The charset used is the current page encoding.
38
+ def xml
39
+ @_node.asXml
40
+ end
41
+
42
+ # Return the value of the node's attribute.
43
+ #
44
+ # @param [String] name attribute on node
45
+ # @return [String] the value of the named attribute
46
+ # @return [nil] when the node does not have the named attribute
47
+ def [](name)
48
+ @_node.hasAttribute(name.to_s) ? @_node.getAttribute(name.to_s) : nil
49
+ end
50
+
51
+ # Return the value of a form element. If the element is a select box and
52
+ # has "multiple" declared as an attribute, then all selected options will
53
+ # be returned as an array.
54
+ #
55
+ # @return [String, Array<String>] the node's value
56
+ def value
57
+ case tag_name
58
+ when "select"
59
+ if self[:multiple]
60
+ selected_options.map { |option| option.value }
61
+ else
62
+ selected_option = @_node.selected_options.first
63
+ selected_option ? Node.new(selected_option).value : nil
64
+ end
65
+ when "option"
66
+ self[:value] || text
67
+ when "textarea"
68
+ @_node.getText
69
+ else
70
+ self[:value]
71
+ end
72
+ end
73
+
74
+ # Set the value of the form input.
75
+ #
76
+ # @param [String] value
77
+ def value=(value)
78
+ case tag_name
79
+ when "textarea"
80
+ @_node.setText("")
81
+ type(value)
82
+ when "input"
83
+ if file_input?
84
+ @_node.setValueAttribute(value)
85
+ else
86
+ @_node.setValueAttribute("")
87
+ type(value)
88
+ end
89
+ end
90
+ end
91
+
92
+ # Types each character into a text or input field.
93
+ #
94
+ # @param [String] value the string to type
95
+ def type(value)
96
+ value.each_char do |c|
97
+ @_node.type(c)
98
+ end
99
+ end
100
+
101
+ # @return [true, false] whether the node allows multiple-option selection (if the node is a select).
102
+ def multiple_select?
103
+ !self[:multiple].nil?
104
+ end
105
+
106
+ # @return [true, false] whether the node is a file input
107
+ def file_input?
108
+ tag_name == "input" && @_node.getAttribute("type") == "file"
109
+ end
110
+
111
+
112
+ # Unselect an option.
113
+ #
114
+ # @return [true, false] whether the unselection was successful
115
+ def unselect
116
+ @_node.setSelected(false)
117
+ end
118
+
119
+ # Return the option elements for a select box.
120
+ #
121
+ # @return [Array<Node>] the options
122
+ def options
123
+ @_node.getOptions.map { |node| Node.new(node) }
124
+ end
125
+
126
+ # Return the selected option elements for a select box.
127
+ #
128
+ # @return [Array<Node>] the selected options
129
+ def selected_options
130
+ @_node.getSelectedOptions.map { |node| Node.new(node) }
131
+ end
132
+
133
+ # Fire a JavaScript event on the current node. Note that you should not
134
+ # prefix event names with "on", so:
135
+ #
136
+ # link.fire_event('mousedown')
137
+ #
138
+ # @param [String] JavaScript event name
139
+ def fire_event(name)
140
+ @_node.fireEvent(name)
141
+ end
142
+
143
+ # @return [String] the node's tag name
144
+ def tag_name
145
+ @_node.getNodeName
146
+ end
147
+
148
+ # @return [true, false] whether the node is visible to the user accounting
149
+ # for CSS.
150
+ def visible?
151
+ @_node.isDisplayed
152
+ end
153
+
154
+ # @return [true, false] whether the node is selected to the user accounting
155
+ # for CSS.
156
+ def selected?
157
+ if @_node.respond_to?(:isSelected)
158
+ @_node.isSelected
159
+ else
160
+ !! self[:selected]
161
+ end
162
+ end
163
+
164
+ # Click the node and then wait for any triggered JavaScript callbacks to
165
+ # fire.
166
+ def click
167
+ @_node.click
168
+ @_node.getPage.getEnclosingWindow.getJobManager.waitForJobs(1000)
169
+ @_node.getPage.getEnclosingWindow.getJobManager.waitForJobsStartingBefore(1000)
170
+ end
171
+
172
+ # Search for child nodes which match the given XPath selector.
173
+ #
174
+ # @param [String] selector an XPath selector
175
+ # @return [Array<Node>] the matched nodes
176
+ def find(selector)
177
+ nodes = @_node.getByXPath(selector).map { |node| Node.new(node) }
178
+ @nodes << nodes
179
+ nodes
180
+ end
181
+
182
+ # @return [String] the XPath expression for this node
183
+ def xpath
184
+ @_node.getCanonicalXPath
185
+ end
186
+ end
187
+
188
+ end
@@ -0,0 +1,113 @@
1
+ module Akephalos
2
+
3
+ # Akephalos::Page wraps HtmlUnit's HtmlPage class, exposing an API for
4
+ # interacting with a page in the browser.
5
+ class Page
6
+ # @param [HtmlUnit::HtmlPage] page
7
+ def initialize(page)
8
+ @nodes = []
9
+ @_page = page
10
+ end
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
16
+ def find(selector)
17
+ nodes = current_frame.getByXPath(selector).map { |node| Node.new(node) }
18
+ @nodes << nodes
19
+ nodes
20
+ end
21
+
22
+ # Return the page's source, including any JavaScript-triggered DOM changes.
23
+ #
24
+ # @return [String] the page's modified source
25
+ def modified_source
26
+ current_frame.asXml
27
+ end
28
+
29
+ # Return the page's source as returned by the web server.
30
+ #
31
+ # @return [String] the page's original source
32
+ def source
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
60
+ end
61
+
62
+ # @return [String] the current page's URL.
63
+ def current_url
64
+ current_frame.getWebResponse.getRequestSettings.getUrl.toString
65
+ end
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]
71
+ def execute_script(script)
72
+ current_frame.executeJavaScript(script)
73
+ nil
74
+ end
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
80
+ def evaluate_script(script)
81
+ current_frame.executeJavaScript(script).getJavaScriptResult
82
+ end
83
+
84
+ # Compare this page with an HtmlUnit page.
85
+ #
86
+ # @param [HtmlUnit::HtmlPage] other an HtmlUnit page
87
+ # @return [true, false]
88
+ def ==(other)
89
+ @_page == other
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
111
+ end
112
+
113
+ end
@@ -0,0 +1,92 @@
1
+ require 'socket'
2
+ require 'drb/drb'
3
+
4
+ # We need to define our own NativeException class for the cases when a native
5
+ # exception is raised by the JRuby DRb server.
6
+ class NativeException < StandardError; end
7
+
8
+ module Akephalos
9
+
10
+ # The +RemoteClient+ class provides an interface to an +Akephalos::Client+
11
+ # isntance on a remote DRb server.
12
+ #
13
+ # == Usage
14
+ # client = Akephalos::RemoteClient.new
15
+ # client.visit "http://www.oinopa.com"
16
+ # client.page.source # => "<!DOCTYPE html PUBLIC..."
17
+ class RemoteClient
18
+ # @return [DRbObject] a new instance of Akephalos::Client from the DRb
19
+ # server
20
+ def self.new(options = {})
21
+ manager.new_client(options)
22
+ end
23
+
24
+ # Starts a remove JRuby DRb server unless already running and returns an
25
+ # instance of Akephalos::ClientManager.
26
+ #
27
+ # @return [DRbObject] an instance of Akephalos::ClientManager
28
+ def self.manager
29
+ return @manager if defined?(@manager)
30
+
31
+ server_port = start!
32
+
33
+ DRb.start_service("druby://127.0.0.1:#{find_available_port}")
34
+ manager = DRbObject.new_with_uri("druby://127.0.0.1:#{server_port}")
35
+
36
+ # We want to share our local configuration with the remote server
37
+ # process, so we share an undumped version of our configuration. This
38
+ # lets us continue to make changes locally and have them reflected in the
39
+ # remote process.
40
+ manager.configuration = Akephalos.configuration.extend(DRbUndumped)
41
+
42
+ @manager = manager
43
+ end
44
+
45
+ # Start a remote server process and return when it is available for use.
46
+ def self.start!
47
+ port = find_available_port
48
+
49
+ remote_client = IO.popen("ruby #{Akephalos::BIN_DIR + 'akephalos'} #{port}")
50
+
51
+ # Set up a monitor thread to detect if the forked server exits
52
+ # prematurely.
53
+ server_monitor = Thread.new { Thread.current[:exited] = Process.wait(remote_client.pid) }
54
+
55
+ # Wait for the server to be accessible on the socket we specified.
56
+ until responsive?(port)
57
+ exit!(1) if server_monitor[:exited]
58
+ sleep 0.5
59
+ end
60
+ server_monitor.kill
61
+
62
+ # Ensure that the remote server shuts down gracefully when we are
63
+ # finished.
64
+ at_exit { Process.kill(:INT, remote_client.pid) }
65
+
66
+ port
67
+ end
68
+
69
+ private
70
+
71
+ # @api private
72
+ # @param [Integer] port the port to check for responsiveness
73
+ # @return [true, false] whether the port is responsive
74
+ def self.responsive?(port)
75
+ socket = TCPSocket.open('127.0.0.1', port)
76
+ true
77
+ rescue Errno::ECONNREFUSED
78
+ false
79
+ ensure
80
+ socket.close if socket
81
+ end
82
+
83
+ # @api private
84
+ # @return [Integer] the next available port
85
+ def self.find_available_port
86
+ server = TCPServer.new('127.0.0.1', 0)
87
+ server.addr[1]
88
+ ensure
89
+ server.close if server
90
+ end
91
+ end
92
+ end