guard-jasmine 0.2.2 → 0.3.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.
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