steam 0.0.2

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.
Files changed (62) hide show
  1. data/MIT-LICENSE +21 -0
  2. data/README.textile +91 -0
  3. data/Rakefile +23 -0
  4. data/TODO +7 -0
  5. data/lib/core_ext/ruby/array/flatten_once.rb +9 -0
  6. data/lib/core_ext/ruby/hash/except.rb +11 -0
  7. data/lib/core_ext/ruby/hash/slice.rb +14 -0
  8. data/lib/core_ext/ruby/kernel/silence_warnings.rb +8 -0
  9. data/lib/core_ext/ruby/process/daemon.rb +23 -0
  10. data/lib/core_ext/ruby/string/camelize.rb +5 -0
  11. data/lib/core_ext/ruby/string/underscore.rb +5 -0
  12. data/lib/steam.rb +43 -0
  13. data/lib/steam/browser.rb +24 -0
  14. data/lib/steam/browser/html_unit.rb +87 -0
  15. data/lib/steam/browser/html_unit/actions.rb +176 -0
  16. data/lib/steam/browser/html_unit/client.rb +74 -0
  17. data/lib/steam/browser/html_unit/connection.rb +79 -0
  18. data/lib/steam/browser/html_unit/drb.rb +45 -0
  19. data/lib/steam/browser/html_unit/matchers.rb +57 -0
  20. data/lib/steam/browser/html_unit/page.rb +51 -0
  21. data/lib/steam/browser/html_unit/web_response.rb +116 -0
  22. data/lib/steam/connection.rb +9 -0
  23. data/lib/steam/connection/mock.rb +57 -0
  24. data/lib/steam/connection/net_http.rb +42 -0
  25. data/lib/steam/connection/open_uri.rb +24 -0
  26. data/lib/steam/connection/rails.rb +20 -0
  27. data/lib/steam/connection/static.rb +33 -0
  28. data/lib/steam/java.rb +74 -0
  29. data/lib/steam/process.rb +53 -0
  30. data/lib/steam/request.rb +49 -0
  31. data/lib/steam/response.rb +13 -0
  32. data/lib/steam/session.rb +30 -0
  33. data/lib/steam/session/rails.rb +33 -0
  34. data/lib/steam/version.rb +3 -0
  35. data/test/all.rb +3 -0
  36. data/test/browser/html_unit/actions_test.rb +183 -0
  37. data/test/browser/html_unit/javascript_test.rb +60 -0
  38. data/test/browser/html_unit/rails_actions_test.rb +151 -0
  39. data/test/browser/html_unit_test.rb +97 -0
  40. data/test/connection/cascade_test.rb +42 -0
  41. data/test/connection/mock_test.rb +58 -0
  42. data/test/connection/rails_test.rb +16 -0
  43. data/test/connection/static_test.rb +14 -0
  44. data/test/fixtures/html_fakes.rb +191 -0
  45. data/test/java_test.rb +29 -0
  46. data/test/playground/connection.rb +19 -0
  47. data/test/playground/dragdrop_behavior.rb +60 -0
  48. data/test/playground/drb.rb +55 -0
  49. data/test/playground/java_signature.rb +22 -0
  50. data/test/playground/nokogiri.rb +15 -0
  51. data/test/playground/put_headers.rb +83 -0
  52. data/test/playground/rack.rb +11 -0
  53. data/test/playground/rjb_bind.rb +42 -0
  54. data/test/playground/stack_level_problem.rb +129 -0
  55. data/test/playground/thread_problem.rb +57 -0
  56. data/test/playground/web_response_data.rb +21 -0
  57. data/test/playground/webrat.rb +48 -0
  58. data/test/playground/xhr_accept_headers.rb +61 -0
  59. data/test/process_test.rb +55 -0
  60. data/test/session_test.rb +15 -0
  61. data/test/test_helper.rb +56 -0
  62. metadata +135 -0
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2009 Sven Fuchs <svenfuchs@artweb-design.de>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,91 @@
1
+ <a name="readme"></a>
2
+
3
+ h1. Steam
4
+
5
+ Steam is a headless integration testing tool driving "HtmlUnit":http://htmlunit.sourceforge.net to enable testing JavaScript-driven web sites. In that it is similar to "Culerity":http://github.com/langalex/culerity which drives "Celerity":http://github.com/jarib/celerity (which also drives "HtmlUnit":http://htmlunit.sourceforge.net). See below for a "comparsion":#comparsion.
6
+
7
+ <a name="installation"></a>
8
+
9
+ h2. Installation
10
+
11
+ Steam currently has the following dependencies:
12
+
13
+ * Working Java Runtime
14
+ * HtmlUnit (jar files)
15
+ * RJB and Locator gems
16
+
17
+ Installing Steam as as a gem will automatically install the required RJB and Locator gems:
18
+
19
+ pre. $ gem install steam
20
+
21
+ To install HtmlUnit you can download it from "Sourceforge":http://sourceforge.net/projects/htmlunit/files.
22
+
23
+ You then need to add HtmlUnit to your Java classpath. The following ways should both work:
24
+
25
+ <pre># anywhere during startup, e.g. in features/support/env.rb
26
+ ENV['CLASSPATH'] = Dir["path/to/your/htmlunit/*.jar"].join(':')
27
+
28
+ # after steam has been added to the load path, e.g. in features/support/env.rb
29
+ Steam.config[:html_unit][:java_path] = 'path/to/your/htmlunit'</pre>
30
+
31
+ If you're on Mac OS X then you also need to export the JAVA_HOME variable for RJB. See here for two solutions: "Installing RJB on Mac OS X":http://www.elctech.com/articles/sudo-java_home-and-mac-os-x. The visudo way worked for us. Don't forget to add yourself to the sudoers file, though.
32
+
33
+
34
+ <a name="configuration"></a>
35
+
36
+ h2. Configuration
37
+
38
+ You should not need to configure anything. If you do need though have a look at "Steam.config":http://github.com/svenfuchs/steam/blob/master/lib/steam.rb
39
+
40
+ E.g. in order to tweak the Java load params you can
41
+
42
+ pre. Steam.config[:java_load_params] = "-Xmx2048M"
43
+
44
+
45
+ <a name="usage"></a>
46
+
47
+ h2. Usage
48
+
49
+ You can use Steam by itself as well as with Cucumber. You can find an example for a Cucumber setup in examples/cucumber/env.rb.
50
+
51
+ Steam is widely compatible with Webrat - many actions are implemented and take the same or very similar parameters as their Webrat equivalent. You even might be able to use the default webrat_steps.rb that ships with Cucumber, *but his file is meant as an example and might be out of date.*
52
+
53
+
54
+ <a name="demo"></a>
55
+
56
+ h2. Demo
57
+
58
+ You can find a demo application here: "http://github.com/clemens/steam-demo":http://github.com/clemens/steam-demo
59
+
60
+
61
+ <a name="comparsion"></a>
62
+
63
+ h2. Comparsion to others
64
+
65
+ Steam's advantages over Culerity/Celerity:
66
+
67
+ * runs in Ruby MRI and does not require an entire JRuby environment.
68
+ * Steam can have the HtmlUnit browser running in the same stack as your tests, thus making the whole thing less complex and hard to debug
69
+ * Steam does not build on Celerity which is a quite heavy-weight Ruby wrapper around HtmlUnit adding a lot of unnecessary code
70
+ * Steam uses "Locator":http://github.com/svenfuchs/locator
71
+
72
+ Culerity/Celerity's advantages over Steam:
73
+
74
+ * RJB can't resolve the mismatch of Ruby vs Java threads which makes fancy setups impossible to solve
75
+ * Celerity implements a *lot* of stuff, maybe it contains something you need (e.g. maybe you want to test pop-down windows opening in the background?)
76
+ * Steam still is in its infancy
77
+
78
+
79
+ <a name="acknowledgements"></a>
80
+
81
+ h2. Acknowledgements
82
+
83
+ Kudos to "Alexander Lang":http://github.com/langalex for writing "Culerity":http://github.com/langalex/culerity which pioneered full-stack AJAX-enabled integration testing in Rails.
84
+
85
+
86
+ <a name="developers"></a>
87
+
88
+ h2. Developers
89
+
90
+ * "Sven Fuchs":http://github.com/svenfuchs
91
+ * "Clemens Kofler"::http://github.com/clemens
@@ -0,0 +1,23 @@
1
+ $: << File.expand_path('../lib', __FILE__)
2
+
3
+ require 'rake/testtask'
4
+ require 'steam/version'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |s|
9
+ s.name = 'steam'
10
+ s.version = Steam::VERSION
11
+ s.summary = 'Headless integration testing w/ HtmlUnit: enables testing JavaScript-driven web sites '
12
+ s.email = 'svenfuchs@artweb-design.de'
13
+ s.homepage = 'http://github.com/svenfuchs/steam'
14
+ s.description = 'Steam is a headless integration testing tool driving HtmlUnit to enable testing JavaScript-driven web sites.'
15
+ s.authors = ['Sven Fuchs', 'Clemens Kofler']
16
+ s.files = FileList['[A-Z]*', 'lib/steam.rb', 'lib/{core_ext,steam}/**/*']
17
+
18
+ s.add_dependency 'rjb', '>= 1.2.0'
19
+ s.add_dependency 'locator', '>= 0.0.4'
20
+ end
21
+ rescue LoadError
22
+ puts 'Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com'
23
+ end
data/TODO ADDED
@@ -0,0 +1,7 @@
1
+ http://htmlunit.sourceforge.net/logging.html
2
+
3
+ http://htmlunit.sourceforge.net/faq.html#MemoryLeak
4
+ HtmlUnit appears to be leaking memory; what's the deal?
5
+ Make sure (a) that you are using the latest version of HtmlUnit, and (b) that you are calling WebClient.closeAllWindows() when you are finished with your WebClient instance.
6
+
7
+
@@ -0,0 +1,9 @@
1
+ class Array
2
+ def flatten_once
3
+ result = []
4
+ for element in self # a little faster than each
5
+ result.push(*element)
6
+ end
7
+ result
8
+ end
9
+ end unless Array.method_defined?(:flatten_once)
@@ -0,0 +1,11 @@
1
+ class Hash
2
+ def except!(*keys)
3
+ keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
4
+ keys.each { |key| delete(key) }
5
+ self
6
+ end
7
+
8
+ def except(*keys)
9
+ dup.except!(*keys)
10
+ end
11
+ end unless Hash.method_defined?(:slice)
@@ -0,0 +1,14 @@
1
+ class Hash
2
+ def slice!(*keys)
3
+ omit = slice(*self.keys - keys)
4
+ hash = slice(*keys)
5
+ replace(hash)
6
+ omit
7
+ end
8
+
9
+ def slice(*keys)
10
+ hash = self.class.new
11
+ keys.each { |k| hash[k] = self[k] if has_key?(k) }
12
+ hash
13
+ end
14
+ end unless Hash.method_defined?(:slice)
@@ -0,0 +1,8 @@
1
+ module Kernel
2
+ def silence_warnings
3
+ old_verbose, $VERBOSE = $VERBOSE, nil
4
+ yield
5
+ ensure
6
+ $VERBOSE = old_verbose
7
+ end
8
+ end unless Kernel.method_defined?(:silence_warnings)
@@ -0,0 +1,23 @@
1
+ module Process
2
+ def self.daemon(nochdir = nil, noclose = nil)
3
+ exit if fork # Parent exits, child continues.
4
+ Process.setsid # Become session leader.
5
+ exit if fork # Zap session leader. See [1].
6
+
7
+ unless nochdir
8
+ Dir.chdir "/" # Release old working directory.
9
+ end
10
+
11
+ File.umask 0000 # Ensure sensible umask. Adjust as needed.
12
+
13
+ unless noclose
14
+ STDIN.reopen "/dev/null" # Free file descriptors and
15
+ STDOUT.reopen "/dev/null", "a" # point them somewhere sensible.
16
+ STDERR.reopen '/dev/null', 'a'
17
+ end
18
+
19
+ trap("TERM") { exit }
20
+
21
+ return 0
22
+ end unless respond_to?(:daemon)
23
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def camelize
3
+ split(/[^a-z0-9]/i).map { |w| w.capitalize }.join
4
+ end
5
+ end unless String.method_defined?(:camelize)
@@ -0,0 +1,5 @@
1
+ class String
2
+ def underscore
3
+ self[0, 1].downcase + self[1..-1].gsub(/[A-Z]/) { |c| "_#{c.downcase}" }
4
+ end
5
+ end unless String.method_defined?(:underscore)
@@ -0,0 +1,43 @@
1
+ require 'rack'
2
+
3
+ module Steam
4
+ autoload :Browser, 'steam/browser'
5
+ autoload :Connection, 'steam/connection'
6
+ autoload :Java, 'steam/java'
7
+ autoload :Process, 'steam/process'
8
+ autoload :Request, 'steam/request'
9
+ autoload :Response, 'steam/response'
10
+ autoload :Session, 'steam/session'
11
+
12
+ class << self
13
+ def config
14
+ @@config ||= {
15
+ :request_url => 'http://localhost',
16
+ :server_name => 'localhost',
17
+ :server_port => '3000',
18
+ :url_scheme => 'http',
19
+ :charset => 'utf-8',
20
+ :java_load_params => '-Xmx1024M',
21
+ :drb_uri => 'druby://127.0.0.1:9000',
22
+ :html_unit => {
23
+ :java_path => File.expand_path("../../vendor/htmlunit-2.6/", __FILE__),
24
+ :browser_version => :FIREFOX_3,
25
+ :css => true,
26
+ :javascript => true,
27
+ :resynchronize => true,
28
+ :js_timeout => 5000,
29
+ :log_level => :warning,
30
+ :log_incorrectness => false,
31
+ :on_error_status => nil, # set to :raise to raise an exception on error status, :print to print content
32
+ :on_script_error => nil # set to :raise to raise an exception on javascript exceptions
33
+ }
34
+ }
35
+ end
36
+ end
37
+
38
+ class ElementNotFound < StandardError
39
+ def initialize(*args)
40
+ super "could not find element: #{args.map { |arg| arg.inspect }.join(', ') }"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,24 @@
1
+ # We currently only implement HtmlUnit as a browser. Maybe at some point
2
+ # Webdriver might be an interesting alternative.
3
+
4
+ require 'core_ext/ruby/string/camelize'
5
+
6
+ module Steam
7
+ module Browser
8
+ autoload :HtmlUnit, 'steam/browser/html_unit'
9
+
10
+ class << self
11
+ def create(*args)
12
+ options = args.last.is_a?(Hash) ? args.pop : {}
13
+ type = args.shift if args.first.is_a?(Symbol)
14
+ connection = args.pop
15
+
16
+ type ||= :html_unit
17
+ type = const_get(type.to_s.camelize)
18
+ type = type.const_get('Drb') if options[:daemon]
19
+
20
+ type.new(connection, :daemon => true)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,87 @@
1
+ # Browser implementation using HtmlUnit. Steam::Session delegates here, so this
2
+ # interface is available in your Cucumber environment. Also see HtmlUnit::Actions
3
+ # which is included here.
4
+
5
+ require 'locator'
6
+ require 'core_ext/ruby/kernel/silence_warnings'
7
+
8
+ module Steam
9
+ module Browser
10
+ class HtmlUnit
11
+ autoload :Actions, 'steam/browser/html_unit/actions'
12
+ autoload :Client, 'steam/browser/html_unit/client'
13
+ autoload :Drb, 'steam/browser/html_unit/drb'
14
+ autoload :Forker, 'steam/browser/html_unit/forker'
15
+ autoload :Connection, 'steam/browser/html_unit/connection'
16
+ autoload :Matchers, 'steam/browser/html_unit/matchers'
17
+ autoload :Page, 'steam/browser/html_unit/page'
18
+ autoload :WebResponse, 'steam/browser/html_unit/web_response'
19
+
20
+ include Actions # Matchers
21
+
22
+ attr_accessor :client, :page, :connection, :request, :response
23
+
24
+ def initialize(*args)
25
+ @client = Client.new(*args)
26
+ end
27
+
28
+ def close
29
+ @client.closeAllWindows
30
+ end
31
+
32
+ def request(url)
33
+ call Request.env_for(url)
34
+ end
35
+ alias :visit :request
36
+
37
+ def call(env)
38
+ respond_to do
39
+ @request = Rack::Request.new(env)
40
+ client.request(@request.url)
41
+ end.to_a
42
+ end
43
+
44
+ def execute(javascript)
45
+ page.execute(javascript) # FIXME does execute return a page so we need to respond?
46
+ end
47
+
48
+ def locate(*args, &block)
49
+ Locator.locate(dom, *args, &block) || raise(ElementNotFound.new(*args))
50
+ end
51
+
52
+ def locate_in_browser(*args, &block)
53
+ if args.first.respond_to?(:_classname) # native HtmlUnit element
54
+ args.first
55
+ elsif args.first.respond_to?(:xpath) # Locator element
56
+ silence_warnings { page.getFirstByXPath(args.first.xpath) }
57
+ else
58
+ locate_in_browser(locate(*args, &block)) # something else
59
+ end
60
+ end
61
+
62
+ def within(*args, &block)
63
+ Locator.within(*args, &block)
64
+ end
65
+
66
+ protected
67
+
68
+ def respond_to
69
+ result = yield || raise('Block did not yield a dom.gargoylesoftware.htmlunit.html.HtmlPage.')
70
+ @page = Page.new(result)
71
+ client.wait_for_javascript(Steam.config[:html_unit][:js_timeout])
72
+ @response = Response.new(*page.to_a)
73
+ end
74
+
75
+ def dom
76
+ case Locator::Dom.adapter.name # yuck
77
+ when /Nokogiri/
78
+ response.body
79
+ when /Htmlunit/
80
+ @page.page
81
+ else
82
+ raise 'incompatible Locator::Dom adapter'
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,176 @@
1
+ # Convenience dsl for executing typical actions on the web browser and current
2
+ # page. Mimicks the well-known Webrat API but adds stuff like drag/drop support.
3
+
4
+ require 'core_ext/ruby/hash/slice'
5
+
6
+ module Steam
7
+ module Browser
8
+ class HtmlUnit
9
+ module Actions
10
+ def click_on(*args)
11
+ respond_to { locate_in_browser(*args).click }
12
+ end
13
+
14
+ def click_link(element, options = {})
15
+ respond_to { locate_in_browser(:link, element, options).click }
16
+ end
17
+
18
+ def click_button(element, options = {})
19
+ respond_to { locate_in_browser(:button, element, options).click }
20
+ end
21
+
22
+ def click_area(element, options = {})
23
+ respond_to { locate_in_browser(:area, element, options).click }
24
+ end
25
+
26
+ def fill_in(element, options = {})
27
+ respond_to do
28
+ value = options.delete(:with)
29
+ element = locate_in_browser(:field, element, options)
30
+ result = element.setText(value) rescue element.setValueAttribute(value)
31
+ # TODO - submit a bug: element.setText returns nil, textarea.setValueAttribute returns a page
32
+ result || page
33
+ end
34
+ end
35
+
36
+ def check(element, options = {})
37
+ respond_to { locate_in_browser(:check_box, element, options).setChecked(true) }
38
+ end
39
+
40
+ def uncheck(element, options = {})
41
+ respond_to { locate_in_browser(:check_box, element, options).setChecked(false) }
42
+ end
43
+
44
+ def choose(element, options = {})
45
+ respond_to { locate_in_browser(:radio_button, element, options).setChecked(true) }
46
+ end
47
+
48
+ def select(element, options = {})
49
+ options.update(:within => [:select, options.delete(:from)])
50
+ respond_to { locate_in_browser(:select_option, element, options).setSelected(true) }
51
+ end
52
+
53
+ def set_hidden_field(element, options = {})
54
+ respond_to do
55
+ value = options.delete(:to)
56
+ locate_in_browser(:hidden_field, element, options).setValueAttribute(value)
57
+ end
58
+ end
59
+
60
+ # TODO implement a way to supply content_type
61
+ def attach_file(element, path, options = {})
62
+ fill_in(element, options.merge(:with => path))
63
+ end
64
+
65
+ def submit_form(element, options = {})
66
+ respond_to do
67
+ scope = [:form, element, options]
68
+ locate_in_browser(:input, :type => 'submit', :within => scope).click
69
+ end
70
+ end
71
+
72
+ def drag_and_drop(element, options = {})
73
+ drag(element, options)
74
+ drop
75
+ end
76
+
77
+ def drag(element, options = {})
78
+ respond_to do
79
+ target = extract_drop_target!(options, [:to, :onto, :over, :target])
80
+ element = locate_in_browser(element, options)
81
+ if target
82
+ element.mouseDown
83
+ @_drop_target = locate_in_browser(target)
84
+ @_drop_target.mouseMove
85
+ else
86
+ element.mouseDown
87
+ end
88
+ end
89
+ end
90
+
91
+ def drop(element = nil, options = {})
92
+ respond_to do
93
+ element = @_drop_target || locate_in_browser(element, options)
94
+ element.mouseMove unless @_drop_target
95
+ @_drop_target = nil
96
+ element.mouseUp
97
+ end
98
+ end
99
+
100
+ def hover(element, options = {})
101
+ respond_to { locate_in_browser(element, options).mouseOver }
102
+ end
103
+
104
+ def blur(element, options = {})
105
+ respond_to do
106
+ locate_in_browser(element, options).blur
107
+ @page # blur seems to return nil
108
+ end
109
+ end
110
+
111
+ def focus(element, options = {})
112
+ respond_to do
113
+ locate_in_browser(element, options).focus
114
+ @page # focus seems to return nil
115
+ end
116
+ end
117
+
118
+ def double_click(element, options = {})
119
+ respond_to { locate_in_browser(element, options).dblClick }
120
+ end
121
+
122
+ # Rails specific respond_tos
123
+ DATE_TIME_CODE = {
124
+ :year => '1i',
125
+ :month => '2i',
126
+ :day => '3i',
127
+ :hour => '4i',
128
+ :minute => '5i',
129
+ :second => '6i'
130
+ }
131
+
132
+ def select_date(date, options = {})
133
+ date = date.respond_to?(:strftime) ? date : Date.parse(date)
134
+ prefix = locate_id_prefix(options)
135
+
136
+ # FIXME .to_s chould be done somewhere inside the locator
137
+ select(date.year.to_s, :from => "#{prefix}_#{DATE_TIME_CODE[:year]}")
138
+ select(date.strftime('%B'), :from => "#{prefix}_#{DATE_TIME_CODE[:month]}")
139
+ select(date.day.to_s, :from => "#{prefix}_#{DATE_TIME_CODE[:day]}")
140
+ end
141
+
142
+ def select_datetime(datetime, options = {})
143
+ select_date(datetime)
144
+ select_time(datetime)
145
+ end
146
+
147
+ def select_time(time, options = {})
148
+ time = time.respond_to?(:strftime) ? time : DateTime.parse(time)
149
+ prefix = locate_id_prefix(options)
150
+
151
+ select(time.hour.to_s.rjust(2,'0'), :from => "#{prefix}_#{DATE_TIME_CODE[:hour]}")
152
+ select(time.min.to_s.rjust(2,'0'), :from => "#{prefix}_#{DATE_TIME_CODE[:minute]}")
153
+ # second?
154
+ end
155
+
156
+ protected
157
+
158
+ # inspired by Webrat
159
+ def locate_id_prefix(options)
160
+ options[:id_prefix] ? options[:id_prefix] : locate_id_prefix_label(options).attribute('for')
161
+ end
162
+
163
+ def locate_id_prefix_label(options)
164
+ locate_in_browser(:label, options[:from]) rescue locate_in_browser(:label, :for => options[:from])
165
+ end
166
+
167
+ def extract_drop_target!(targets, keys)
168
+ rest = targets.slice!(*keys)
169
+ target = targets.values.compact.first
170
+ targets.replace(rest)
171
+ target
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end