steam 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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