jasmine 0.4.0

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 (32) hide show
  1. data/README.markdown +21 -0
  2. data/bin/jasmine +55 -0
  3. data/generators/jasmine/jasmine_generator.rb +25 -0
  4. data/generators/jasmine/templates/INSTALL +9 -0
  5. data/generators/jasmine/templates/lib/tasks/jasmine.rake +23 -0
  6. data/generators/jasmine/templates/spec/javascripts/ExampleSpec.js +11 -0
  7. data/generators/jasmine/templates/spec/javascripts/SpecHelper.js +1 -0
  8. data/generators/jasmine/templates/spec/javascripts/support/jasmine_config.rb +23 -0
  9. data/generators/jasmine/templates/spec/javascripts/support/jasmine_spec.rb +17 -0
  10. data/generators/jasmine/templates/spec/javascripts/support/sources-rails.yaml +8 -0
  11. data/generators/jasmine/templates/spec/javascripts/support/sources.yaml +5 -0
  12. data/jasmine/contrib/ruby/jasmine_runner.rb +334 -0
  13. data/jasmine/contrib/ruby/jasmine_spec_builder.rb +153 -0
  14. data/jasmine/contrib/ruby/run.html +47 -0
  15. data/jasmine/lib/TrivialReporter.js +117 -0
  16. data/jasmine/lib/consolex.js +28 -0
  17. data/jasmine/lib/jasmine-0.10.0.js +2261 -0
  18. data/jasmine/lib/jasmine.css +86 -0
  19. data/jasmine/lib/json2.js +478 -0
  20. data/lib/jasmine.rb +6 -0
  21. data/lib/jasmine/base.rb +63 -0
  22. data/lib/jasmine/config.rb +151 -0
  23. data/lib/jasmine/run.html.erb +43 -0
  24. data/lib/jasmine/selenium_driver.rb +44 -0
  25. data/lib/jasmine/server.rb +125 -0
  26. data/lib/jasmine/spec_builder.rb +152 -0
  27. data/spec/config_spec.rb +77 -0
  28. data/spec/jasmine_self_test_config.rb +15 -0
  29. data/spec/jasmine_self_test_spec.rb +18 -0
  30. data/spec/server_spec.rb +65 -0
  31. data/spec/spec_helper.rb +3 -0
  32. metadata +143 -0
