guard-jasmine 1.19.2 → 2.0.0beta1

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -234
  3. data/lib/generators/guard_jasmine/install_generator.rb +17 -0
  4. data/lib/generators/guard_jasmine/templates/Guardfile +9 -0
  5. data/lib/guard/jasmine.rb +18 -15
  6. data/lib/guard/jasmine/cli.rb +5 -5
  7. data/lib/guard/jasmine/formatter.rb +10 -0
  8. data/lib/guard/jasmine/inspector.rb +1 -2
  9. data/lib/guard/jasmine/phantomjs/guard-jasmine.js +54 -180
  10. data/lib/guard/jasmine/phantomjs/guard-reporter.js +187 -0
  11. data/lib/guard/jasmine/phantomjs/src/guard-jasmine.coffee +101 -0
  12. data/lib/guard/jasmine/phantomjs/src/guard-reporter.coffee +109 -0
  13. data/lib/guard/jasmine/phantomjs/test/guard-reporter_spec.coffee +41 -0
  14. data/lib/guard/jasmine/runner.rb +178 -268
  15. data/lib/guard/jasmine/server.rb +17 -3
  16. data/lib/guard/jasmine/util.rb +1 -7
  17. data/lib/guard/jasmine/version.rb +1 -1
  18. metadata +135 -26
  19. data/lib/guard/jasmine/phantomjs/guard-jasmine.coffee +0 -193
  20. data/lib/guard/jasmine/phantomjs/lib/console.js +0 -188
  21. data/lib/guard/jasmine/phantomjs/lib/junit_reporter.js +0 -224
  22. data/lib/guard/jasmine/phantomjs/lib/reporter.js +0 -144
  23. data/lib/guard/jasmine/phantomjs/lib/result.js +0 -155
  24. data/lib/guard/jasmine/phantomjs/src/console.coffee +0 -149
  25. data/lib/guard/jasmine/phantomjs/src/reporter.coffee +0 -139
  26. data/lib/guard/jasmine/phantomjs/src/result.coffee +0 -95
  27. data/lib/guard/jasmine/phantomjs/test/console_spec.coffee +0 -125
  28. data/lib/guard/jasmine/phantomjs/test/reporter_spec.coffee +0 -0
  29. data/lib/guard/jasmine/phantomjs/test/result_spec.coffee +0 -311
