akephalos 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -38,6 +38,36 @@ Here's some sample RSpec code:
38
38
  end
39
39
  end
40
40
 
41
+ ## Configuration
42
+
43
+ There are now a few configuration options available through Capybara's new
44
+ `register_driver` API.
45
+
46
+ ### Using a different browser
47
+
48
+ HtmlUnit supports a few browser implementations, and you can choose which
49
+ browser you would like to use through Akephalos. By default, Akephalos uses
50
+ Firefox 3.6.
51
+
52
+ Capybara.register_driver :akephalos do |app|
53
+ # available options:
54
+ # :ie6, :ie7, :ie8, :firefox_3, :firefox_3_6
55
+ Capybara::Driver::Akephalos.new(app, :browser => :ie8)
56
+ end
57
+
58
+ ### Ignoring javascript errors
59
+
60
+ By default HtmlUnit (and Akephalos) will raise an exception when an error
61
+ is encountered in javascript files. This is generally desireable, except
62
+ that certain libraries aren't supported by HtmlUnit. If possible, it's
63
+ best to keep the default behavior, and use Filters (see below) to mock
64
+ offending libraries. If needed, however, you can configure Akephalos to
65
+ ignore javascript errors.
66
+
67
+ Capybara.register_driver :akephalos do |app|
68
+ Capybara::Driver::Akephalos.new(app, :validate_scripts => false)
69
+ end
70
+
41
71
  ## More
42
72
 