data/lib/jasmine.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'jasmine/base'
2
+ require 'jasmine/config'
3
+ require 'jasmine/server'
4
+ require 'jasmine/selenium_driver'
5
+
6
+ require 'jasmine/spec_builder'
@@ -0,0 +1,63 @@
1
+ require 'socket'
2
+ require 'erb'
3
+ require 'json'
4
+
5
+ module Jasmine
6
+ def self.root
7
+ File.expand_path(File.join(File.dirname(__FILE__), '../../jasmine'))
8
+ end
9
+
10
+ # this seemingly-over-complex method is necessary to get an open port on at least some of our Macs
11
+ def self.open_socket_on_unused_port
12
+ infos = Socket::getaddrinfo("localhost", nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM, 0, Socket::AI_PASSIVE)
13
+ families = Hash[*infos.collect { |af, *_| af }.uniq.zip([]).flatten]
14
+
15
+ return TCPServer.open('0.0.0.0', 0) if families.has_key?('AF_INET')
16
+ return TCPServer.open('::', 0) if families.has_key?('AF_INET6')
17
+ return TCPServer.open(0)
18
+ end
19
+
20
+ def self.find_unused_port
21
+ socket = open_socket_on_unused_port
22
+ port = socket.addr[1]
23
+ socket.close
24
+ port
25
+ end
26
+
27
+ def self.server_is_listening_on(hostname, port)
28
+ require 'socket'
29
+ begin
30
+ socket = TCPSocket.open(hostname, port)
31
+ rescue Errno::ECONNREFUSED
32
+ return false
33
+ end
34
+ socket.close
35
+ true
36
+ end
37
+
38
+ def self.wait_for_listener(port, name = "required process", seconds_to_wait = 10)
39
+ time_out_at = Time.now + seconds_to_wait
40
+ until server_is_listening_on "localhost", port
41
+ sleep 0.1
42
+ puts "Waiting for #{name} on #{port}..."
43
+ raise "#{name} didn't show up on port #{port} after #{seconds_to_wait} seconds." if Time.now > time_out_at
44
+ end
45
+ end
46
+
47
+ def self.kill_process_group(process_group_id, signal="TERM")
48
+ Process.kill signal, -process_group_id # negative pid means kill process group. (see man 2 kill)
49
+ end
50
+
51
+ def self.cachebust(files, root_dir="", replace=nil, replace_with=nil)
52
+ require 'digest/md5'
53
+ files.collect do |file_name|
54
+ real_file_name = replace && replace_with ? file_name.sub(replace, replace_with) : file_name
55
+ begin
56
+ digest = Digest::MD5.hexdigest(File.read("#{root_dir}#{real_file_name}"))
57
+ rescue
58
+ digest = "MISSING-FILE"
59
+ end
60
+ "#{file_name}?cachebust=#{digest}"
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,151 @@
1
+ module Jasmine
2
+ class Config
3
+ require 'yaml'
4
+
5
+ def initialize(options = {})
6
+ require 'selenium_rc'
7
+ @selenium_jar_path = SeleniumRC::Server.allocate.jar_path
8
+ @options = options
9
+
10
+ @browser = options[:browser] ? options.delete(:browser) : 'firefox'
11
+ @selenium_pid = nil
12
+ @jasmine_server_pid = nil
13
+ end
14
+
15
+ def start_server(port = 8888)
16
+ Jasmine::Server.new(port, self).start
17
+ end
18
+
19
+ def start
20
+ start_servers
21
+ @client = Jasmine::SeleniumDriver.new("localhost", @selenium_server_port, "*#{@browser}", "http://localhost:#{@jasmine_server_port}/")
22
+ @client.connect
23
+ end
24
+
25
+ def stop
26
+ @client.disconnect
27
+ stop_servers
28
+ end
29
+
30
+ def start_servers
31
+ @jasmine_server_port = Jasmine::find_unused_port
32
+ @selenium_server_port = Jasmine::find_unused_port
33
+
34
+ server = Jasmine::Server.new(@jasmine_server_port, self)
35
+
36
+ @selenium_pid = fork do
37
+ Process.setpgrp
38
+ exec "java -jar #{@selenium_jar_path} -port #{@selenium_server_port} > /dev/null 2>&1"
39
+ end
40
+ puts "selenium started. pid is #{@selenium_pid}"
41
+
42
+ @jasmine_server_pid = fork do
43
+ Process.setpgrp
44
+ server.start
45
+ exit! 0
46
+ end
47
+ puts "jasmine server started. pid is #{@jasmine_server_pid}"
48
+
49
+ Jasmine::wait_for_listener(@selenium_server_port, "selenium server")
50
+ Jasmine::wait_for_listener(@jasmine_server_port, "jasmine server")
51
+ end
52
+
53
+ def stop_servers
54
+ puts "shutting down the servers..."
55
+ Jasmine::kill_process_group(@selenium_pid) if @selenium_pid
56
+ Jasmine::kill_process_group(@jasmine_server_pid) if @jasmine_server_pid
57
+ end
58
+
59
+ def run
60
+ begin
61
+ start
62
+ puts "servers are listening on their ports -- running the test script..."
63
+ tests_passed = @client.run
64
+ ensure
65
+ stop
66
+ end
67
+ return tests_passed
68
+ end
69
+
70
+ def eval_js(script)
71
+ @client.eval_js(script)
72
+ end
73
+
74
+ def stylesheets
75
+ []
76
+ end
77
+
78
+ def src_files
79
+ []
80
+ end
81
+
82
+ def spec_files
83
+ raise "You need to declare a spec_files method in #{self.class}!"
84
+ end
85
+
86
+ def match_files(dir, pattern)
87
+ dir = File.expand_path(dir)
88
+ Dir.glob(File.join(dir, pattern)).collect {|f| f.sub("#{dir}/", "")}.sort
89
+ end
90
+
91
+ def project_root
92
+ Dir.pwd
93
+ end
94
+
95
+ def src_dir
96
+ if simple_config['src_dir']
97
+ File.join(project_root, simple_config['src_dir'])
98
+ else
99
+ project_root
100
+ end
101
+ end
102
+
103
+ def simple_config_file
104
+ File.join(project_root, 'spec/javascripts/support/sources.yaml')
105
+ end
106
+
107
+ def simple_config
108
+ config = File.exist?(simple_config_file) ? File.open(simple_config_file) { |yf| YAML::load( yf ) } : false
109
+ config || {}
110
+ end
111
+
112
+ def src_files
113
+ simple_config['sources'] || []
114
+ end
115
+
116
+ def spec_dir
117
+ if simple_config['spec_dir']
118
+ File.join(project_root, simple_config['spec_dir'])
119
+ else
120
+ File.join(project_root, 'spec/javascripts')
121
+ end
122
+ end
123
+
124
+ def spec_files
125
+ match_files(spec_dir, "**/*.js")
126
+ end
127
+
128
+ def spec_path
129
+ "/__spec__"
130
+ end
131
+
132
+ def root_path
133
+ "/__root__"
134
+ end
135
+
136
+ def mappings
137
+ {
138
+ spec_path => spec_dir,
139
+ root_path => project_root
140
+ }
141
+ end
142
+
143
+ def js_files
144
+ src_files.collect {|f| "/" + f } + spec_files.collect {|f| File.join(spec_path, f) }
145
+ end
146
+
147
+ def spec_files_full_paths
148
+ spec_files.collect {|spec_file| File.join(spec_dir, spec_file) }
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,43 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
2
+ <html xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
3
+ <head>
4
+ <meta content="text/html;charset=UTF-8" http-equiv="Content-Type"/>
5
+ <title>Jasmine suite</title>
6
+ <% css_files.each do |css_file| %>
7
+ <link rel="stylesheet" href="<%= css_file %>" type="text/css" media="screen"/>
8
+ <% end %>
9
+
10
+ <% jasmine_files.each do |jasmine_file| %>
11
+ <script src="<%= jasmine_file %>" type="text/javascript"></script>
12
+ <% end %>
13
+
14
+ <script type="text/javascript">
15
+ var jsApiReporter;
16
+ (function() {
17
+ var jasmineEnv = jasmine.getEnv();
18
+
19
+ jsApiReporter = new jasmine.JsApiReporter();
20
+ var trivialReporter = new jasmine.TrivialReporter();
21
+
22
+ jasmineEnv.addReporter(jsApiReporter);
23
+ jasmineEnv.addReporter(trivialReporter);
24
+
25
+ jasmineEnv.specFilter = function(spec) {
26
+ return trivialReporter.specFilter(spec);
27
+ };
28
+
29
+ window.onload = function() {
30
+ jasmineEnv.execute();
31
+ };
32
+ })();
33
+ </script>
34
+
35
+ <% js_files.each do |js_file| %>
36
+ <script src="<%= js_file %>" type="text/javascript"></script>
37
+ <% end %>
38
+
39
+ </head>
40
+ <body>
41
+ <div id="jasmine_content"></div>
42
+ </body>
43
+ </html>
@@ -0,0 +1,44 @@
1
+ module Jasmine
2
+ class SeleniumDriver
3
+ def initialize(selenium_host, selenium_port, selenium_browser_start_command, http_address)
4
+ require 'selenium/client'
5
+ @driver = Selenium::Client::Driver.new(
6
+ selenium_host,
7
+ selenium_port,
8
+ selenium_browser_start_command,
9
+ http_address
10
+ )
11
+ @http_address = http_address
12
+ end
13
+
14
+ def tests_have_finished?
15
+ @driver.get_eval("window.jasmine.getEnv().currentRunner.finished") == "true"
16
+ end
17
+
18
+ def connect
19
+ @driver.start
20
+ @driver.open("/")
21
+ end
22
+
23
+ def disconnect
24
+ @driver.stop
25
+ end
26
+
27
+ def run
28
+ until tests_have_finished? do
29
+ sleep 0.1
30
+ end
31
+
32
+ puts @driver.get_eval("window.results()")
33
+ failed_count = @driver.get_eval("window.jasmine.getEnv().currentRunner.results().failedCount").to_i
34
+ failed_count == 0
35
+ end
36
+
37
+ def eval_js(script)
38
+ escaped_script = "'" + script.gsub(/(['\\])/) { '\\' + $1 } + "'"
39
+
40
+ result = @driver.get_eval("try { eval(#{escaped_script}, window); } catch(err) { window.eval(#{escaped_script}); }")
41
+ JSON.parse("{\"result\":#{result}}")["result"]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,125 @@
1
+ module Jasmine
2
+ class RunAdapter
3
+ def initialize(config)
4
+ @config = config
5
+ @jasmine_files = [
6
+ "/__JASMINE_ROOT__/lib/" + File.basename(Dir.glob("#{Jasmine.root}/lib/jasmine*.js").first),
7
+ "/__JASMINE_ROOT__/lib/TrivialReporter.js",
8
+ "/__JASMINE_ROOT__/lib/json2.js",
9
+ "/__JASMINE_ROOT__/lib/consolex.js",
10
+ ]
11
+ @jasmine_stylesheets = ["/__JASMINE_ROOT__/lib/jasmine.css"]
12
+ end
13
+
14
+ def call(env)
15
+ run
16
+ end
17
+
18
+ #noinspection RubyUnusedLocalVariable
19
+ def run
20
+ jasmine_files = @jasmine_files
21
+ css_files = @jasmine_stylesheets + (@config.stylesheets || [])
22
+ js_files = @config.js_files
23
+
24
+ body = ERB.new(File.read(File.join(File.dirname(__FILE__), "run.html.erb"))).result(binding)
25
+ [
26
+ 200,
27
+ { 'Content-Type' => 'text/html' },
28
+ body
29
+ ]
30
+ end
31
+
32
+
33
+ end
34
+
35
+ class Redirect
36
+ def initialize(url)
37
+ @url = url
38
+ end
39
+
40
+ def call(env)
41
+ [
42
+ 302,
43
+ { 'Location' => @url },
44
+ []
45
+ ]
46
+ end
47
+ end
48
+
49
+ class JsAlert
50
+ def call(env)
51
+ [
52
+ 200,
53
+ { 'Content-Type' => 'application/javascript' },
54
+ "document.write('<p>Couldn\\'t load #{env["PATH_INFO"]}!</p>');"
55
+ ]
56
+ end
57
+ end
58
+
59
+ class FocusedSuite
60
+ def initialize(config)
61
+ @config = config
62
+ # @spec_files_or_proc = spec_files_or_proc || []
63
+ # @options = options
64
+ end
65
+
66
+ def call(env)
67
+ spec_files = @config.spec_files_or_proc
68
+ matching_specs = spec_files.select {|spec_file| spec_file =~ /#{Regexp.escape(env["PATH_INFO"])}/ }.compact
69
+ if !matching_specs.empty?
70
+ run_adapter = Jasmine::RunAdapter.new(matching_specs, @options)
71
+ run_adapter.run
72
+ else
73
+ [
74
+ 200,
75
+ { 'Content-Type' => 'application/javascript' },
76
+ "document.write('<p>Couldn\\'t find any specs matching #{env["PATH_INFO"]}!</p>');"
77
+ ]
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+ class Server
84
+ attr_reader :thin
85
+
86
+ def initialize(port, config)
87
+ @port = port
88
+ @config = config
89
+
90
+ require 'thin'
91
+ thin_config = {
92
+ '/__suite__' => Jasmine::FocusedSuite.new(@config),
93
+ '/run.html' => Jasmine::Redirect.new('/'),
94
+ '/' => Jasmine::RunAdapter.new(@config)
95
+ }
96
+
97
+ @config.mappings.each do |from, to|
98
+ thin_config[from] = Rack::File.new(to)
99
+ end
100
+
101
+ thin_config["/__JASMINE_ROOT__"] = Rack::File.new(Jasmine.root)
102
+
103
+ app = Rack::Cascade.new([
104
+ Rack::URLMap.new({'/' => Rack::File.new(@config.src_dir)}),
105
+ Rack::URLMap.new(thin_config),
106
+ JsAlert.new
107
+ ])
108
+
109
+ @thin = Thin::Server.new('0.0.0.0', @port, app)
110
+ end
111
+
112
+ def start
113
+ begin
114
+ thin.start
115
+ rescue RuntimeError => e
116
+ raise e unless e.message == 'no acceptor'
117
+ raise RuntimeError.new("A server is already running on port #{@port}")
118
+ end
119
+ end
120
+
121
+ def stop
122
+ thin.stop
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,152 @@
1
+ require 'enumerator'
2
+
3
+ module Jasmine
4
+ class SpecBuilder
5
+ attr_accessor :suites
6
+
7
+ def initialize(config)
8
+ @config = config
9
+ @spec_files = config.spec_files
10
+ @runner = config
11
+ @spec_ids = []
12
+ end
13
+
14
+ def start
15
+ guess_example_locations
16
+
17
+ @runner.start
18
+ load_suite_info
19
+ wait_for_suites_to_finish_running
20
+ end
21
+
22
+ def stop
23
+ @runner.stop
24
+ end
25
+
26
+ def script_path
27
+ File.expand_path(__FILE__)
28
+ end
29
+
30
+ def guess_example_locations
31
+ @example_locations = {}
32
+
33
+ example_name_parts = []
34
+ previous_indent_level = 0
35
+ @config.spec_files_full_paths.each do |filename|
36
+ line_number = 1
37
+ File.open(filename, "r") do |file|
38
+ file.readlines.each do |line|
39
+ match = /^(\s*)(describe|it)\s*\(\s*["'](.*)["']\s*,\s*function/.match(line)
40
+ if (match)
41
+ indent_level = match[1].length / 2
42
+ example_name = match[3]
43
+ example_name_parts[indent_level] = example_name
44
+
45
+ full_example_name = example_name_parts.slice(0, indent_level + 1).join(" ")
46
+ @example_locations[full_example_name] = "#{filename}:#{line_number}: in `it'"
47
+ end
48
+ line_number += 1
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def load_suite_info
55
+ started = Time.now
56
+ while !eval_js('jsApiReporter && jsApiReporter.started') do
57
+ raise "couldn't connect to Jasmine after 60 seconds" if (started + 60 < Time.now)
58
+ sleep 0.1
59
+ end
60
+
61
+ @suites = eval_js("var result = jsApiReporter.suites(); if (window.Prototype && Object.toJSON) { Object.toJSON(result) } else { JSON.stringify(result) }")
62
+ end
63
+
64
+ def results_for(spec_id)
65
+ @spec_results ||= load_results
66
+ @spec_results[spec_id.to_s]
67
+ end
68
+
69
+ def load_results
70
+ @spec_results = {}
71
+ @spec_ids.each_slice(50) do |slice|
72
+ @spec_results.merge!(eval_js("var result = jsApiReporter.resultsForSpecs(#{JSON.generate(slice)}); if (window.Prototype && Object.toJSON) { Object.toJSON(result) } else { JSON.stringify(result) }"))
73
+ end
74
+ @spec_results
75
+ end
76
+
77
+ def wait_for_suites_to_finish_running
78
+ puts "Waiting for suite to finish in browser ..."
79
+ while !eval_js('jsApiReporter.finished') do
80
+ sleep 0.1
81
+ end
82
+ end
83
+
84
+ def declare_suites
85
+ me = self
86
+ suites.each do |suite|
87
+ declare_suite(self, suite)
88
+ end
89
+ end
90
+
91
+ def declare_suite(parent, suite)
92
+ me = self
93
+ parent.describe suite["name"] do
94
+ suite["children"].each do |suite_or_spec|
95
+ type = suite_or_spec["type"]
96
+ if type == "suite"
97
+ me.declare_suite(self, suite_or_spec)
98
+ elsif type == "spec"
99
+ me.declare_spec(self, suite_or_spec)
100
+ else
101
+ raise "unknown type #{type} for #{suite_or_spec.inspect}"
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ def declare_spec(parent, spec)
108
+ me = self
109
+ example_name = spec["name"]
110
+ @spec_ids << spec["id"]
111
+ backtrace = @example_locations[parent.description + " " + example_name]
112
+ parent.it example_name, {}, backtrace do
113
+ me.report_spec(spec["id"])
114
+ end
115
+ end
116
+
117
+ def report_spec(spec_id)
118
+ spec_results = results_for(spec_id)
119
+ out = ""
120
+ messages = spec_results['messages'].each do |message|
121
+ case
122
+ when message["type"] == "MessageResult"
123
+ puts message["text"]
124
+ puts "\n"
125
+ else
126
+ unless message["message"] =~ /^Passed.$/
127
+ STDERR << message["message"]
128
+ STDERR << "\n"
129
+
130
+ out << message["message"]
131
+ out << "\n"
132
+ end
133
+
134
+ if !message["passed"] && message["trace"]["stack"]
135
+ stack_trace = message["trace"]["stack"].gsub(/<br \/>/, "\n").gsub(/<\/?b>/, " ")
136
+ STDERR << stack_trace.gsub(/\(.*\)@http:\/\/localhost:[0-9]+\/specs\//, "/spec/")
137
+ STDERR << "\n"
138
+ end
139
+ end
140
+
141
+ end
142
+ fail out unless spec_results['result'] == 'passed'
143
+ puts out unless out.empty?
144
+ end
145
+
146
+ private
147
+
148
+ def eval_js(js)
149
+ @runner.eval_js(js)
150
+ end
151
+ end
152
+ end