@@ -0,0 +1,101 @@
1
+
2
+ # Set default values
3
+ options =
4
+ url: phantom.args[0] || 'http://127.0.0.1:3000/jasmine'
5
+ timeout: parseInt(phantom.args[1] || 10000)
6
+
7
+ # Create the web page.
8
+ page = require('webpage').create()
9
+
10
+ # Catch JavaScript errors
11
+ # abort the request and return the error
12
+ page.onError = (message, trace) ->
13
+ reportError "Javascript error encountered on Jasmine test page: #{ message }", trace
14
+
15
+ # Once the page is initialized, setup the script for
16
+ # the GuardReporter class
17
+ page.onInitialized = ->
18
+ page.injectJs 'guard-reporter.js'
19
+ page.evaluate ->
20
+ window.onload = ->
21
+ window.reporter = new GuardReporter()
22
+ window.jasmine.getEnv().addReporter(window.reporter) if window.jasmine
23
+
24
+ # Once the page is finished loading
25
+ page.onLoadFinished = (status)->
26
+ if status isnt 'success'
27
+ reportError "Unable to access Jasmine specs at #{ options.url }, page returned status: #{status}"
28
+ else
29
+ waitFor reporterReady, jasmineAvailable, options.timeout, reporterMissing
30
+
31
+ # Open web page, which will kick off the Jasmine test runner
32
+ page.open options.url
33
+
34
+ # Test if Jasmine and guard has been loaded
35
+ reporterReady = ->
36
+ page.evaluate ->
37
+ window.jasmine && window.reporter
38
+
39
+ # Start specs after they are have been loaded
40
+ jasmineAvailable = ->
41
+ waitFor specsDone, exitSuccessfully, options.timeout, specsTimedout
42
+
43
+ # Error message for when jasmine never loaded asynchronously
44
+ reporterMissing = ->
45
+ text = page.evaluate -> document.getElementsByTagName('body')[0]?.innerText
46
+ reportError """
47
+ The reporter is not available!
48
+ Perhaps the url ( #{ options.url } ) is incorrect?
49
+
50
+ #{ text }
51
+ """
52
+
53
+ # tests if the resultComplete flag is set on the reporter
54
+ specsDone = ->
55
+ result = page.evaluate ->
56
+ window.reporter.resultComplete
57
+
58
+ # We should end up here. Logs the results as JSON and exits
59
+ exitSuccessfully = ->
60
+ results = page.evaluate -> window.reporter.results()
61
+ console.log JSON.stringify( results )
62
+ phantom.exit()
63
+
64
+
65
+ # Error message for when specs time out
66
+ specsTimedout = ->
67
+ text = page.evaluate -> document.getElementsByTagName('body')[0]?.innerText
68
+ reportError """
69
+ Timeout waiting for the Jasmine test results!
70
+
71
+ #{ text }
72
+ """
73
+
74
+ # Wait until the test condition is true or a timeout occurs.
75
+ #
76
+ # @param [Function] test the test that returns true if condition is met
77
+ # @param [Function] ready the action when the condition is fulfilled
78
+ # @param [Number] timeout the max amount of time to wait in milliseconds
79
+ #
80
+ waitFor = (test, ready, timeout = 10000, timeoutFunction)->
81
+ condition = false
82
+ interval = undefined
83
+ start = Date.now(0)
84
+ wait = ->
85
+ if !condition && (Date.now() - start < timeout)
86
+ condition = test()
87
+ else
88
+ clearInterval interval
89
+ if condition
90
+ ready()
91
+ else
92
+ timeoutFunction()
93
+ interval = setInterval( wait, 250 )
94
+
95
+ # Logs the error to the console as JSON and exits with status '1'
96
+ reportError = (msg, trace=[])->
97
+ if 0 == trace.length
98
+ err = new Error();
99
+ trace = err.stack
100
+ console.log JSON.stringify({ error: msg, trace: trace })
101
+ phantom.exit(1)
@@ -0,0 +1,109 @@
1
+ # Capture statements that were logged to the console
2
+ # during spec execution.
3
+ #
4
+ # To do so it substitues it's own functions for the console.<levels>
5
+ #
6
+ extendObject = (a, b)->
7
+ for key,value of b
8
+ a[key] = value if b.hasOwnProperty(key)
9
+ return a
10
+
11
+
12
+ class ConsoleCapture
13
+ # Instead of attempting to de-activate the console dot reporter in hacky ways,
14
+ # just ignore it's output
15
+ @DOT_REPORTER_MATCH = /\[\d+m[F.]..0m/
16
+ @levels: ['log','info','warn','error','debug' ]
17
+ @original = console
18
+
19
+ @original_levels = {}
20
+ @original_levels[level] = console[level] for level in ConsoleCapture.levels
21
+
22
+ constructor:->
23
+ @original = {}
24
+ @captured = []
25
+ this._reassign_level( level ) for level in ConsoleCapture.levels
26
+
27
+ revert: ->
28
+ for level in ConsoleCapture.levels
29
+ ConsoleCapture.original[level] = ConsoleCapture.original_levels[level]
30
+
31
+ _reassign_level: ( level )->
32
+ my = this
33
+ console[level] = ->
34
+ args = Array.prototype.slice.call(arguments, 0)
35
+ return if args[0] && args[0].toString && args[0].toString().match( ConsoleCapture.DOT_REPORTER_MATCH )
36
+ my.captured.push( [ level ].concat( args ) )
37
+ ConsoleCapture.original_levels[ level ].apply( ConsoleCapture.original, arguments )
38
+
39
+
40
+ # Implements a Jasmine reporter
41
+ class GuardReporter
42
+ @STACK_MATCHER=new RegExp("__spec__\/(.*):([0-9]+)","g")
43
+
44
+ jasmineStarted: ->
45
+ @console = new ConsoleCapture();
46
+ @startedAt = Date.now()
47
+ @currentSuite = { suites: [] }
48
+ @stack = [ @currentSuite ]
49
+
50
+ suiteStarted: (suite)->
51
+ suite = extendObject({ specs: [], suites: [] }, suite )
52
+ @currentSuite.suites.push( suite )
53
+ @currentSuite = suite
54
+ @stack.push(suite)
55
+
56
+ suiteDone: (Suite)->
57
+ @stack.pop()
58
+ @currentSuite = @stack[@stack.length-1]
59
+
60
+ jasmineDone: ->
61
+ @resultComplete = true
62
+
63
+ specDone: (spec)->
64
+ @resultReceived = true
65
+ spec = extendObject({ logs: @console.captured, errors: [] }, spec )
66
+ for failure in spec.failedExpectations
67
+ error = extendObject({trace:[]}, failure )
68
+ while match = GuardReporter.STACK_MATCHER.exec( failure.stack )
69
+ error.trace.push({ file: match[1], line: parseInt(match[2]) })
70
+ delete error.stack
71
+ spec.errors.push( error )
72
+ delete spec.failedExpectations
73
+ @currentSuite.specs.push( spec )
74
+
75
+ this.resetConsoleLog()
76
+ spec
77
+
78
+ resetConsoleLog: ->
79
+ @console.revert()
80
+ @console = new ConsoleCapture
81
+
82
+ eachSuite: (suite)->
83
+ suites = [].concat( suite.suites )
84
+ for suite in suite.suites
85
+ suites = suites.concat( this.eachSuite(suite) )
86
+ suites
87
+
88
+ results: ->
89
+ stats = {
90
+ time : ( Date.now() - @startedAt ) / 1000
91
+ specs : 0
92
+ failed : 0
93
+ pending : 0
94
+ disabled : 0
95
+ }
96
+ for suite in this.eachSuite(@stack[0])
97
+ stats.specs += suite.specs.length
98
+ for spec in suite.specs
99
+ stats[spec.status] += 1 unless undefined == stats[spec.status]
100
+ {
101
+ jasmine_version: jasmine?.version
102
+ stats: stats
103
+ suites: @stack[0].suites
104
+ }
105
+
106
+ if typeof module isnt 'undefined' and module.exports
107
+ module.exports = GuardReporter
108
+ else
109
+ window.GuardReporter = GuardReporter
@@ -0,0 +1,41 @@
1
+ sinon = require 'sinon'
2
+ {expect} = require 'chai'
3
+
4
+ GuardReporter = require '../src/guard-reporter'
5
+
6
+ describe 'Reporter', ->
7
+ beforeEach ->
8
+ @reporter = new GuardReporter
9
+ @reporter.jasmineStarted()
10
+
11
+ it 'captures the console', ->
12
+ @reporter.suiteStarted({name:'Blank'})
13
+ console.log("A %s Logging", "Test")
14
+ console.warn("This is your last warning")
15
+ @reporter.specDone( { failedExpectations: [] } )
16
+ expect( @reporter.results() )
17
+ .to.have.deep.property('.suites[0].specs[0].logs')
18
+ .and.equal([
19
+ [ 'log', 'A %s Logging', "Test" ],
20
+ [ 'warn', 'This is your last warning' ]
21
+ ])
22
+
23
+ it "reports counts", ->
24
+ @reporter.suiteStarted({name:'Blank'})
25
+ console.log("A %s Logging", "Test")
26
+ console.warn("This is your last warning")
27
+ @reporter.specDone( { status: 'passed', failedExpectations: [] } )
28
+ @reporter.specDone( { status: 'failed', failedExpectations: [{
29
+ matcherName:"toEqual",
30
+ message: "Expected 2 to equal 5"
31
+ }] })
32
+ @reporter.specDone( { status: 'passed', failedExpectations: [] } )
33
+ @reporter.specDone( { status: 'pending', failedExpectations: [] } )
34
+ results = @reporter.results()
35
+ expect( results ).to.have.property('stats')
36
+ expect( results.stats )
37
+ .to.have.property('specs').and.equal(4)
38
+ expect( results.stats )
39
+ .to.have.property('failed').and.equal(1)
40
+ expect( results.stats )
41
+ .to.have.property('pending').and.equal(1)
@@ -11,53 +11,68 @@ module Guard
11
11
  # evaluates the JSON response from the PhantomJS Script `guard_jasmine.coffee`,
12
12
  # writes the result to the console and triggers optional system notifications.
13
13
  #
14
- module Runner
15
- extend ::Guard::Jasmine::Util
16
-
17
- class << self
18
-
19
- # Name of the coverage threshold options
20
- THRESHOLDS = [:statements_threshold, :functions_threshold, :branches_threshold, :lines_threshold]
21
-
22
- # Run the supplied specs.
23
- #
24
- # @param [Array<String>] paths the spec files or directories
25
- # @param [Hash] options the options for the execution
26
- # @option options [String] :jasmine_url the url of the Jasmine test runner
27
- # @option options [String] :phantomjs_bin the location of the PhantomJS binary
28
- # @option options [Integer] :timeout the maximum time in seconds to wait for the spec runner to finish
29
- # @option options [String] :rackup_config custom rackup config to use
30
- # @option options [Boolean] :notification show notifications
31
- # @option options [Boolean] :hide_success hide success message notification
32
- # @option options [Integer] :max_error_notify maximum error notifications to show
33
- # @option options [Symbol] :specdoc options for the specdoc output, either :always, :never
34
- # @option options [Symbol] :console options for the console.log output, either :always, :never or :failure
35
- # @option options [String] :spec_dir the directory with the Jasmine specs
36
- # @return [Boolean, Array<String>] the status of the run and the failed files
37
- #
38
- def run(paths, options = { })
39
- return [false, []] if paths.empty?
14
+ class Runner
15
+ include ::Guard::Jasmine::Util
16
+
17
+ attr_reader :options
18
+
19
+ # Name of the coverage threshold options
20
+ THRESHOLDS = [:statements_threshold, :functions_threshold, :branches_threshold, :lines_threshold]
21
+
22
+ # Run the supplied specs.
23
+ #
24
+ # @param [Hash] options the options for the execution
25
+ # @option options [String] :jasmine_url the url of the Jasmine test runner
26
+ # @option options [String] :phantomjs_bin the location of the PhantomJS binary
27
+ # @option options [Integer] :timeout the maximum time in seconds to wait for the spec runner to finish
28
+ # @option options [String] :rackup_config custom rackup config to use
29
+ # @option options [Boolean] :notification show notifications
30
+ # @option options [Boolean] :hide_success hide success message notification
31
+ # @option options [Integer] :max_error_notify maximum error notifications to show
32
+ # @option options [Symbol] :specdoc options for the specdoc output, either :always, :never
33
+ # @option options [Symbol] :console options for the console.log output, either :always, :never or :failure
34
+ # @option options [String] :spec_dir the directory with the Jasmine specs
35
+ # @option options [Hash] :query_params Parameters to pass along with the request
36
+ # @option options [Boolean] :debug display raw JSON output from the runner
37
+ #
38
+ def initialize(options)
39
+ @options = options
40
+ end
40
41
 
41
- notify_start_message(paths, options)
42
+ # @param [Array<String>] paths the spec files or directories
43
+ # @return [Hash<String,Array>] keys for the spec_file, value is array of failure messages.
44
+ # Only specs with failures will be returned. Therefore an empty? return hash indicates success.
45
+ def run(paths, per_run_options = {})
46
+ previous_options = @options
47
+ @options.merge!( per_run_options )
42
48
 
43
- results = paths.inject([]) do |results, file|
44
- results << evaluate_response(run_jasmine_spec(file, options), file, options) if File.exist?(file_and_line_number_parts(file)[0])
49
+ return {} if paths.empty?
45
50
 
46
- results
47
- end.compact
51
+ notify_start_message(paths)
48
52
 
49
- [response_status_for(results), failed_paths_from(results)]
53
+ run_results = paths.each_with_object({}) do |file, results|
54
+ if File.exist?(file_and_line_number_parts(file)[0])
55
+ results[file] = evaluate_response(run_jasmine_spec(file), file)
56
+ end
57
+ end
58
+ # return the errors
59
+ return run_results.each_with_object({}) do | spec_run, hash |
60
+ file, r = spec_run
61
+ errors = collect_spec_errors(r['suites']||[])
62
+ errors.push( r['error'] ) if r.has_key? 'error'
63
+ hash[file] = errors unless errors.empty?
50
64
  end
65
+ ensure
66
+ @options=previous_options
67
+ end
51
68
 
52
- private
69
+ private
53
70
 
54
- # Shows a notification in the console that the runner starts.
55
- #
71
+ # Shows a notification in the console that the runner starts.
72
+ #
56
73
  # @param [Array<String>] paths the spec files or directories
57
- # @param [Hash] options the options for the execution
58
- # @option options [String] :spec_dir the directory with the Jasmine specs
59
74
  #
60
- def notify_start_message(paths, options)
75
+ def notify_start_message(paths)
61
76
  message = if paths == [options[:spec_dir]]
62
77
  'Run all Jasmine suites'
63
78
  else
@@ -67,33 +82,12 @@ module Guard
67
82
  Formatter.info(message, reset: true)
68
83
  end
69
84
 
70
- # Returns the failed spec file names.
71
- #
72
- # @param [Array<Object>] results the spec runner results
73
- # @return [Array<String>] the list of failed spec files
74
- #
75
- def failed_paths_from(results)
76
- results.map { |r| !r['passed'] ? r['file'] : nil }.compact
77
- end
78
-
79
- # Returns the response status for the given result set.
80
- #
81
- # @param [Array<Object>] results the spec runner results
82
- # @return [Boolean] whether it has passed or not
83
- #
84
- def response_status_for(results)
85
- results.none? { |r| r.has_key?('error') || !r['passed'] }
86
- end
87
-
88
85
  # Run the Jasmine spec by executing the PhantomJS script.
89
86
  #
90
87
  # @param [String] file the path of the spec
91
- # @param [Hash] options the options for the execution
92
- # @option options [Integer] :timeout the maximum time in seconds to wait for the spec runner to finish
93
88
  #
94
- def run_jasmine_spec(file, options)
95
- suite = jasmine_suite(file, options)
96
- Formatter.info("Run Jasmine suite at #{ suite }")
89
+ def run_jasmine_spec(file)
90
+ suite = jasmine_suite(file)
97
91
 
98
92
  arguments = [
99
93
  options[:timeout] * 1000,
@@ -105,30 +99,27 @@ module Guard
105
99
  options[:junit_consolidate],
106
100
  "'#{ options[:junit_save_path] }'"
107
101
  ]
108
-
109
- IO.popen("#{ phantomjs_command(options) } \"#{ suite }\" #{ arguments.collect { |i| i.to_s }.join(' ')}", 'r:UTF-8')
102
+ cmd = "#{ phantomjs_command } \"#{ suite }\" #{ arguments.collect { |i| i.to_s }.join(' ')}"
103
+ IO.popen(cmd, 'r:UTF-8')
110
104
  end
111
105
 
112
106
  # Get the PhantomJS binary and script to execute.
113
107
  #
114
- # @param [Hash] options the options for the execution
115
- # @option options [String] :phantomjs_bin the location of the PhantomJS binary
116
108
  # @return [String] the command
117
109
  #
118
- def phantomjs_command(options)
110
+ def phantomjs_command
119
111
  options[:phantomjs_bin] + ' ' + phantomjs_script
112
+ #options[:phantomjs_bin] + ' --remote-debugger-port=9000 ' + phantomjs_script
120
113
  end
121
114
 
122
115
  # Get the Jasmine test runner URL with the appended suite name
123
116
  # that acts as the spec filter.
124
117
  #
125
118
  # @param [String] file the spec file
126
- # @param [Hash] options the options for the execution
127
- # @option options [String] :jasmine_url the url of the Jasmine test runner
128
119
  # @return [String] the Jasmine url
129
120
  #
130
- def jasmine_suite(file, options)
131
- options[:jasmine_url] + query_string_for_suite(file, options)
121
+ def jasmine_suite(file)
122
+ options[:jasmine_url] + query_string_for_suite(file)
132
123
  end
133
124
 
134
125
  # Get the PhantomJS script that executes the spec and extracts
@@ -144,22 +135,17 @@ module Guard
144
135
  # will be run.
145
136
  #
146
137
  # @param [String] file the spec file
147
- # @param [Hash] options the options for the execution
148
- # @option options [String] :spec_dir the directory with the Jasmine specs
149
138
  # @return [String] the suite name
150
139
  #
151
- def query_string_for_suite(file, options)
152
- return '' if file == options[:spec_dir]
153
-
154
- query_string = query_string_for_suite_from_line_number(file, options)
155
-
156
- unless query_string
157
- query_string = query_string_for_suite_from_first_describe(file, options)
140
+ def query_string_for_suite(file)
141
+ params = {}
142
+ if options[:query_params]
143
+ params.merge!(options[:query_params])
158
144
  end
159
-
160
- query_string = query_string ? "?spec=#{ query_string }" : ''
161
-
162
- URI.encode(query_string)
145
+ if file != options[:spec_dir]
146
+ params[:spec] = suite_from_line_number(file) || suite_from_first_describe(file)
147
+ end
148
+ params.empty? ? "" : "?"+URI.encode_www_form(params)
163
149
  end
164
150
 
165
151
  # When providing a line number by either the option or by
@@ -167,11 +153,9 @@ module Guard
167
153
  # fromt the corresponding line number in the file.
168
154
  #
169
155
  # @param [String] file the spec file
170
- # @param [Hash] options the options for the execution
171
- # @option options [Fixnum] :line_number the line number to run
172
156
  # @return [String] the suite name
173
157
  #
174
- def query_string_for_suite_from_line_number(file, options)
158
+ def suite_from_line_number(file)
175
159
  file_name, line_number = file_and_line_number_parts(file)
176
160
  line_number ||= options[:line_number]
177
161
 
@@ -196,12 +180,11 @@ module Guard
196
180
  # found.
197
181
  #
198
182
  # @param [String] file the spec file
199
- # @param [Hash] options the options for the execution
200
183
  # @return [String] the suite name
201
184
  #
202
- def query_string_for_suite_from_first_describe(file, options)
185
+ def suite_from_first_describe(file)
203
186
  File.foreach(file) do |line|
204
- if line =~ /describe\s*[("']+(.*?)["')]+/
187
+ if line =~ /describe\s*[("']+(.*?)["')]+/ #'
205
188
  return $1
206
189
  end
207
190
  end
@@ -241,7 +224,7 @@ module Guard
241
224
  # @return [String] the extracted title
242
225
  #
243
226
  def spec_title(line)
244
- line[/['"](.+?)['"]/, 1]
227
+ line[/['"](.+?)["']/, 1]
245
228
  end
246
229
 
247
230
  # Evaluates the JSON response that the PhantomJS script
@@ -250,33 +233,31 @@ module Guard
250
233
  #
251
234
  # @param [String] output the JSON output the spec run
252
235
  # @param [String] file the file name of the spec
253
- # @param [Hash] options the options for the execution
254
- # @return [Hash] the suite result
236
+ # @return [Hash] results of the suite's specs
255
237
  #
256
- def evaluate_response(output, file, options)
238
+ def evaluate_response(output, file)
257
239
  json = output.read
258
240
  json = json.encode('UTF-8') if json.respond_to?(:encode)
259
-
260
241
  begin
261
242
  result = MultiJson.decode(json, { max_nesting: false })
262
243
  raise 'No response from Jasmine runner' if !result && options[:is_cli]
263
-
244
+ pp result if options[:debug]
264
245
  if result['error']
265
246
  if options[:is_cli]
266
247
  raise 'An error occurred in the Jasmine runner'
267
248
  else
268
- notify_runtime_error(result, options)
249
+ notify_runtime_error(result)
269
250
  end
270
251
  elsif result
271
252
  result['file'] = file
272
- notify_spec_result(result, options)
253
+ notify_spec_result(result)
273
254
  end
274
255
 
275
256
  if result && result['coverage'] && options[:coverage]
276
- notify_coverage_result(result['coverage'], file, options)
257
+ notify_coverage_result(result['coverage'], file)
277
258
  end
278
259
 
279
- result
260
+ return result
280
261
 
281
262
  rescue MultiJson::DecodeError => e
282
263
  if e.data == ''
@@ -287,10 +268,11 @@ module Guard
287
268
  end
288
269
  else
289
270
  if options[:is_cli]
290
- raise 'Cannot decode JSON from PhantomJS runner'
271
+ raise "Cannot decode JSON from PhantomJS runner, message received was:\n#{json}"
291
272
  else
292
273
  Formatter.error("Cannot decode JSON from PhantomJS runner: #{ e.message }")
293
274
  Formatter.error("JSON response: #{ e.data }")
275
+ Formatter.error("message received was:\n#{json}")
294
276
  end
295
277
  end
296
278
  ensure
@@ -302,12 +284,11 @@ module Guard
302
284
  # prohibits the execution of the Jasmine spec.
303
285
  #
304
286
  # @param [Hash] result the suite result
305
- # @param [Hash] options the options for the execution
306
- # @option options [Boolean] :notification show notifications
307
287
  #
308
- def notify_runtime_error(result, options)
288
+ def notify_runtime_error(result)
309
289
  message = "An error occurred: #{ result['error'] }"
310
- Formatter.error(message)
290
+ Formatter.error(message )
291
+ Formatter.error( result['trace'] ) if result['trace']
311
292
  Formatter.notify(message, title: 'Jasmine error', image: :failed, priority: 2) if options[:notification]
312
293
  end
313
294
 
@@ -315,62 +296,59 @@ module Guard
315
296
  # and some stats.
316
297
  #
317
298
  # @param [Hash] result the suite result
318
- # @param [Hash] options the options for the execution
319
- # @option options [Boolean] :notification show notifications
320
- # @option options [Boolean] :hide_success hide success message notification
321
299
  #
322
- def notify_spec_result(result, options)
323
- specs = result['stats']['specs']
324
- failures = result['stats']['failures']
325
- time = result['stats']['time']
326
- specs_plural = specs == 1 ? '' : 's'
327
- failures_plural = failures == 1 ? '' : 's'
328
-
329
- Formatter.info("\nFinished in #{ time } seconds")
330
-
331
- message = "#{ specs } spec#{ specs_plural }, #{ failures } failure#{ failures_plural }"
300
+ def notify_spec_result(result)
301
+ specs = result['stats']['specs'] - result['stats']['disabled']
302
+ failed = result['stats']['failed']
303
+ time = sprintf( '%0.2f', result['stats']['time'] )
304
+ specs_plural = specs == 1 ? '' : 's'
305
+ failed_plural = failed == 1 ? '' : 's'
306
+ Formatter.info("Finished in #{ time } seconds")
307
+ pending = result['stats']['pending'].to_i > 0 ? " #{result['stats']['pending']} pending," : ""
308
+ message = "#{ specs } spec#{ specs_plural },#{pending} #{ failed } failure#{ failed_plural }"
332
309
  full_message = "#{ message }\nin #{ time } seconds"
333
- passed = failures == 0
310
+ passed = failed == 0
311
+
312
+ report_specdoc(result, passed) if specdoc_shown?(passed)
334
313
 
335
314
  if passed
336
- report_specdoc(result, passed, options)
337
315
  Formatter.success(message)
338
316
  Formatter.notify(full_message, title: 'Jasmine suite passed') if options[:notification] && !options[:hide_success]
339
317
  else
340
- report_specdoc(result, passed, options)
318
+ errors = collect_spec_errors(result['suites'])
319
+ error_message = errors[0..options[:max_error_notify]].join("\n")
320
+
341
321
  Formatter.error(message)
342
- notify_errors(result, options)
343
- Formatter.notify(full_message, title: 'Jasmine suite failed', image: :failed, priority: 2) if options[:notification]
322
+ if options[:notification]
323
+ Formatter.notify( "#{error_message}\n#{full_message}",
324
+ title: 'Jasmine suite failed', image: :failed, priority: 2)
325
+ end
344
326
  end
345
327
 
346
- Formatter.info("Done.\n")
347
328
  end
348
329
 
349
- # Notification about the coverage of a spec run, success or failure,
330
+ # Notification about the coverage of a spec run, success or failed,
350
331
  # and some stats.
351
332
  #
352
333
  # @param [Hash] coverage the coverage hash from the JSON
353
334
  # @param [String] file the file name of the spec
354
- # @param [Hash] options the options for the execution
355
- # @option options [Boolean] :notification show notifications
356
- # @option options [Boolean] :hide_success hide success message notification
357
335
  #
358
- def notify_coverage_result(coverage, file, options)
336
+ def notify_coverage_result(coverage, file)
359
337
  if coverage_bin
360
338
  FileUtils.mkdir_p(coverage_root) unless File.exist?(coverage_root)
361
339
 
362
- update_coverage(coverage, file, options)
340
+ update_coverage(coverage, file)
363
341
 
364
342
  if options[:coverage_summary]
365
343
  generate_summary_report
366
344
  else
367
- generate_text_report(file, options)
345
+ generate_text_report(file)
368
346
  end
369
347
 
370
- check_coverage(options)
348
+ check_coverage
371
349
 
372
350
  if options[:coverage_html]
373
- generate_html_report(options)
351
+ generate_html_report
374
352
  end
375
353
  else
376
354
  Formatter.error('Skipping coverage report: unable to locate istanbul in your PATH')
@@ -381,9 +359,8 @@ module Guard
381
359
  # last coverage run.
382
360
  #
383
361
  # @param [String] file the file name of the spec
384
- # @param [Hash] options the options for the execution
385
362
  #
386
- def generate_text_report(file, options)
363
+ def generate_text_report(file)
387
364
  Formatter.info 'Spec coverage details:'
388
365
 
389
366
  if file == options[:spec_dir]
@@ -405,11 +382,9 @@ module Guard
405
382
  # Uses the Istanbul text reported to output the result of the
406
383
  # last coverage run.
407
384
  #
408
- # @param [Hash] options the options for the coverage
409
- #
410
- def check_coverage(options)
411
- if any_coverage_threshold?(options)
412
- coverage = `#{coverage_bin} check-coverage #{ istanbul_coverage_options(options) } #{ coverage_file } 2>&1`
385
+ def check_coverage
386
+ if any_coverage_threshold?
387
+ coverage = `#{coverage_bin} check-coverage #{ istanbul_coverage_options } #{ coverage_file } 2>&1`
413
388
  coverage = coverage.split("\n").grep(/ERROR/).join.sub('ERROR:', '')
414
389
  failed = $? && $?.exitstatus != 0
415
390
 
@@ -426,10 +401,8 @@ module Guard
426
401
  # Uses the Istanbul text reported to output the result of the
427
402
  # last coverage run.
428
403
  #
429
- # @param [Hash] options for the HTML report
430
- #
431
- def generate_html_report(options)
432
- report_directory = coverage_report_directory(options)
404
+ def generate_html_report
405
+ report_directory = coverage_report_directory
433
406
  `#{coverage_bin} report --dir #{ report_directory } --root #{ coverage_root } html #{ coverage_file }`
434
407
  Formatter.info "Updated HTML report available at: #{ report_directory }/index.html"
435
408
  end
@@ -453,12 +426,10 @@ module Guard
453
426
  #
454
427
  # @param [Hash] result the suite result
455
428
  # @param [Boolean] passed status
456
- # @param [Hash] options the options
457
- # @option options [Symbol] :console options for the console.log output, either :always, :never or :failure
458
429
  #
459
- def report_specdoc(result, passed, options)
430
+ def report_specdoc(result, passed)
460
431
  result['suites'].each do |suite|
461
- report_specdoc_suite(suite, passed, options)
432
+ report_specdoc_suite(suite, passed)
462
433
  end
463
434
  end
464
435
 
@@ -466,128 +437,78 @@ module Guard
466
437
  #
467
438
  # @param [Hash] suite the suite
468
439
  # @param [Boolean] passed status
469
- # @param [Hash] options the options
470
- # @option options [Symbol] :console options for the console.log output, either :always, :never or :failure
471
- # @option options [Symbol] :focus options for focus on failures in the specdoc
472
440
  # @param [Number] level the indention level
473
441
  #
474
- def report_specdoc_suite(suite, passed, options, level = 0)
475
- # Print the suite description when the specdoc is shown or there are logs to display
476
- if specdoc_shown?(passed, options) || console_logs_shown?(suite, passed, options) || error_logs_shown?(suite, passed, options)
477
- Formatter.suite_name((' ' * level) + suite['description']) if passed || options[:focus] && contains_failed_spec?(suite)
478
- end
442
+ def report_specdoc_suite(suite, run_passed, level = 0)
443
+
444
+ # Print the suite description when the specdoc is shown or there are logs to display
445
+ Formatter.suite_name((' ' * level) + suite['description'])
479
446
 
480
447
  suite['specs'].each do |spec|
481
- if spec['passed']
482
- if passed || !options[:focus] || console_for_spec?(spec, options) || errors_for_spec?(spec, options)
483
- Formatter.success(indent(" ✔ #{ spec['description'] }", level)) if description_shown?(passed, spec, options)
484
- report_specdoc_errors(spec, options, level)
485
- report_specdoc_logs(spec, options, level)
486
- end
448
+ # Specs are shown if they failed, or if they passed and the "focus" option is falsey
449
+ # If specs are going to be shown, then pending are also shown
450
+ # If the focus option is set, then only failing tests are shown
451
+ next unless :always==options[:specdoc] || spec['status'] == 'failed' || ( !run_passed && !options[:focus] )
452
+ if spec['status'] == 'passed'
453
+ Formatter.success(indent(" ✔ #{ spec['description'] }", level))
454
+ elsif spec['status'] == 'failed'
455
+ Formatter.spec_failed(indent(" ✘ #{ spec['description'] }", level))
487
456
  else
488
- Formatter.spec_failed(indent(" #{ spec['description'] }", level)) if description_shown?(passed, spec, options)
489
- spec['messages'].each do |message|
490
- Formatter.spec_failed(indent(" ➤ #{ format_message(message, false) }", level)) if specdoc_shown?(passed, options)
491
- end
492
- report_specdoc_errors(spec, options, level)
493
- report_specdoc_logs(spec, options, level)
457
+ Formatter.spec_pending(indent(" #{ spec['description'] }", level))
494
458
  end
459
+ report_specdoc_errors(spec, level)
460
+ report_specdoc_logs(spec, level)
495
461
  end
496
462
 
497
- suite['suites'].each { |s| report_specdoc_suite(s, passed, options, level + 2) } if suite['suites']
463
+ suite['suites'].each { |s| report_specdoc_suite(s, run_passed, level + 2) } if suite['suites']
498
464
  end
499
465
 
500
466
  # Is the specdoc shown for this suite?
501
467
  #
502
468
  # @param [Boolean] passed the spec status
503
- # @param [Hash] options the options
504
469
  #
505
- def specdoc_shown?(passed, options = { })
470
+ def specdoc_shown?(passed)
506
471
  options[:specdoc] == :always || (options[:specdoc] == :failure && !passed)
507
472
  end
508
473
 
509
- # Are console logs shown for this suite?
510
- #
511
- # @param [Hash] suite the suite
512
- # @param [Boolean] passed the spec status
513
- # @param [Hash] options the options
514
- #
515
- def console_logs_shown?(suite, passed, options = { })
516
- # Are console messages displayed?
517
- console_enabled = options[:console] == :always || (options[:console] == :failure && !passed)
518
-
519
- # Are there any logs to display at all for this suite?
520
- logs_for_current_options = suite['specs'].select do |spec|
521
- spec['logs'] && (options[:console] == :always || (options[:console] == :failure && !spec['passed']))
522
- end
523
-
524
- any_logs_present = !logs_for_current_options.empty?
525
-
526
- console_enabled && any_logs_present
527
- end
528
474
 
529
475
  # Are console logs shown for this spec?
530
476
  #
531
477
  # @param [Hash] spec the spec
532
- # @param [Hash] options the options
533
478
  #
534
- def console_for_spec?(spec, options = { })
535
- spec['logs'] && ((spec['passed'] && options[:console] == :always) ||
536
- (!spec['passed'] && options[:console] != :never))
479
+ def console_for_spec?(spec)
480
+ spec['logs'] && (( spec['status'] == 'passed' && options[:console] == :always) ||
481
+ (spec['status'] == 'failed' && options[:console] != :never) )
537
482
  end
538
483
 
539
- # Are error logs shown for this suite?
540
- #
541
- # @param [Hash] suite the suite
542
- # @param [Boolean] passed the spec status
543
- # @param [Hash] options the options
544
- #
545
- def error_logs_shown?(suite, passed, options = { })
546
- # Are error messages displayed?
547
- errors_enabled = options[:errors] == :always || (options[:errors] == :failure && !passed)
548
-
549
- # Are there any errors to display at all for this suite?
550
- errors_for_current_options = suite['specs'].select do |spec|
551
- spec['errors'] && (options[:errors] == :always || (options[:errors] == :failure && !spec['passed']))
552
- end
553
-
554
- any_errors_present= !errors_for_current_options.empty?
555
-
556
- errors_enabled && any_errors_present
557
- end
558
484
 
559
485
  # Are errors shown for this spec?
560
486
  #
561
487
  # @param [Hash] spec the spec
562
- # @param [Hash] options the options
563
- def errors_for_spec?(spec, options = { })
564
- spec['errors'] && ((spec['passed'] && options[:errors] == :always) ||
565
- (!spec['passed'] && options[:errors] != :never))
488
+ def errors_for_spec?(spec)
489
+ spec['errors'] && ((spec['status']=='passed' && options[:errors] == :always) ||
490
+ (spec['status']=='failed' && options[:errors] != :never))
566
491
  end
567
492
 
568
493
  # Is the description shown for this spec?
569
494
  #
570
495
  # @param [Boolean] passed the spec status
571
496
  # @param [Hash] spec the spec
572
- # @param [Hash] options the options
573
497
  #
574
- def description_shown?(passed, spec, options = { })
575
- specdoc_shown?(passed, options) || console_for_spec?(spec, options) || errors_for_spec?(spec, options)
498
+ def description_shown?(passed, spec)
499
+ specdoc_shown?(passed) || console_for_spec?(spec) || errors_for_spec?(spec)
576
500
  end
577
501
 
578
502
  # Shows the logs for a given spec.
579
503
  #
580
504
  # @param [Hash] spec the spec result
581
- # @param [Hash] options the options
582
- # @option options [Symbol] :console options for the console.log output, either :always, :never or :failure
583
505
  # @param [Number] level the indention level
584
506
  #
585
- def report_specdoc_logs(spec, options, level)
586
- if spec['logs'] && (options[:console] == :always || (options[:console] == :failure && !spec['passed']))
587
- spec['logs'].each do |log|
588
- log.split("\n").each_with_index do |message, index|
589
- Formatter.info(indent(" #{ index == 0 ? '•' : ' ' } #{ message }", level))
590
- end
507
+ def report_specdoc_logs(spec, level)
508
+ if console_for_spec?(spec)
509
+ spec['logs'].each do |log_level, message|
510
+ log_level = log_level == 'log' ? '' : "#{log_level.upcase}: "
511
+ Formatter.info(indent(" #{log_level}#{ message }", level))
591
512
  end
592
513
  end
593
514
  end
@@ -595,19 +516,16 @@ module Guard
595
516
  # Shows the errors for a given spec.
596
517
  #
597
518
  # @param [Hash] spec the spec result
598
- # @param [Hash] options the options
599
- # @option options [Symbol] :errors options for the errors output, either :always, :never or :failure
600
519
  # @param [Number] level the indention level
601
520
  #
602
- def report_specdoc_errors(spec, options, level)
603
- if spec['errors'] && (options[:errors] == :always || (options[:errors] == :failure && !spec['passed']))
521
+ def report_specdoc_errors(spec, level)
522
+ if spec['errors'] && (options[:errors] == :always || (options[:errors] == :failure && spec['status']=='failed'))
604
523
  spec['errors'].each do |error|
524
+ Formatter.spec_failed(indent(" ➤ #{ format_error(error,true) }", level))
605
525
  if error['trace']
606
526
  error['trace'].each do |trace|
607
- Formatter.spec_failed(indent(" ➜ Exception: #{ error['msg'] } in #{ trace['file'] } on line #{ trace['line'] }", level))
527
+ Formatter.spec_failed(indent(" ➜ #{ trace['file'] } on line #{ trace['line'] }", level+2))
608
528
  end
609
- else
610
- Formatter.spec_failed(indent(" ➜ Exception: #{ error['msg'] }", level))
611
529
  end
612
530
  end
613
531
  end
@@ -622,24 +540,6 @@ module Guard
622
540
  (' ' * level) + message
623
541
  end
624
542
 
625
- # Show system notifications about the occurred errors.
626
- #
627
- # @param [Hash] result the suite result
628
- # @param [Hash] options the options
629
- # @option options [Integer] :max_error_notify maximum error notifications to show
630
- # @option options [Boolean] :notification show notifications
631
- #
632
- def notify_errors(result, options)
633
- collect_specs(result['suites']).each_with_index do |spec, index|
634
- if !spec['passed'] && options[:max_error_notify] > index
635
- msg = spec['messages'].map { |message| format_message(message, true) }.join(', ')
636
- Formatter.notify("#{ spec['description'] }: #{ msg }",
637
- title: 'Jasmine spec failed',
638
- image: :failed,
639
- priority: 2) if options[:notification]
640
- end
641
- end
642
- end
643
543
 
644
544
  # Tests if the given suite has a failing spec underneath.
645
545
  #
@@ -647,7 +547,19 @@ module Guard
647
547
  # @return [Boolean] the search result
648
548
  #
649
549
  def contains_failed_spec?(suite)
650
- collect_specs([suite]).any? { |spec| !spec['passed'] }
550
+ collect_specs([suite]).any? { |spec| spec['status'] == 'failed' }
551
+ end
552
+
553
+
554
+ # Get all failed specs from the suites and its nested suites.
555
+ #
556
+ # @param suites [Array<Hash>] the suites results
557
+ # @return [Array<Hash>] all failed
558
+ #
559
+ def collect_spec_errors(suites)
560
+ collect_specs(suites).map { |spec|
561
+ (spec['errors']||[]).map { |error| format_error(error,false) }
562
+ }.flatten
651
563
  end
652
564
 
653
565
  # Get all specs from the suites and its nested suites.
@@ -656,10 +568,9 @@ module Guard
656
568
  # @return [Array<Hash>] all specs
657
569
  #
658
570
  def collect_specs(suites)
659
- suites.inject([]) do |specs, suite|
660
- specs = (specs | suite['specs']) if suite['specs']
661
- specs = (specs | collect_specs(suite['suites'])) if suite['suites']
662
- specs
571
+ suites.each_with_object([]) do |suite, specs|
572
+ specs.concat( suite['specs'] )
573
+ specs.concat( collect_specs(suite['suites']) ) if suite['suites']
663
574
  end
664
575
  end
665
576
 
@@ -669,9 +580,11 @@ module Guard
669
580
  # @param [Boolean] short show a short version of the message
670
581
  # @return [String] the cleaned error message
671
582
  #
672
- def format_message(message, short)
673
- if message =~ /(.*?) in http.+?assets\/(.*)\?body=\d+\s\((line\s\d+)/
674
- short ? $1 : "#{ $1 } in #{ $2 } on #{ $3 }"
583
+ def format_error(error, short)
584
+ message = error['message'].gsub(%r{ in http.*\(line \d+\)$},'')
585
+ if !short && error['trace'] && error['trace'].length > 0
586
+ location = error['trace'][0]
587
+ "#{message} in #{location['file']}:#{location['line']}"
675
588
  else
676
589
  message
677
590
  end
@@ -682,9 +595,8 @@ module Guard
682
595
  #
683
596
  # @param [Hash] coverage the last run coverage data
684
597
  # @param [String] file the file name of the spec
685
- # @param [Hash] options the options for the execution
686
598
  #
687
- def update_coverage(coverage, file, options)
599
+ def update_coverage(coverage, file)
688
600
  if file == options[:spec_dir]
689
601
  File.write(coverage_file, MultiJson.encode(coverage, { max_nesting: false }))
690
602
  else
@@ -707,16 +619,15 @@ module Guard
707
619
  #
708
620
  # @return [Boolean] true if any coverage threshold is set
709
621
  #
710
- def any_coverage_threshold?(options)
622
+ def any_coverage_threshold?
711
623
  THRESHOLDS.any? { |threshold| options[threshold] != 0 }
712
624
  end
713
625
 
714
626
  # Converts the options to Istanbul recognized options
715
627
  #
716
- # @param [Hash] options the options for the coverage
717
628
  # @return [String] the command line options
718
629
  #
719
- def istanbul_coverage_options(options)
630
+ def istanbul_coverage_options
720
631
  THRESHOLDS.inject([]) do |coverage, name|
721
632
  threshold = options[name]
722
633
  coverage << (threshold != 0 ? "--#{ name.to_s.sub('_threshold', '') } #{ threshold }" : '')
@@ -750,13 +661,12 @@ module Guard
750
661
 
751
662
  # Creates and returns the coverage report directory.
752
663
  #
753
- # @param [Hash] options for the coverage report directory
754
664
  # @return [String] the coverage report directory
755
665
  #
756
- def coverage_report_directory(options)
666
+ def coverage_report_directory
757
667
  File.expand_path(options[:coverage_html_dir])
758
668
  end
759
- end
760
669
  end
761
670
  end
762
671
  end
672
+