43
73
  * [bin/akephalos](http://bernerdschaefer.github.com/akephalos/akephalos-bin.html)
@@ -7,7 +7,7 @@ require "optparse"
7
7
  options = { :interactive => false }
8
8
 
9
9
  parser = OptionParser.new do |opts|
10
- opts.banner = "Usage: akephalos [--interactive, --use-htmlunit-snapshot] | [--server] <socket_file>"
10
+ opts.banner = "Usage: akephalos [--interactive, --use-htmlunit-snapshot] | [--server] <port>"
11
11
  opts.on("-s", "--server", "Run in server mode (default)")
12
12
  opts.on("-i", "--interactive", "Run in interactive mode") { options[:interactive] = true }
13
13
  opts.on("--use-htmlunit-snapshot", "Use the snapshot of htmlunit") { options[:use_htmlunit_snapshot] = true }
@@ -30,15 +30,15 @@ when options[:use_htmlunit_snapshot]
30
30
  Dir.chdir("vendor") do
31
31
  $stdout.print "Downloading latest snapshot... "
32
32
  $stdout.flush
33
- %x[curl -O http://build.canoo.com/htmlunit/artifacts/htmlunit-2.8-SNAPSHOT-with-dependencies.zip &> /dev/null]
33
+ %x[curl -O http://build.canoo.com/htmlunit/artifacts/htmlunit-2.9-SNAPSHOT-with-dependencies.zip &> /dev/null]
34
34
  puts "done"
35
35
 
36
36
  $stdout.print "Extracting dependencies... "
37
37
  $stdout.flush
38
- %x[unzip -j -d htmlunit htmlunit-2.8-SNAPSHOT-with-dependencies.zip htmlunit-2.8-SNAPSHOT/lib htmlunit-2.8-SNAPSHOT/lib/* &> /dev/null]
38
+ %x[unzip -j -d htmlunit htmlunit-2.9-SNAPSHOT-with-dependencies.zip htmlunit-2.9-SNAPSHOT/lib htmlunit-2.9-SNAPSHOT/lib/* &> /dev/null]
39
39
  puts "done"
40
40
 
41
- File.unlink "htmlunit-2.8-SNAPSHOT-with-dependencies.zip"
41
+ File.unlink "htmlunit-2.9-SNAPSHOT-with-dependencies.zip"
42
42
  end
43
43
 
44
44
  $stdout.puts "="*40
@@ -50,7 +50,7 @@ when options[:interactive]
50
50
  require 'akephalos/console'
51
51
  Akephalos::Console.start
52
52
  else
53
- unless socket_file = ARGV[0]
53
+ unless port = ARGV[0]
54
54
  puts parser.help
55
55
  exit
56
56
  end
@@ -58,7 +58,7 @@ else
58
58
  if RUBY_PLATFORM == "java"
59
59
  $:.unshift("vendor", lib, src)
60
60
  require 'akephalos/server'
61
- Akephalos::Server.start!(socket_file)
61
+ Akephalos::Server.start!(port)
62
62
  else
63
63
  require 'rubygems'
64
64
  require 'jruby-jars'
@@ -71,10 +71,11 @@ else
71
71
  "-cp", [JRubyJars.core_jar_path, JRubyJars.stdlib_jar_path].join(":"),
72
72
  "org.jruby.Main"
73
73
  ]
74
- ruby_args = [
74
+ ruby_args = [
75
+ "-Ku",
75
76
  "-I", "vendor:#{lib}:#{src}",
76
77
  "-r", "akephalos/server",
77
- "-e", "Akephalos::Server.start!(#{socket_file.inspect})"
78
+ "-e", "Akephalos::Server.start!(#{port.inspect})"
78
79
  ]
79
80
 
80
81
  # Bundler sets ENV["RUBYOPT"] to automatically load bundler/setup.rb, but
@@ -5,15 +5,15 @@
5
5
  #
6
6
  require 'java' if RUBY_PLATFORM == 'java'
7
7
  require 'pathname'
8
- require 'capybara'
9
8
 
10
9
  module Akephalos
11
10
  BIN_DIR = Pathname(__FILE__).expand_path.dirname.parent + 'bin'
12
11
  end
13
12
 
14
13
  require 'akephalos/client'
14
+ require 'capybara'
15
15
  require 'akephalos/capybara'
16
16
 
17
- if Object.const_defined? :Cucumber
18
- require 'akephalos/cucumber'
17
+ Capybara.register_driver :akephalos do |app|
18
+ Capybara::Driver::Akephalos.new(app)
19
19
  end
@@ -6,8 +6,14 @@
6
6
  # directly or over DRb.
7
7
  class Capybara::Driver::Akephalos < Capybara::Driver::Base
8
8
 
9
- # Akephalos-specific implementation for Capybara's Node class.
10
- class Node < Capybara::Node
9
+ # Akephalos-specific implementation for Capybara's Driver::Node class.
10
+ class Node < Capybara::Driver::Node
11
+
12
+ # @api capybara
13
+ # @return [String] the inner text of the node
14
+ def text
15
+ native.text
16
+ end
11
17
 
12
18
  # @api capybara
13
19
  # @param [String] name attribute name
@@ -16,22 +22,16 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
16
22
  name = name.to_s
17
23
  case name
18
24
  when 'checked'
19
- node.checked?
25
+ native.checked?
20
26
  else
21
- node[name.to_s]
27
+ native[name.to_s]
22
28
  end
23
29
  end
24
30
 
25
31
  # @api capybara
26
- # @return [String] the inner text of the node
27
- def text
28
- node.text
29
- end
30
-
31
- # @api capybara
32
- # @return [String] the form element's value
32
+ # @return [String, Array<String>] the form element's value
33
33
  def value
34
- node.value
34
+ native.value
35
35
  end
36
36
 
37
37
  # Set the form element's value.
@@ -40,7 +40,7 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
40
40
  # @param [String] value the form element's new value
41
41
  def set(value)
42
42
  if tag_name == 'textarea'
43
- node.value = value.to_s
43
+ native.value = value.to_s
44
44
  elsif tag_name == 'input' and type == 'radio'
45
45
  click
46
46
  elsif tag_name == 'input' and type == 'checkbox'
@@ -48,77 +48,93 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
48
48
  click
49
49
  end
50
50
  elsif tag_name == 'input'
51
- node.value = value.to_s
51
+ native.value = value.to_s
52
52
  end
53
53
  end
54
54
 
55
- # Select an option from a select box.
56
- #
57
55
  # @api capybara
58
- # @param [String] option the option to select
59
- def select(option)
60
- result = node.select_option(option)
61
-
62
- if result == nil
63
- options = node.options.map(&:text).join(", ")
64
- raise Capybara::OptionNotFound, "No such option '#{option}' in this select box. Available options: #{options}"
65
- else
66
- result
67
- end
56
+ def select_option
57
+ native.click
68
58
  end
69
59
 
70
60
  # Unselect an option from a select box.
71
61
  #
72
62
  # @api capybara
73
- # @param [String] option the option to unselect
74
- def unselect(option)
75
- unless self[:multiple]
76
- raise Capybara::UnselectNotAllowed, "Cannot unselect option '#{option}' from single select box."
63
+ def unselect_option
64
+ unless select_node.multiple_select?
65
+ raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
77
66
  end
78
67
 
79
- result = node.unselect_option(option)
68
+ native.unselect
69
+ end
80
70
 
81
- if result == nil
82
- options = node.options.map(&:text).join(", ")
83
- raise Capybara::OptionNotFound, "No such option '#{option}' in this select box. Available options: #{options}"
84
- else
85
- result
86
- end
71
+ # Click the element.
72
+ def click
73
+ native.click
87
74
  end
88
75
 
89
- # Trigger an event on the element.
76
+ # Drag the element on top of the target element.
90
77
  #
91
78
  # @api capybara
92
- # @param [String] event the event to trigger
93
- def trigger(event)
94
- node.fire_event(event.to_s)
79
+ # @param [Node] element the target element
80
+ def drag_to(element)
81
+ trigger('mousedown')
82
+ element.trigger('mousemove')
83
+ element.trigger('mouseup')
95
84
  end
96
85
 
97
86
  # @api capybara
98
87
  # @return [String] the element's tag name
99
88
  def tag_name
100
- node.tag_name
89
+ native.tag_name
101
90
  end
102
91
 
103
92
  # @api capybara
104
93
  # @return [true, false] the element's visiblity
105
94
  def visible?
106
- node.visible?
95
+ native.visible?
107
96
  end
108
97
 
109
- # Drag the element on top of the target element.
98
+ # @api capybara
99
+ # @return [true, false] the element's visiblity
100
+ def checked?
101
+ native.checked?
102
+ end
103
+
104
+ # @api capybara
105
+ # @return [true, false] the element's visiblity
106
+ def selected?
107
+ native.selected?
108
+ end
109
+
110
+ # @api capybara
111
+ # @return [String] the XPath to locate the node
112
+ def path
113
+ native.xpath
114
+ end
115
+
116
+ # Trigger an event on the element.
110
117
  #
111
118
  # @api capybara
112
- # @param [Node] element the target element
113
- def drag_to(element)
114
- trigger('mousedown')
115
- element.trigger('mousemove')
116
- element.trigger('mouseup')
119
+ # @param [String] event the event to trigger
120
+ def trigger(event)
121
+ native.fire_event(event.to_s)
117
122
  end
118
123
 
119
- # Click the element.
120
- def click
121
- node.click
124
+ # @api capybara
125
+ # @param [String] selector XPath query
126
+ # @return [Array<Node>] the matched nodes
127
+ def find(selector)
128
+ nodes = []
129
+ native.find(selector).each { |node| nodes << self.class.new(self, node) }
130
+ nodes
131
+ end
132
+
133
+ protected
134
+
135
+ # @return [true, false] whether the node allows multiple-option selection (if the node is a select).
136
+ def multiple_select?
137
+ tag_name == "select" && native.multiple_select?
122
138
  end
123
139
 
124
140
  private
@@ -129,25 +145,52 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
129
145
  # @return [Array<Node>] the matched nodes
130
146
  def all_unfiltered(selector)
131
147
  nodes = []
132
- node.find(selector).each { |node| nodes << Node.new(driver, node) }
148
+ native.find(selector).each { |node| nodes << self.class.new(driver, node) }
133
149
  nodes
134
150
  end
135
151
 
136
152
  # @return [String] the node's type attribute
137
153
  def type
138
- node[:type]
154
+ native[:type]
139
155
  end
140
- end
141
156
 
142
- attr_reader :app, :rack_server
143
-
144
- # @return [Client] an instance of Akephalos::Client
145
- def self.driver
146
- @driver ||= Akephalos::Client.new
157
+ # @return [Node] the select node, if this is an option node
158
+ def select_node
159
+ find('./ancestor::select').first
160
+ end
147
161
  end
148
162
 
149
- def initialize(app)
163
+ attr_reader :app, :rack_server, :options
164
+
165
+ # Creates a new instance of the Akephalos Driver for Capybara. The driver is
166
+ # registered with Capybara by a name, so that it can be chosen when
167
+ # Capybara's javascript_driver is changed. By default, Akephalos is
168
+ # registered like this:
169
+ #
170
+ # Capybara.register_driver :akephalos do |app|
171
+ # Capybara::Akephalos::Driver.new(
172
+ # app,
173
+ # :browser => :firefox_3_6,
174
+ # :validate_scripts => true
175
+ # )
176
+ # end
177
+ #
178
+ # @param app the Rack application to run
179
+ # @param [Hash] options the Akephalos configuration options
180
+ # @option options [Symbol] :browser (:firefox_3_6) the browser
181
+ # compatibility mode to run in. Available options:
182
+ # :firefox_3_6
183
+ # :firefox_3
184
+ # :ie6
185
+ # :ie7
186
+ # :ie8
187
+ #
188
+ # @option options [true, false] :validate_scripts (true) whether to raise
189
+ # exceptions on script errors
190
+ #
191
+ def initialize(app, options = {})
150
192
  @app = app
193
+ @options = options
151
194
  @rack_server = Capybara::Server.new(@app)
152
195
  @rack_server.boot if Capybara.run_server
153
196
  end
@@ -165,8 +208,19 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
165
208
  end
166
209
 
167
210
  # @return [String] the page's modified source
211
+ # page.modified_source will return a string with
212
+ # html entities converted into the unicode equivalent
213
+ # but the string will be marked as ASCII-8BIT
214
+ # which causes conversion issues so we force the encoding
215
+ # to UTF-8 (ruby 1.9 only)
168
216
  def body
169
- page.modified_source
217
+ body_source = page.modified_source
218
+
219
+ if body_source.respond_to?(:force_encoding)
220
+ body_source.force_encoding("UTF-8")
221
+ else
222
+ body_source
223
+ end
170
224
  end
171
225
 
172
226
  # @return [Hash{String => String}] the page's response headers
@@ -190,8 +244,15 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
190
244
  end
191
245
 
192
246
  # Clear all cookie session data.
247
+ # @deprecated This method is deprecated in Capybara's master branch. Use
248
+ # {#reset!} instead.
193
249
  def cleanup!
194
- browser.clear_cookies
250
+ reset!
251
+ end
252
+
253
+ # Clear all cookie session data.
254
+ def reset!
255
+ cookies.clear
195
256
  end
196
257
 
197
258
  # @return [String] the page's current URL
@@ -232,7 +293,26 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
232
293
 
233
294
  # @return the browser
234
295
  def browser
235
- self.class.driver
296
+ @browser ||= Akephalos::Client.new(@options)
297
+ end
298
+
299
+ # @return the session cookies
300
+ def cookies
301
+ browser.cookies
302
+ end
303
+
304
+ # @return [String] the current user agent string
305
+ def user_agent
306
+ browser.user_agent
307
+ end
308
+
309
+ # Set the User-Agent header for this session. If :default is given, the
310
+ # User-Agent header will be reset to the default browser's user agent.
311
+ #
312
+ # @param [:default] user_agent the default user agent
313
+ # @param [String] user_agent the user agent string to use
314
+ def user_agent=(user_agent)
315
+ browser.user_agent = user_agent
236
316
  end
237
317
 
238
318
  # Disable waiting in Capybara, since waiting is handled directly by
@@ -252,3 +332,7 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
252
332
  end
253
333
 
254
334
  end
335
+
336
+ Capybara.register_driver :akephalos do |app|
337
+ Capybara::Driver::Akephalos.new(app)
338
+ end
@@ -10,6 +10,7 @@ else
10
10
  require 'akephalos/page'
11
11
  require 'akephalos/node'
12
12
 
13
+ require 'akephalos/client/cookies'
13
14
  require 'akephalos/client/filter'
14
15
 
15
16
  module Akephalos
@@ -18,34 +19,55 @@ else
18
19
  # point for all interaction with the browser, exposing its current page and
19
20
  # allowing navigation.
20
21
  class Client
21
- java_import 'com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController'
22
- java_import 'com.gargoylesoftware.htmlunit.SilentCssErrorHandler'
23
22
 
23
+ # @return [Akephalos::Page] the current page
24
24
  attr_reader :page
25
25
 
26
- def initialize
26
+ # @return [HtmlUnit::BrowserVersion] the configured browser version
27
+ attr_reader :browser_version
28
+
29
+ # @return [true/false] whether to raise errors on javascript failures
30
+ attr_reader :validate_scripts
31
+
32
+ # The default configuration options for a new Client.
33
+ DEFAULT_OPTIONS = {
34
+ :browser => :firefox_3_6,
35
+ :validate_scripts => true
36
+ }
37
+
38
+ # Map of browser version symbols to their HtmlUnit::BrowserVersion
39
+ # instances.
40
+ BROWSER_VERSIONS = {
41
+ :ie6 => HtmlUnit::BrowserVersion::INTERNET_EXPLORER_6,
42
+ :ie7 => HtmlUnit::BrowserVersion::INTERNET_EXPLORER_7,
43
+ :ie8 => HtmlUnit::BrowserVersion::INTERNET_EXPLORER_8,
44
+ :firefox_3 => HtmlUnit::BrowserVersion::FIREFOX_3,
45
+ :firefox_3_6 => HtmlUnit::BrowserVersion::FIREFOX_3_6
46
+ }
47
+
48
+ # @param [Hash] options the configuration options for this client
49
+ #
50
+ # @option options [Symbol] :browser (:firefox_3_6) the browser version (
51
+ # see BROWSER_VERSIONS)
52
+ #
53
+ # @option options [true, false] :validate_scripts (true) whether to raise
54
+ # errors on javascript errors
55
+ def initialize(options = {})
56
+ process_options!(options)
57
+
27
58
  @_client = java.util.concurrent.FutureTask.new do
28
- client = WebClient.new
59
+ client = HtmlUnit::WebClient.new(browser_version)
29
60
 
30
61
  Filter.new(client)
31
- client.setAjaxController(NicelyResynchronizingAjaxController.new)
32
- client.setCssErrorHandler(SilentCssErrorHandler.new)
33
-
62
+ client.setThrowExceptionOnFailingStatusCode(false)
63
+ client.setAjaxController(HtmlUnit::NicelyResynchronizingAjaxController.new)
64
+ client.setCssErrorHandler(HtmlUnit::SilentCssErrorHandler.new)
65
+ client.setThrowExceptionOnScriptError(validate_scripts)
34
66
  client
35
67
  end
36
68
  Thread.new { @_client.run }
37
69
  end
38
70
 
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
45
- def configuration=(config)
46
- Akephalos.configuration = config
47
- end
48
-
49
71
  # Visit the requested URL and return the page.
50
72
  #
51
73
  # @param [String] url the URL to load
@@ -55,9 +77,29 @@ else
55
77
  page
56
78
  end
57
79
 
58
- # Clear all cookies for this browser session.
59
- def clear_cookies
60
- client.getCookieManager.clearCookies
80
+ # @return [Cookies] the cookies for this session
81
+ def cookies
82
+ @cookies ||= Cookies.new(client.getCookieManager)
83
+ end
84
+
85
+ # @return [String] the current user agent string
86
+ def user_agent
87
+ @user_agent || client.getBrowserVersion.getUserAgent
88
+ end
89
+
90
+ # Set the User-Agent header for this session. If :default is given, the
91
+ # User-Agent header will be reset to the default browser's user agent.
92
+ #
93
+ # @param [:default] user_agent the default user agent
94
+ # @param [String] user_agent the user agent string to use
95
+ def user_agent=(user_agent)
96
+ if user_agent == :default
97
+ @user_agent = nil
98
+ client.removeRequestHeader("User-Agent")
99
+ else
100
+ @user_agent = user_agent
101
+ client.addRequestHeader("User-Agent", user_agent)
102
+ end
61
103
  end
62
104
 
63
105
  # @return [Page] the current page
@@ -77,6 +119,23 @@ else
77
119
  @page
78
120
  end
79
121
 
122
+ # @return [true, false] whether javascript errors will raise exceptions
123
+ def validate_scripts?
124
+ !!validate_scripts
125
+ end
126
+
127
+ # Merges the DEFAULT_OPTIONS with those provided to initialize the Client
128
+ # state, namely, its browser version and whether it should
129
+ # validate scripts.
130
+ #
131
+ # @param [Hash] options the options to process
132
+ def process_options!(options)
133
+ options = DEFAULT_OPTIONS.merge(options)
134
+
135
+ @browser_version = BROWSER_VERSIONS.fetch(options.delete(:browser))
136
+ @validate_scripts = options.delete(:validate_scripts)
137
+ end
138
+
80
139
  private
81
140
 
82
141
  # Call the future set up in #initialize and return the WebCLient
@@ -0,0 +1,73 @@
1
+ module Akephalos
2
+ class Client
3
+ # Interface for working with HtmlUnit's CookieManager, providing a basic
4
+ # API for manipulating the cookies in a session.
5
+ class Cookies
6
+ include Enumerable
7
+
8
+ # @param [HtmlUnit::CookieManager] cookie manager
9
+ def initialize(cookie_manager)
10
+ @cookie_manager = cookie_manager
11
+ end
12
+
13
+ # @param [name] the cookie name
14
+ # @return [Cookie] the cookie with the given name
15
+ # @return [nil] when no cookie is found
16
+ def [](name)
17
+ cookie = @cookie_manager.getCookie(name)
18
+ Cookie.new(cookie) if cookie
19
+ end
20
+
21
+ # Clears all cookies for this session.
22
+ def clear
23
+ @cookie_manager.clearCookies
24
+ end
25
+
26
+ # Iterator for all cookies in the current session.
27
+ def each
28
+ @cookie_manager.getCookies.each do |cookie|
29
+ yield Cookie.new(cookie)
30
+ end
31
+ end
32
+
33
+ # Remove the cookie from the session.
34
+ #
35
+ # @param [Cookie] the cookie to remove
36
+ def delete(cookie)
37
+ @cookie_manager.removeCookie(cookie.native)
38
+ end
39
+
40
+ # @return [true, false] whether there are any cookies
41
+ def empty?
42
+ !any?
43
+ end
44
+
45
+ class Cookie
46
+
47
+ attr_reader :domain, :expires, :name, :path, :value
48
+
49
+ # @param [HtmlUnit::Cookie] the cookie
50
+ def initialize(cookie)
51
+ @_cookie = cookie
52
+ @domain = cookie.getDomain
53
+ @expires = cookie.getExpires
54
+ @name = cookie.getName
55
+ @path = cookie.getPath
56
+ @value = cookie.getValue
57
+ @secure = cookie.isSecure
58
+ end
59
+
60
+ def secure?
61
+ !!@secure
62
+ end
63
+
64
+ # @return [HtmlUnit::Cookie] the native cookie object
65
+ # @api private
66
+ def native
67
+ @_cookie
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -9,11 +9,7 @@ module Akephalos
9
9
  # found. If no filters are defined, or no filters match the request, then
10
10
  # the response will bubble up to HtmlUnit for the normal request/response
11
11
  # cycle.
12
- class Filter < WebConnectionWrapper
13
- java_import 'com.gargoylesoftware.htmlunit.util.NameValuePair'
14
- java_import 'com.gargoylesoftware.htmlunit.WebResponseData'
15
- java_import 'com.gargoylesoftware.htmlunit.WebResponseImpl'
16
-
12
+ class Filter < HtmlUnit::Util::WebConnectionWrapper
17
13
  # Filters an outgoing request, and if a match is found, returns the mock
18
14
  # response.
19
15
  #
@@ -23,14 +19,16 @@ module Akephalos
23
19
  def filter(request)
24
20
  if filter = find_filter(request)
25
21
  start_time = Time.now
26
- headers = filter[:headers].map { |name, value| NameValuePair.new(name.to_s, value.to_s) }
27
- response = WebResponseData.new(
22
+ headers = filter[:headers].map do |name, value|
23
+ HtmlUnit::Util::NameValuePair.new(name.to_s, value.to_s)
24
+ end
25
+ response = HtmlUnit::WebResponseData.new(
28
26
  filter[:body].to_s.to_java_bytes,
29
27
  filter[:status],
30
28
  HTTP_STATUS_CODES.fetch(filter[:status], "Unknown"),
31
29
  headers
32
30
  )
33
- WebResponseImpl.new(response, request, Time.now - start_time)
31
+ HtmlUnit::WebResponseImpl.new(response, request, Time.now - start_time)
34
32
  end
35
33
  end
36
34
 
@@ -1,7 +1,7 @@
1
1
  # Begin a new Capybara session, by default connecting to localhost on port
2
2
  # 3000.
3
3
  def session
4
- Capybara.app_host = "http://localhost:3000"
4
+ Capybara.app_host ||= "http://localhost:3000"
5
5
  @session ||= Capybara::Session.new(:Akephalos)
6
6
  end
7
7
  alias page session
@@ -13,15 +13,23 @@ java.lang.System.setProperty("org.apache.commons.logging.Log", "org.apache.commo
13
13
  java.lang.System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "fatal")
14
14
  java.lang.System.setProperty("org.apache.commons.logging.simplelog.showdatetime", "true")
15
15
 
16
- java_import "com.gargoylesoftware.htmlunit.WebClient"
17
- java_import "com.gargoylesoftware.htmlunit.util.WebConnectionWrapper"
18
- java_import 'com.gargoylesoftware.htmlunit.HttpMethod'
19
-
20
-
21
- # Disable history tracking
22
- com.gargoylesoftware.htmlunit.History.field_reader :ignoreNewPages_
23
-
24
- # Run in Firefox compatibility mode
25
- com.gargoylesoftware.htmlunit.BrowserVersion.setDefault(
26
- com.gargoylesoftware.htmlunit.BrowserVersion::FIREFOX_3
27
- )
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.NicelyResynchronizingAjaxController"
22
+ java_import "com.gargoylesoftware.htmlunit.SilentCssErrorHandler"
23
+ java_import "com.gargoylesoftware.htmlunit.WebClient"
24
+ java_import "com.gargoylesoftware.htmlunit.WebResponseData"
25
+ java_import "com.gargoylesoftware.htmlunit.WebResponseImpl"
26
+
27
+ # Container module for com.gargoylesoftware.htmlunit.util namespace.
28
+ module Util
29
+ java_import "com.gargoylesoftware.htmlunit.util.NameValuePair"
30
+ java_import "com.gargoylesoftware.htmlunit.util.WebConnectionWrapper"
31
+ end
32
+
33
+ # Disable history tracking
34
+ History.field_reader :ignoreNewPages_
35
+ end
@@ -1,28 +1,30 @@
1
- # Reopen com.gargoylesoftware.htmlunit.HttpMethod to add convenience methods.
2
- class HttpMethod
1
+ module HtmlUnit
2
+ # Reopen HtmlUnit's HttpMethod class to add convenience methods.
3
+ class HttpMethod
3
4
 
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]
11
- def ===(other)
12
- case other
13
- when HttpMethod
14
- super
15
- when :any
16
- true
17
- when :get
18
- self == self.class::GET
19
- when :post
20
- self == self.class::POST
21
- when :put
22
- self == self.class::PUT
23
- when :delete
24
- self == self.class::DELETE
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
25
27
  end
26
- end
27
28
 
29
+ end
28
30
  end
@@ -11,7 +11,11 @@ module Akephalos
11
11
 
12
12
  # @return [true, false] whether the element is checked
13
13
  def checked?
14
- @_node.isChecked
14
+ if @_node.respond_to?(:isChecked)
15
+ @_node.isChecked
16
+ else
17
+ !! self[:checked]
18
+ end
15
19
  end
16
20
 
17
21
  # @return [String] inner text of the node
@@ -37,11 +41,13 @@ module Akephalos
37
41
  case tag_name
38
42
  when "select"
39
43
  if self[:multiple]
40
- @_node.selected_options.map { |option| option.text }
44
+ selected_options.map { |option| option.value }
41
45
  else
42
46
  selected_option = @_node.selected_options.first
43
- selected_option ? selected_option.text : nil
47
+ selected_option ? Node.new(selected_option).value : nil
44
48
  end
49
+ when "option"
50
+ self[:value] || text
45
51
  when "textarea"
46
52
  @_node.getText
47
53
  else
@@ -55,28 +61,43 @@ module Akephalos
55
61
  def value=(value)
56
62
  case tag_name
57
63
  when "textarea"
58
- @_node.setText(value)
64
+ @_node.setText("")
65
+ type(value)
59
66
  when "input"
60
- @_node.setValueAttribute(value)
67
+ if file_input?
68
+ @_node.setValueAttribute(value)
69
+ else
70
+ @_node.setValueAttribute("")
71
+ type(value)
72
+ end
61
73
  end
62
74
  end
63
75
 
64
- # Select an option from a select box by its value.
76
+ # Types each character into a text or input field.
65
77
  #
66
- # @return [true, false] whether the selection was successful
67
- def select_option(option)
68
- opt = @_node.getOptions.detect { |o| o.asText == option }
78
+ # @param [String] value the string to type
79
+ def type(value)
80
+ value.each_char do |c|
81
+ @_node.type(c)
82
+ end
83
+ end
84
+
85
+ # @return [true, false] whether the node allows multiple-option selection (if the node is a select).
86
+ def multiple_select?
87
+ !self[:multiple].nil?
88
+ end
69
89
 
70
- opt && opt.setSelected(true)
90
+ # @return [true, false] whether the node is a file input
91
+ def file_input?
92
+ tag_name == "input" && @_node.getAttribute("type") == "file"
71
93
  end
72
94
 
73
- # Unselect an option from a select box by its value.
95
+
96
+ # Unselect an option.
74
97
  #
75
98
  # @return [true, false] whether the unselection was successful
76
- def unselect_option(option)
77
- opt = @_node.getOptions.detect { |o| o.asText == option }
78
-
79
- opt && opt.setSelected(false)
99
+ def unselect
100
+ @_node.setSelected(false)
80
101
  end
81
102
 
82
103
  # Return the option elements for a select box.
@@ -114,6 +135,16 @@ module Akephalos
114
135
  @_node.isDisplayed
115
136
  end
116
137
 
138
+ # @return [true, false] whether the node is selected to the user accounting
139
+ # for CSS.
140
+ def selected?
141
+ if @_node.respond_to?(:isSelected)
142
+ @_node.isSelected
143
+ else
144
+ !! self[:selected]
145
+ end
146
+ end
147
+
117
148
  # Click the node and then wait for any triggered JavaScript callbacks to
118
149
  # fire.
119
150
  def click
@@ -131,6 +162,11 @@ module Akephalos
131
162
  @nodes << nodes
132
163
  nodes
133
164
  end
165
+
166
+ # @return [String] the XPath expression for this node
167
+ def xpath
168
+ @_node.getCanonicalXPath
169
+ end
134
170
  end
135
171
 
136
172
  end
@@ -1,3 +1,4 @@
1
+ require 'socket'
1
2
  require 'drb/drb'
2
3
 
3
4
  # We need to define our own NativeException class for the cases when a native
@@ -14,44 +15,78 @@ module Akephalos
14
15
  # client.visit "http://www.oinopa.com"
15
16
  # client.page.source # => "<!DOCTYPE html PUBLIC..."
16
17
  class RemoteClient
17
- @socket_file = "/tmp/akephalos.#{Process.pid}.sock"
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
18
23
 
19
- # Start a remote akephalos server and return the remote Akephalos::Client
20
- # instance.
24
+ # Starts a remove JRuby DRb server unless already running and returns an
25
+ # instance of Akephalos::ClientManager.
21
26
  #
22
- # @return [DRbObject] the remote client instance
23
- def self.new
24
- start!
27
+ # @return [DRbObject] an instance of Akephalos::ClientManager
28
+ def self.manager
29
+ return @manager if defined?(@manager)
30
+
31
+ server_port = start!
32
+
25
33
  DRb.start_service
26
- client = DRbObject.new_with_uri("drbunix://#{@socket_file}")
34
+ manager = DRbObject.new_with_uri("druby://127.0.0.1:#{server_port}")
35
+
27
36
  # We want to share our local configuration with the remote server
28
37
  # process, so we share an undumped version of our configuration. This
29
38
  # lets us continue to make changes locally and have them reflected in the
30
39
  # remote process.
31
- client.configuration = Akephalos.configuration.extend(DRbUndumped)
32
- client
40
+ manager.configuration = Akephalos.configuration.extend(DRbUndumped)
41
+
42
+ @manager = manager
33
43
  end
34
44
 
35
45
  # Start a remote server process and return when it is available for use.
36
46
  def self.start!
37
- remote_client = fork do
38
- exec("#{Akephalos::BIN_DIR + 'akephalos'} #{@socket_file}")
39
- end
47
+ port = find_available_port
48
+
49
+ remote_client = IO.popen("#{Akephalos::BIN_DIR + 'akephalos'} #{port}")
40
50
 
41
51
  # Set up a monitor thread to detect if the forked server exits
42
52
  # prematurely.
43
- server_monitor = Thread.new { Thread.current[:exited] = Process.wait }
53
+ server_monitor = Thread.new { Thread.current[:exited] = Process.wait(remote_client.pid) }
44
54
 
45
55
  # Wait for the server to be accessible on the socket we specified.
46
- until File.exists?(@socket_file)
56
+ until responsive?(port)
47
57
  exit!(1) if server_monitor[:exited]
48
- sleep 1
58
+ sleep 0.5
49
59
  end
50
60
  server_monitor.kill
51
61
 
52
62
  # Ensure that the remote server shuts down gracefully when we are
53
63
  # finished.
54
- at_exit { Process.kill(:INT, remote_client); File.unlink(@socket_file) }
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
55
90
  end
56
91
  end
57
92
  end
@@ -14,21 +14,66 @@ class NameError::Message
14
14
  end
15
15
  end
16
16
 
17
- [Akephalos::Page, Akephalos::Node].each { |klass| klass.send(:include, DRbUndumped) }
17
+ [
18
+ Akephalos::Page,
19
+ Akephalos::Node,
20
+ Akephalos::Client::Cookies,
21
+ Akephalos::Client::Cookies::Cookie
22
+ ].each { |klass| klass.send(:include, DRbUndumped) }
18
23
 
19
24
  module Akephalos
20
25
 
26
+ # The ClientManager is shared over DRb with the remote process, and
27
+ # facilitates communication between the processes.
28
+ #
29
+ # @api private
30
+ class ClientManager
31
+ include DRbUndumped
32
+
33
+ # @return [Akephalos::Client] a new client instance
34
+ def self.new_client(options = {})
35
+ # Store the client to ensure it isn't prematurely garbage collected.
36
+ @client = Client.new(options)
37
+ end
38
+
39
+ # Set the global configuration settings for Akephalos.
40
+ #
41
+ # @param [Hash] config the configuration settings
42
+ # @return [Hash] the configuration
43
+ def self.configuration=(config)
44
+ Akephalos.configuration = config
45
+ end
46
+
47
+ end
48
+
21
49
  # Akephalos::Server is used by `akephalos --server` to start a DRb server
22
- # serving an instance of Akephalos::Client.
50
+ # serving Akephalos::ClientManager.
23
51
  class Server
24
- # Start DRb service for an Akephalos::Client.
52
+
53
+ # Start DRb service for Akephalos::ClientManager.
25
54
  #
26
- # @param [String] socket_file path to socket file to start
27
- def self.start!(socket_file)
28
- client = Client.new
29
- DRb.start_service("drbunix://#{socket_file}", client)
55
+ # @param [String] port attach server to
56
+ def self.start!(port)
57
+ abort_on_parent_exit!
58
+ DRb.start_service("druby://127.0.0.1:#{port}", ClientManager)
30
59
  DRb.thread.join
31
60
  end
61
+
62
+ private
63
+
64
+ # Exit if STDIN is no longer readable, which corresponds to the process
65
+ # which started the server exiting prematurely.
66
+ #
67
+ # @api private
68
+ def self.abort_on_parent_exit!
69
+ Thread.new do
70
+ begin
71
+ STDIN.read
72
+ rescue IOError
73
+ exit
74
+ end
75
+ end
76
+ end
32
77
  end
33
78
 
34
79
  end
@@ -1,3 +1,3 @@
1
1
  module Akephalos #:nodoc
2
- VERSION = "0.2.4"
2
+ VERSION = "0.2.5"
3
3
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 2
8
- - 4
9
- version: 0.2.4
8
+ - 5
9
+ version: 0.2.5
10
10
  platform: ruby
11
11
  authors:
12
12
  - Bernerd Schaefer
@@ -14,27 +14,29 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-09-14 00:00:00 -05:00
17
+ date: 2011-02-04 00:00:00 +01:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: capybara
22
22
  prerelease: false
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
24
25
  requirements:
25
26
  - - ~>
26
27
  - !ruby/object:Gem::Version
27
28
  segments:
28
29
  - 0
29
- - 3
30
- - 8
31
- version: 0.3.8
30
+ - 4
31
+ - 0
32
+ version: 0.4.0
32
33
  type: :runtime
33
34
  version_requirements: *id001
34
35
  - !ruby/object:Gem::Dependency
35
36
  name: jruby-jars
36
37
  prerelease: false
37
38
  requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
38
40
  requirements:
39
41
  - - ">="
40
42
  - !ruby/object:Gem::Version
@@ -47,6 +49,7 @@ dependencies:
47
49
  name: sinatra
48
50
  prerelease: false
49
51
  requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
50
53
  requirements:
51
54
  - - ">="
52
55
  - !ruby/object:Gem::Version
@@ -59,14 +62,15 @@ dependencies:
59
62
  name: rspec
60
63
  prerelease: false
61
64
  requirement: &id004 !ruby/object:Gem::Requirement
65
+ none: false
62
66
  requirements:
63
- - - "="
67
+ - - ">="
64
68
  - !ruby/object:Gem::Version
65
69
  segments:
66
- - 1
70
+ - 2
67
71
  - 3
68
72
  - 0
69
- version: 1.3.0
73
+ version: 2.3.0
70
74
  type: :development
71
75
  version_requirements: *id004
72
76
  description: Headless Browser for Integration Testing with Capybara
@@ -79,6 +83,7 @@ extra_rdoc_files: []
79
83
 
80
84
  files:
81
85
  - lib/akephalos/capybara.rb
86
+ - lib/akephalos/client/cookies.rb
82
87
  - lib/akephalos/client/filter.rb
83
88
  - lib/akephalos/client.rb
84
89
  - lib/akephalos/configuration.rb
@@ -112,6 +117,7 @@ files:
112
117
  - src/htmlunit/xml-apis-1.3.04.jar
113
118
  - README.md
114
119
  - MIT_LICENSE
120
+ - bin/akephalos
115
121
  has_rdoc: true
116
122
  homepage: http://bernerdschaefer.github.com/akephalos
117
123
  licenses: []
@@ -123,6 +129,7 @@ require_paths:
123
129
  - lib
124
130
  - src
125
131
  required_ruby_version: !ruby/object:Gem::Requirement
132
+ none: false
126
133
  requirements:
127
134
  - - ">="
128
135
  - !ruby/object:Gem::Version
@@ -130,6 +137,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
130
137
  - 0
131
138
  version: "0"
132
139
  required_rubygems_version: !ruby/object:Gem::Requirement
140
+ none: false
133
141
  requirements:
134
142
  - - ">="
135
143
  - !ruby/object:Gem::Version
@@ -141,7 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
149
  requirements: []
142
150
 
143
151
  rubyforge_project: akephalos
144
- rubygems_version: 1.3.6
152
+ rubygems_version: 1.3.7
145
153
  signing_key:
146
154
  specification_version: 3
147
155
  summary: Headless Browser for Integration Testing with Capybara