guard-jasmine 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -135,6 +135,12 @@ There following options can be passed to Guard::Jasmine:
135
135
  :all_on_start => false # Run all specs on start.
136
136
  # default: true
137
137
 
138
+ :keep_failed => false # Keep failed specs and add them the next run again.
139
+ # default: true
140
+
141
+ :all_after_pass => false # Run all specs after a single spec has passed.
142
+ # default: true
143
+
138
144
  :notifications => false # Show success and error messages.
139
145
  # default: true
140
146
 
@@ -144,7 +150,7 @@ There following options can be passed to Guard::Jasmine:
144
150
 
145
151
  ## Alternatives
146
152
 
147
- * [guard-jasmine-headless-webkit][], a Guard for [jasmine-headless-webkit][], but doesn't run in JRuby.
153
+ * [guard-jasmine-headless-webkit][], a Guard for [jasmine-headless-webkit][], but doesn't run on JRuby.
148
154
  * [Evergreen][], runs CoffeeScript specs headless, but has no
149
155
  continuous testing support.
150
156
  * [Jezebel][] a Node.js REPL and continuous test runner for [Jessie][], a Node runner for Jasmine, but has no full
@@ -168,26 +174,17 @@ For questions please join us on our [Google group](http://groups.google.com/grou
168
174
 
169
175
  ## Acknowledgment
170
176
 
171
- [Ariya Hidayat][] for [PhantomJS][], a powerfull headless WebKit browser.
172
-
173
- [Brad Phelan][] for [Jasminerice][], an elegant solution for [Jasmine][] in the Rails 3.1 asset pipeline.
174
-
175
- [Pivotal Labs][] for their beautiful [Jasmine][] BDD testing framework that makes JavaScript testing fun.
176
-
177
- [Jeremy Ashkenas][] for [CoffeeScript][], that little language that compiles into JavaScript and makes me enjoy the
177
+ - [Ariya Hidayat][] for [PhantomJS][], a powerfull headless WebKit browser.
178
+ - [Brad Phelan][] for [Jasminerice][], an elegant solution for [Jasmine][] in the Rails 3.1 asset pipeline.
179
+ - [Pivotal Labs][] for their beautiful [Jasmine][] BDD testing framework that makes JavaScript testing fun.
180
+ - [Jeremy Ashkenas][] for [CoffeeScript][], that little language that compiles into JavaScript and makes me enjoy the
178
181
  frontend.
179
-
180
- The [Guard Team][] for giving us such a nice piece of software that is so easy to extend, one *has* to make a plugin
182
+ - The [Guard Team][] for giving us such a nice piece of software that is so easy to extend, one *has* to make a plugin
181
183
  for it!
182
-
183
- All the authors of the numerous [Guards][] available for making the Guard ecosystem so much growing and comprehensive.
184
+ - All the authors of the numerous [Guards][] available for making the Guard ecosystem so much growing and comprehensive.
184
185
 
185
186
  ## License
186
187
 
187
- The Jasmine PhantomJS runner file [run-jasmine.coffee][] from [Roejames12][] is licensed under the BSD license.
188
-
189
- The Guard::Jasmine itself is released under:
190
-
191
188
  (The MIT License)
192
189
 
193
190
  Copyright (c) 2011 Michael Kessler
@@ -218,8 +215,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
218
215
  [PhantomJS]: http://www.phantomjs.org/
219
216
  [the PhantomJS download section]: http://code.google.com/p/phantomjs/downloads/list
220
217
  [PhantomJS build instructions]: http://code.google.com/p/phantomjs/wiki/BuildInstructions
221
- [Roejames12]: https://github.com/Roejames12
222
- [run-jasmine.coffee]: https://github.com/ariya/phantomjs/blob/master/examples/run-jasmine.coffee
223
218
  [Brad Phelan]: http://twitter.com/#!/bradgonesurfing
224
219
  [Jasminerice]: https://github.com/bradphelan/jasminerice
225
220
  [Pivotal Labs]: http://pivotallabs.com/
@@ -1,6 +1,7 @@
1
1
  require 'guard'
2
2
  require 'guard/guard'
3
3
  require 'guard/watcher'
4
+ require 'net/http'
4
5
 
5
6
  module Guard
6
7
 
@@ -13,6 +14,8 @@ module Guard
13
14
  autoload :Inspector, 'guard/jasmine/inspector'
14
15
  autoload :Runner, 'guard/jasmine/runner'
15
16
 
17
+ attr_accessor :last_run_failed, :last_failed_paths
18
+
16
19
  # Initialize Guard::Jasmine.
17
20
  #
18
21
  # @param [Array<Guard::Watcher>] watchers the watchers in the Guard block
@@ -22,17 +25,24 @@ module Guard
22
25
  # @option options [Boolean] :notification show notifications
23
26
  # @option options [Boolean] :hide_success hide success message notification
24
27
  # @option options [Boolean] :all_on_start run all suites on start
28
+ # @option options [Boolean] :keep_failed keep failed specs and add them the next run again
29
+ # @option options [Boolean] :all_after_pass run all specs after a single spec has passed
25
30
  #
26
31
  def initialize(watchers = [], options = { })
27
32
  defaults = {
28
- :jasmine_url => 'http://localhost:3000/jasmine',
29
- :phantomjs_bin => '/usr/local/bin/phantomjs',
30
- :notification => true,
31
- :hide_success => false,
32
- :all_on_start => true
33
+ :jasmine_url => 'http://localhost:3000/jasmine',
34
+ :phantomjs_bin => '/usr/local/bin/phantomjs',
35
+ :notification => true,
36
+ :hide_success => false,
37
+ :all_on_start => true,
38
+ :keep_failed => true,
39
+ :all_after_pass => true
33
40
  }
34
41
 
35
42
  super(watchers, defaults.merge(options))
43
+
44
+ self.last_run_failed = false
45
+ self.last_failed_paths = []
36
46
  end
37
47
 
38
48
  # Gets called once when Guard starts.
@@ -40,7 +50,20 @@ module Guard
40
50
  # @return [Boolean] when the start was successful
41
51
  #
42
52
  def start
43
- run_all if options[:all_on_start]
53
+ if jasmine_runner_available?(options[:jasmine_url])
54
+ run_all if options[:all_on_start]
55
+ end
56
+
57
+ true
58
+ end
59
+
60
+ # Gets called when the Guard should reload itself.
61
+ #
62
+ # @return [Boolean] when the reload was successful
63
+ #
64
+ def reload
65
+ self.last_run_failed = false
66
+ self.last_failed_paths = []
44
67
 
45
68
  true
46
69
  end
@@ -50,7 +73,12 @@ module Guard
50
73
  # @return [Boolean] when running all specs was successful
51
74
  #
52
75
  def run_all
53
- Runner.run(['spec/javascripts'], options)
76
+ passed, failed_specs = Runner.run(['spec/javascripts'], options)
77
+
78
+ self.last_failed_paths = failed_specs
79
+ self.last_run_failed = !passed
80
+
81
+ passed
54
82
  end
55
83
 
56
84
  # Gets called when watched paths and files have changes.
@@ -59,9 +87,63 @@ module Guard
59
87
  # @return [Boolean] when running the changed specs was successful
60
88
  #
61
89
  def run_on_change(paths)
62
- Runner.run(Inspector.clean(paths), options)
90
+ paths += self.last_failed_paths if options[:keep_failed]
91
+
92
+ passed, failed_specs = Runner.run(Inspector.clean(paths), options)
63
93
 
64
- #TODO: Evaluate result
94
+ if passed
95
+ self.last_failed_paths = self.last_failed_paths - paths
96
+ run_all if self.last_run_failed && options[:all_after_pass]
97
+ else
98
+ self.last_failed_paths = self.last_failed_paths + failed_specs
99
+ end
100
+
101
+ self.last_run_failed = !passed
102
+
103
+ passed
104
+ end
105
+
106
+ private
107
+
108
+ # Verifies if the Jasmine test runner is available.
109
+ #
110
+ # @param [String] url the location of the test runner
111
+ # @return [Boolean] when the runner is available
112
+ #
113
+ def jasmine_runner_available?(url)
114
+ url = URI.parse(url)
115
+
116
+ begin
117
+ Net::HTTP.start(url.host, url.port) do |http|
118
+ response = http.request(Net::HTTP::Head.new(url.path))
119
+
120
+ if response.code.to_i == 200
121
+ Formatter.info("Jasmine test runner is available at #{ url }")
122
+ else
123
+ notify_jasmine_runner_failure(url) if options[:notification]
124
+ end
125
+
126
+ response.code.to_i == 200
127
+ end
128
+
129
+ rescue Errno::ECONNREFUSED => e
130
+ notify_jasmine_runner_failure(url)
131
+
132
+ false
133
+ end
134
+ end
135
+
136
+ # Notify that the Jasmine runner is not available.
137
+ #
138
+ # @param [String] url the url of the Jasmine runner
139
+ #
140
+ def notify_jasmine_runner_failure(url)
141
+ message = "Jasmine test runner not available at #{ url }"
142
+ Formatter.error(message)
143
+ Formatter.notify(message,
144
+ :title => 'Jasmine test runner not available',
145
+ :image => :failed,
146
+ :priority => 2)
65
147
  end
66
148
 
67
149
  end
@@ -48,6 +48,24 @@ module Guard
48
48
  ::Guard::UI.info(color(message, ';32'), options)
49
49
  end
50
50
 
51
+ # Print a red spec failed message to the console.
52
+ #
53
+ # @param [String] message the message to print
54
+ # @param [Hash] options the output options
55
+ #
56
+ def spec_failed(message, options = { })
57
+ ::Guard::UI.info(color(message, ';31'), options)
58
+ end
59
+
60
+ # Print a red spec failed message to the console.
61
+ #
62
+ # @param [String] message the message to print
63
+ # @param [Hash] options the output options
64
+ #
65
+ def suite_name(message, options = { })
66
+ ::Guard::UI.info(color(message, ';33'), options)
67
+ end
68
+
51
69
  # Outputs a system notification.
52
70
  #
53
71
  # @param [String] message the message to print
@@ -1,29 +1,85 @@
1
+ # This file is the script that runs within PhantomJS and requests the Jasmine specs,
2
+ # waits until they are ready, extracts the result form the dom and outputs a JSON
3
+ # structure that is the parsed by Guard::Jasmine.
4
+ #
5
+ # This scripts needs the TrivialReporter to report the results.
6
+ #
7
+ # This file is inspired by the Jasmine runner that comes with the PhantomJS examples:
8
+ # https://github.com/ariya/phantomjs/blob/master/examples/run-jasmine.coffee, by https://github.com/Roejames12
9
+ #
10
+ # This file is licensed under the BSD license.
11
+
1
12
  # Wait until the test condition is true or a timeout occurs.
2
13
  #
3
- # @param [Function] testFx the condition that evaluates to a boolean
4
- # @param [Function] onReady the action when the condition is fulfilled
5
- # @param [Number] timeOutMillis the max amount of time to wait
14
+ # @param [Function] condition the condition that evaluates to a boolean
15
+ # @param [Function] ready the action when the condition is fulfilled
16
+ # @param [Number] timeout the max amount of time to wait
6
17
  #
7
- waitFor = (testFx, onReady, timeOutMillis=3000) ->
18
+ waitFor = (condition, ready, timeout = 3000) ->
8
19
  start = new Date().getTime()
9
- condition = false
10
20
  wait = ->
11
- if (new Date().getTime() - start < timeOutMillis) and not condition
12
- condition = (if typeof testFx is 'string' then eval testFx else testFx())
21
+ if new Date().getTime() - start > timeout
22
+ console.log JSON.stringify({ error: "Timeout requesting Jasmine test runner!" })
23
+ phantom.exit(1)
13
24
  else
14
- if not condition
15
- console.log JSON.stringify { error: "Timeout requesting Jasmine test runner!" }
16
- phantom.exit(1)
17
- else
18
- if typeof onReady is 'string' then eval onReady else onReady()
25
+ if condition()
26
+ ready()
19
27
  clearInterval interval
20
28
 
21
29
  interval = setInterval wait, 100
22
30
 
31
+ # Test if the specs have finished.
32
+ #
33
+ specsReady = ->
34
+ page.evaluate -> if document.body.querySelector('.finished-at') then true else false
35
+
36
+ # Extract the data from a Jasmine TrivialReporter generated DOM
37
+ #
38
+ extractResult = ->
39
+ page.evaluate ->
40
+ stats = /(\d+) specs, (\d+) failures? in (\d+.\d+)s/.exec document.body.querySelector('.description').innerText
41
+
42
+ result = {
43
+ passed: true
44
+ stats: {
45
+ specs: parseInt stats[1]
46
+ failures: parseInt stats[2]
47
+ time: parseFloat stats[3]
48
+ }
49
+ suites: []
50
+ }
51
+
52
+ for suite in document.body.querySelectorAll('div.jasmine_reporter > div.suite')
53
+ description = suite.querySelector('a.description')
54
+
55
+ suite_ = {
56
+ description: description.innerText
57
+ specs: []
58
+ }
59
+
60
+ for spec in suite.querySelectorAll('div.spec')
61
+ passed = spec.getAttribute('class').indexOf('passed') isnt -1
62
+ result['passed'] = false if not passed
63
+
64
+ spec_ = {
65
+ description: spec.querySelector('a.description').getAttribute 'title'
66
+ passed: passed
67
+ }
68
+
69
+ spec_['error_message'] = spec.querySelector('div.resultMessage').innerText if not passed
70
+
71
+ suite_['specs'].push spec_
72
+
73
+ result['suites'].push suite_
74
+
75
+ console.log "JSON_RESULT: #{ JSON.stringify(result, undefined, 2) }"
76
+
77
+ phantom.exit()
78
+
23
79
  # Check arguments of the script.
24
80
  #
25
81
  if phantom.args.length isnt 1
26
- console.log JSON.stringify { error: "Wrong usage of PhantomJS script!" }
82
+ console.log JSON.stringify({ error: "Wrong usage of PhantomJS script!" })
27
83
  phantom.exit()
28
84
  else
29
85
  url = phantom.args[0]
@@ -31,66 +87,16 @@ else
31
87
  page = new WebPage()
32
88
 
33
89
  # Output the Jasmine test runner result as JSON object.
34
- # Ignore all other calls to console.log
90
+ # Ignore all other calls to console.log that may come from the specs.
35
91
  #
36
92
  page.onConsoleMessage = (msg) ->
37
- console.log(RegExp.$1) if /^JasmineResult: ([\s\S]*)$/.test(msg)
93
+ console.log(RegExp.$1) if /^JSON_RESULT: ([\s\S]*)$/.test(msg)
38
94
 
39
95
  # Open web page and run the Jasmine test runner
40
96
  #
41
97
  page.open url, (status) ->
42
-
43
98
  if status isnt 'success'
44
-
45
- console.log "JasmineResult: #{ JSON.stringify { error: "Unable to access Jasmine specs at #{ url }" } }"
99
+ console.log "JSON_RESULT: #{ JSON.stringify({ error: "Unable to access Jasmine specs at #{ url }" }) }"
46
100
  phantom.exit()
47
-
48
101
  else
49
- # Wait until the Jasmine test is run
50
- waitFor ->
51
- page.evaluate ->
52
- if document.body.querySelector '.finished-at' then true else false
53
- , ->
54
- # Jasmine test runner has finished, extract the result from the DOM
55
- page.evaluate ->
56
-
57
- # JSON response to Guard::Jasmine
58
- result = {
59
- suites: []
60
- }
61
-
62
- # Extract runner stats from the HTML
63
- stats = /(\d+) specs, (\d+) failures? in (\d+.\d+)s/.exec document.body.querySelector('.description').innerText
64
-
65
- # Add stats to the result
66
- result['stats'] = {
67
- specs: parseInt stats[1]
68
- failures: parseInt stats[2]
69
- time: parseFloat stats[3]
70
- }
71
-
72
- # Extract failed suites
73
- for failedSuite in document.body.querySelectorAll 'div.jasmine_reporter > div.suite.failed'
74
- description = failedSuite.querySelector('a.description')
75
-
76
- # Add suite information to the result
77
- suite = {
78
- description: description.innerText
79
- filter: description.getAttribute('href')
80
- specs: []
81
- }
82
-
83
- # Collect information about each **failing** spec
84
- for failedSpec in failedSuite.querySelectorAll 'div.spec.failed'
85
- spec = {
86
- description: failedSpec.querySelector('a.description').getAttribute 'title'
87
- error_message: failedSpec.querySelector('div.messages div.resultMessage').innerText
88
- }
89
- suite['specs'].push spec
90
-
91
- result['suites'].push suite
92
-
93
- # Write result as JSON string that is parsed by Guard::Jasmine
94
- console.log "JasmineResult: #{ JSON.stringify result, undefined, 2 }"
95
-
96
- phantom.exit()
102
+ waitFor specsReady, extractResult
@@ -1,3 +1,5 @@
1
+ # coding: utf-8
2
+
1
3
  require 'multi_json'
2
4
 
3
5
  module Guard
@@ -18,30 +20,50 @@ module Guard
18
20
  # @option options [String] :phantomjs_bin the location of the PhantomJS binary
19
21
  # @option options [Boolean] :notification show notifications
20
22
  # @option options [Boolean] :hide_success hide success message notification
21
- # @return [Array<Object>] the result for each suite
23
+ # @return [Boolean, Array<String>] the status of the run and the failed files
22
24
  #
23
25
  def run(paths, options = { })
24
- return false if paths.empty?
26
+ return [false, []] if paths.empty?
25
27
 
26
28
  message = options[:message] || (paths == ['spec/javascripts'] ? 'Run all specs' : "Run specs #{ paths.join(' ') }")
27
29
  UI.info message, :reset => true
28
30
 
29
- paths.inject([]) do |results, file|
30
- results << evaluate_result(run_jasmine_spec(file, options), options)
31
+ results = paths.inject([]) do |results, file|
32
+ results << evaluate_result(run_jasmine_spec(file, options), file, options)
31
33
 
32
34
  results
33
35
  end.compact
36
+
37
+ [response_status_for(results), failed_paths_from(results)]
34
38
  end
35
39
 
36
40
  private
37
41
 
42
+ # Returns the failed spec file names.
43
+ #
44
+ # @param [Array<Object>] results the spec runner results
45
+ # @return [Array<String>] the list of failed spec files
46
+ #
47
+ def failed_paths_from(results)
48
+ results.map { |r| !r['passed'] ? r['file']: nil }.compact
49
+ end
50
+
51
+ # Returns the response status for the given result set.
52
+ #
53
+ # @param [Array<Object>] results the spec runner results
54
+ # @return [Boolean] whether it has passed or not
55
+ #
56
+ def response_status_for(results)
57
+ results.none? { |r| r.has_key?('error') || !r['passed'] }
58
+ end
59
+
38
60
  # Run the Jasmine spec by executing the PhantomJS script.
39
61
  #
40
62
  # @param [String] path the path of the spec
41
63
  #
42
64
  def run_jasmine_spec(file, options)
43
65
  suite = jasmine_suite(file, options)
44
- Formatter.info("Run Jasmine tests: #{ suite }")
66
+ Formatter.info("Run Jasmine tests at #{ suite }")
45
67
  IO.popen(phantomjs_command(options) + ' ' + suite)
46
68
  end
47
69
 
@@ -101,16 +123,18 @@ module Guard
101
123
  # actions.
102
124
  #
103
125
  # @param [String] output the JSON output the spec run
126
+ # @param [String] file the file name of the spec
104
127
  # @param [Hash] options the options for the execution
105
128
  # @return [Hash] the suite result
106
129
  #
107
- def evaluate_result(output, options)
130
+ def evaluate_result(output, file, options)
108
131
  result = MultiJson.decode(output.read)
109
132
  output.close
110
133
 
111
134
  if result['error']
112
135
  notify_runtime_error(result, options)
113
136
  else
137
+ result['file'] = file
114
138
  notify_spec_result(result, options)
115
139
  end
116
140
 
@@ -144,39 +168,38 @@ module Guard
144
168
  time = result['stats']['time']
145
169
  plural = failures == 1 ? '' : 's'
146
170
 
147
- message = "Jasmine ran #{ specs } specs, #{ failures } failure#{ plural } in #{ time }s."
171
+ message = "#{ specs } specs, #{ failures } failure#{ plural }\nin #{ time } seconds"
148
172
 
149
173
  if failures != 0
150
- notify_spec_failures(result, message, options)
174
+ notify_specdoc(result, message, options)
151
175
  else
152
176
  Formatter.success(message)
153
- Formatter.notify(message, :title => 'Jasmine results') if options[:notification] && !options[:hide_success]
177
+ Formatter.notify(message, :title => 'Jasmine specs passed') if options[:notification] && !options[:hide_success]
154
178
  end
155
179
  end
156
180
 
157
- # Notification about spec failures. This combines the suite
158
- # error messages into a single notification.
181
+ # Specdoc like formatting of the result.
159
182
  #
160
183
  # @param [Hash] result the suite result
161
184
  # @param [String] stats the status information
162
- # @param [Hash] options the options for the execution
163
- # @option options [Boolean] :notification show notifications
185
+ # @option options [Boolean] :hide_success hide success message notification
164
186
  #
165
- def notify_spec_failures(result, stats, options)
166
- messages = result['suites'].inject('') do |messages, suite|
187
+ def notify_specdoc(result, stats, options)
188
+ result['suites'].each do |suite|
189
+ Formatter.suite_name("➥ #{ suite['description'] }")
190
+
167
191
  suite['specs'].each do |spec|
168
- messages << "Spec '#{ spec['description'] }' failed with '#{ spec['error_message'] }'!\n"
192
+ if spec['passed']
193
+ Formatter.success(" ✔ #{ spec['description'] }") if !options[:hide_success]
194
+ else
195
+ Formatter.spec_failed(" ✘ #{ spec['description'] } ➤ #{ spec['error_message'] }")
196
+ Formatter.notify(stats, :title => "#{ spec['description'] }: #{ spec['error_message'] }", :image => :failed, :priority => 2) if options[:notification]
197
+ end
169
198
  end
170
-
171
- messages
172
199
  end
173
200
 
174
- messages << stats
175
-
176
- Formatter.error(messages)
177
- Formatter.notify(messages, :title => 'Jasmine results', :image => :failed, :priority => 2) if options[:notification]
201
+ Formatter.info(stats)
178
202
  end
179
-
180
203
  end
181
204
  end
182
205
  end
@@ -1,6 +1,6 @@
1
1
  module Guard
2
2
  module JasmineVersion
3
3
  # Guard::Jasmine version that is used for the Gem specification
4
- VERSION = '0.2.2'
4
+ VERSION = '0.3.0'
5
5
  end
6
6
  end
metadata CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 2
10
- version: 0.2.2
8
+ - 3
9
+ - 0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Michael Kessler