seaweed 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/Spec.coffee CHANGED
@@ -1,17 +1,13 @@
1
+ #= require Spec/ObjectExtensions
2
+ #= require Spec/WindowExtensions
3
+
1
4
  # Seaweed Coffeescript spec framework
2
- window.Spec = {
5
+
6
+ window.Spec ||= {}
7
+ $.extend window.Spec, {
3
8
  EnvironmentInitialized: false
4
-
5
- # Adds   indentation to a string
6
- Pad: (string, times) ->
7
- for i in [1..times]
8
- string = ' ' + string
9
- string
10
-
11
- # Escapes text for HTML
12
- Escape: (string) ->
13
- $('<div/>').text(String(string)).html()
14
-
9
+ _extended: []
10
+
15
11
  # Executes a test case
16
12
  describe: (title, definition) ->
17
13
  @initializeEnvironment() unless @EnvironmentInitialized
@@ -32,6 +28,50 @@ window.Spec = {
32
28
 
33
29
  definition()
34
30
 
31
+ # Tries to format definition source code as readable test description
32
+ descriptionize: (definition) ->
33
+ # Get function source code
34
+ definition = String definition
35
+
36
+ # Remove function boilerplate from beginning
37
+ definition = definition.replace(/^\s*function\s*\([^\)]*\)\s*\{\s*(return\s*)?/, '')
38
+
39
+ # Remove function boilerplate from end
40
+ definition = definition.replace(/\s*;\s*\}\s*$/, '')
41
+
42
+ # Replace symbols with whitespace
43
+ definition = definition.replace(/[\s\(\)_\-\.'"]+/g, ' ')
44
+
45
+ # Split camelCased terms into seperate words
46
+ definition = definition.replace(/([a-z])([A-Z])/g, (s, a, b) -> "#{a} #{b.toLowerCase()}")
47
+
48
+ definition
49
+
50
+ # Escapes text for HTML
51
+ escape: (string) ->
52
+ $('<div/>').text(String(string)).html()
53
+
54
+ # Extends a one or more classes with test methods
55
+ extend: () ->
56
+ for klass in arguments
57
+ @_extended.push klass
58
+ $.extend klass, @ObjectExtensions
59
+ $.extend klass.prototype, @ObjectExtensions if klass.prototype
60
+
61
+ # Fails test, with an error message
62
+ fail: (message, location) ->
63
+ @passed = false
64
+ @error = message
65
+ titles = []
66
+ for item in @testStack
67
+ titles.push item.title
68
+ titles.push @testTitle
69
+ @errors.push {
70
+ title: titles.join ' '
71
+ message: message
72
+ location: location
73
+ }
74
+
35
75
  # Displays a summary of error rate at the end of testing
36
76
  finalize: ->
37
77
  summary = "#{@counts.passed} passed, #{@counts.failed} failed, #{@counts.pending} pending, #{@counts.total} total"
@@ -40,9 +80,12 @@ window.Spec = {
40
80
  document.title = summary
41
81
  if @errors.length
42
82
  $('<h3>Errors</h3>').appendTo document.body
43
- ul = $('<ul></ul>').addClass('errors').appendTo(document.body)
83
+
84
+ html = ['<table class="errors"><thead><tr><th>Error</th><th>Location</th><th>Test</th></tr></thead><tbody>']
44
85
  for error in @errors
45
- ul.append $('<li>').append($('<span>').html(error.message), ' - ', $('<span>').html(error.title))
86
+ html.push '<tr><td>', error.message, '</td><td>', error.location, '</td><td>', error.title, '</td></tr>'
87
+ html.push '</tbody></table>'
88
+ $(document.body).append html.join('')
46
89
  when 'terminal'
47
90
  $('.results').append "<br>"
48
91
  for error in @errors
@@ -55,10 +98,25 @@ window.Spec = {
55
98
  32
56
99
  $('.results').append "&#x1b;[1m&#x1b;[#{color}m#{summary}&#x1b;[0m<br>"
57
100
 
101
+ # Finds a matcher specified by a string, or passes through a matcher
102
+ # specified directly.
103
+ findMatcher: (value) ->
104
+ if typeof value is 'string'
105
+ if found = value.match(/^be([A-Z]\w*)$/)
106
+ beAttribute found[1].replace(/^[A-Z]/, (s) -> s.toLowerCase())
107
+ else if window[value]
108
+ window[value]
109
+ else
110
+ null
111
+ else
112
+ value
113
+
58
114
  # Extends the environment with test methods
59
115
  initializeEnvironment: ->
60
116
  @EnvironmentInitialized = true
61
-
117
+
118
+ $.extend window, @WindowExtensions
119
+
62
120
  @errors = []
63
121
  @counts = {
64
122
  passed: 0
@@ -66,243 +124,73 @@ window.Spec = {
66
124
  pending: 0
67
125
  total: 0
68
126
  }
69
-
127
+
70
128
  @Format = 'ul'
71
129
  @Format = 'terminal' if location.hash == '#terminal'
72
-
130
+
73
131
  # Add results display element to the page
74
132
  switch @Format
75
133
  when 'ul'
76
134
  $('body').append('<ul class="results"></ul>')
77
135
  when 'terminal'
78
136
  $('body').append('<div class="results"></div>')
79
-
80
- # Tests for a positive match
81
- Object.prototype.should = (matcher) ->
82
- result = matcher(this)
83
- Spec.fail "expected #{result[1]}" unless result[0]
84
-
85
- # Tests for a negative match
86
- Object.prototype.shouldNot = (matcher) ->
87
- result = matcher(this)
88
- Spec.fail "expected not #{result[1]}" if result[0]
89
-
90
- # Sets up an expectation
91
- window.expectation = (message) ->
92
- exp = {
93
- message: message
94
- meet: -> @met++
95
- met: 0
96
- desired: 1
97
- twice: ->
98
- @desired = 2
99
- this
100
- exactly: (times) ->
101
- @desired = times
102
- {times: this}
103
- timesString: (times) ->
104
- switch times
105
- when 0
106
- 'not at all'
107
- when 1
108
- 'once'
109
- when 2
110
- 'twice'
111
- else
112
- "#{times} times"
113
- check: ->
114
- if @met != @desired
115
- Spec.fail "expected #{message} #{@timesString @desired}, actually received #{@timesString @met}"
116
- }
117
- Spec.expectations.push exp
118
- exp
119
-
120
- # Creates a stub method with an expectation
121
- Object.prototype.shouldReceive = (name) ->
122
- object = this
123
-
124
- received = expectation "to receive &ldquo;#{name}&rdquo;"
125
-
126
- passthrough = object[name]
127
- object[name] = -> received.meet()
128
-
129
- received.with = (expectArgs...) ->
130
- object[name] = (args...) ->
131
- received.meet()
132
- correct = true
133
- correct = false if expectArgs.length != args.length
134
- if correct
135
- for i in [0..args.length]
136
- correct = false unless String(expectArgs[i]) == String(args[i])
137
- unless correct
138
- Spec.fail "expected ##{name} to be called with arguments &ldquo;#{expectArgs.join ', '}&rdquo;, actual arguments: &ldquo;#{args.join ', '}&rdquo;"
139
- received
140
-
141
- received.andReturn = (returnValue) ->
142
- fn = object[name]
143
- object[name] = ->
144
- fn.apply this, arguments
145
- returnValue
146
- received
147
-
148
- received.andPassthrough = ->
149
- fn = object[name]
150
- object[name] = ->
151
- fn.apply this, arguments
152
- passthrough.apply this, arguments
153
- received
154
-
155
- received
156
-
157
- # Creates a stub method, with an expectation of no calls
158
- Object.prototype.shouldNotReceive = (name) ->
159
- @shouldReceive(name).exactly(0).times
160
-
161
- # Allows an assertion on a non-object value
162
- window.expect = (object) ->
163
- {
164
- to: (matcher) ->
165
- result = matcher(object)
166
- Spec.fail "expected #{result[1]}" unless result[0]
167
- notTo: (matcher) ->
168
- result = matcher(object)
169
- Spec.fail "expected not #{result[1]}" if result[0]
170
- }
171
-
172
- # Adds a setup step to the current test case
173
- window.beforeEach = (action) ->
174
- test = Spec.testStack[Spec.testStack.length - 1]
175
- test.before.push action
176
-
177
- # Prepares a sub-test of the current test case
178
- window.describe = window.context = (title, definition) ->
179
- parent = Spec.testStack[Spec.testStack.length - 1]
180
-
181
- ul = $('<ul></ul>')
182
- switch Spec.Format
183
- when 'ul'
184
- parent.ul.append($('<li>' + title + '</li>').append(ul))
185
- when 'terminal'
186
- $('.results').append(Spec.Pad(title, parent.ul.depth) + "<br>")
187
- ul.depth = parent.ul.depth + 2
188
-
189
- Spec.testStack.push {
190
- title: title
191
- ul: ul
192
- before: []
193
- }
194
- definition()
195
- Spec.testStack.pop()
196
-
197
- # Creates a specificaition
198
- window.it = (title, definition) ->
199
- test = Spec.testStack[Spec.testStack.length - 1]
200
- status = if definition?
201
- env = {sandbox: $('<div/>').appendTo document.body}
202
- for aTest in Spec.testStack
203
- for action in aTest.before
204
- action.call env
205
-
206
- Spec.expectations = []
207
- Spec.testTitle = title
208
-
209
- window.onerror = (message) ->
210
- Spec.fail "Error: #{message}"
211
-
212
- Spec.passed = true
213
- try
214
- definition.call env
215
- catch e
216
- Spec.fail 'Error: ' + e
217
-
218
- for expectation in Spec.expectations
219
- expectation.check()
220
-
221
- delete Spec.expectations
222
- delete Spec.testTitle
223
- delete window.onerror
224
-
225
- env.sandbox.empty().remove()
226
-
227
- if Spec.passed then "passed"; else "failed"
228
- else
229
- "pending"
230
137
 
231
- switch Spec.Format
232
- when 'ul'
233
- li = $('<li>' + title + '</li>')
234
- li.addClass status
235
-
236
- test.ul.append li
237
- when 'terminal'
238
- s = title
239
- color = switch status
240
- when 'passed' then 32
241
- when 'failed' then 31
242
- when 'pending' then 33
243
- $('.results').append Spec.Pad("&#x1b;[#{color}m#{s}&#x1b;[0m<br>", test.ul.depth)
244
-
245
- Spec.counts[status]++
246
- Spec.counts.total++
247
-
248
- # Tests if matched value is a function
249
- window.beAFunction = (value) ->
250
- [typeof value is 'function', "to have type &ldquo;function&rdquo;, actual &ldquo;#{typeof value}&rdquo;"]
251
-
252
- # Tests if matched value === expected value
253
- window.be = (expected) ->
254
- (value) ->
255
- [value is expected, "to be &ldquo;#{Spec.Escape expected}&rdquo;, actual &ldquo;#{Spec.Escape value}&rdquo;"]
256
-
257
- # Tests if matched value is boolean true
258
- window.beTrue = (value) ->
259
- [String(value) == 'true', "to be true, got &ldquo;#{Spec.Escape value}&rdquo;"]
260
-
261
- # Tests if matched value is boolean false
262
- window.beFalse = (value) ->
263
- [String(value) == 'false', "to be false, got &ldquo;#{Spec.Escape value}&rdquo;"]
264
-
265
- # Tests if matched value is an instance of class
266
- window.beAnInstanceOf = (klass) ->
267
- (value) ->
268
- [value instanceof klass, "to be an instance of &ldquo;#{klass}&rdquo;"]
269
-
270
- # Tests if matched value == expected value
271
- window.equal = (expected) ->
272
- (value) ->
273
- [String(value) == String(expected), "to equal &ldquo;#{Spec.Escape expected}&rdquo;, actual &ldquo;#{Spec.Escape value}&rdquo;"]
138
+ @extend Array, Boolean, Date, Element, Function, jQuery, Number, RegExp,
139
+ SpecObject, String
274
140
 
275
- # Fails test, with an error message
276
- fail: (message) ->
277
- @passed = false
278
- @error = message
279
- titles = []
280
- for item in @testStack
281
- titles.push item.title
282
- titles.push @testTitle
283
- @errors.push {
284
- title: titles.join ' '
285
- message: message
286
- }
141
+ # Returns an HTML representation of any kind of object
142
+ inspect: (object) ->
143
+ if object instanceof Array
144
+ s = '['
145
+ first = true
146
+ for item in object
147
+ if first
148
+ first = false
149
+ else
150
+ first += ', '
151
+ s += '&ldquo;' + @escape(String(item)) + '&rdquo;'
152
+ s + ']'
153
+ else if object is null
154
+ 'null'
155
+ else if object is undefined
156
+ 'undefined'
157
+ else if object is true
158
+ 'true'
159
+ else if object is false
160
+ 'false'
161
+ else if typeof object == 'object'
162
+ s = "{"
163
+ first = true
164
+ for key of object
165
+ # Access hasOwnProperty through Object.prototype to work around bug
166
+ # in IE6/7/8 when calling hasOwnProperty on a DOM element
167
+ if Object.prototype.hasOwnProperty.call(object, key)
168
+ if first
169
+ first = false
170
+ else
171
+ s += ", "
172
+ s += @escape(key) + ': &ldquo;' + @escape(String(object[key])) + '&rdquo;'
173
+ s + "}"
174
+ else
175
+ "&ldquo;#{@escape(object)}&rdquo;"
287
176
 
177
+ # Adds &nbsp; indentation to a string
178
+ pad: (string, times) ->
179
+ for i in [1..times]
180
+ string = '&nbsp;' + string
181
+ string
182
+
288
183
  # Cleans test environment initialized with #initializeEnvironment
289
184
  uninitializeEnvironment: ->
290
185
  @EnvironmentInitialized = false
291
186
 
292
- delete Object.prototype.should
293
- delete Object.prototype.shouldNot
294
- delete window.expectation
295
- delete Object.prototype.shouldReceive
296
- delete Object.prototype.shouldNotReceive
297
- delete window.expect
298
- delete window.beforeEach
299
- delete window.describe
300
- delete window.context
301
- delete window.it
302
- delete window.beAFunction
303
- delete window.be
304
- delete window.beTrue
305
- delete window.beFalse
306
- delete window.beAnInstanceOf
307
- delete window.equal
187
+ for klass in @_extended
188
+ for key of @ObjectExtensions
189
+ delete klass[key]
190
+ delete klass.prototype[key] if klass.prototype
191
+
192
+ @_extended.length = 0
193
+
194
+ for key of @WindowExtensions
195
+ delete window[key]
308
196
  }
@@ -0,0 +1,31 @@
1
+ # encoding: UTF-8
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/../seaweed')
4
+
5
+ module Seaweed
6
+ class Runner
7
+ def initialize mode, options={}, parser=nil
8
+ Seaweed.load_configuration
9
+
10
+ Seaweed.port = options[:port] if options[:port]
11
+
12
+ if options[:version]
13
+ puts "Seaweed Version #{Seaweed::VERSION}"
14
+ else
15
+ case mode
16
+ when 's', 'server'
17
+ Seaweed.start_server
18
+ when 't', 'terminal'
19
+ Seaweed.spawn_server
20
+ Seaweed.run_suite
21
+ when 'a', 'auto'
22
+ Seaweed.spawn_server
23
+ Seaweed.run_suite
24
+ Seaweed.watch_for_changes
25
+ else
26
+ puts parser || "Unknown mode “#{mode}”"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'sinatra'
4
+ require 'slim'
5
+ require 'coffee-script'
6
+ require File.expand_path(File.dirname(__FILE__) + '/../seaweed')
7
+
8
+ module Seaweed
9
+ class Server < Sinatra::Application
10
+ # Configure paths
11
+ set :public_folder, ROOT + '/public'
12
+ set :views, ROOT + '/views'
13
+
14
+ # Configure slim for prettier code formatting
15
+ Slim::Engine.set_default_options :pretty => true
16
+
17
+ # Hide redundant log messages
18
+ disable :logging
19
+
20
+ # Processes request for page index
21
+ get "/" do
22
+ # Fetch list of all specification files in specs path
23
+ @scripts = []
24
+ Seaweed.specs.each do |path|
25
+ Dir["#{Seaweed::PROJECT_ROOT}/#{path}/**/*.spec.coffee"].each do |file|
26
+ @scripts << $1 if file.match Regexp.new("^#{Regexp.escape Seaweed::PROJECT_ROOT}\\/#{Regexp.escape path}\\/(.*).coffee$")
27
+ end
28
+ end
29
+
30
+ render :slim, :index
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module Seaweed
2
+ VERSION = "0.1.2"
3
+ end
data/lib/seaweed.rb ADDED
@@ -0,0 +1,121 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rubygems'
4
+ require 'sprockets'
5
+ require 'net/http'
6
+ require 'rack'
7
+ require 'yaml'
8
+ require 'seaweed/version'
9
+
10
+ module Seaweed
11
+ ROOT = File.expand_path File.join(File.dirname(__FILE__), '..')
12
+ PROJECT_ROOT = File.expand_path "."
13
+
14
+ CONFIG_PATHS = [
15
+ File.join(PROJECT_ROOT, 'seaweed.yml'),
16
+ File.join(PROJECT_ROOT, 'config', 'seaweed.yml')
17
+ ]
18
+
19
+ @configuration = {}
20
+
21
+ def self.load_configuration
22
+ # Set configuration defaults
23
+ @configuration['port'] = 4567
24
+ @configuration['libs'] = ['lib']
25
+ @configuration['specs'] = ['spec']
26
+
27
+ # Load custom configuration file
28
+ CONFIG_PATHS.each do |path|
29
+ if File.exists? path
30
+ @configuration.merge! YAML.load(File.read(path))
31
+ puts "Loaded configuration from “#{path}”"
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.port
37
+ @configuration['port']
38
+ end
39
+
40
+ def self.port= value
41
+ @configuration['port'] = value
42
+ end
43
+
44
+ def self.root_url
45
+ "http://localhost:#{port}/"
46
+ end
47
+
48
+ def self.libs
49
+ @configuration['libs']
50
+ end
51
+
52
+ def self.specs
53
+ @configuration['specs']
54
+ end
55
+
56
+ def self.all_paths
57
+ libs + specs
58
+ end
59
+
60
+ # Prepares a Sprockets::Environment object to serve coffeescript assets
61
+ def self.sprockets_environment
62
+ @environment ||= Sprockets::Environment.new.tap do |environment|
63
+ environment.append_path File.join(Seaweed::ROOT, 'lib')
64
+ all_paths.each do |path|
65
+ environment.append_path path
66
+ end
67
+ end
68
+ end
69
+
70
+ def self.start_server
71
+ app = Rack::Builder.app do
72
+ map '/assets' do
73
+ run Seaweed.sprockets_environment
74
+ end
75
+
76
+ map '/' do
77
+ run Seaweed::Server
78
+ end
79
+ end
80
+ Rack::Handler.default.run app, :Port => port
81
+ end
82
+
83
+ def self.spawn_server
84
+ # Start server in its own thread
85
+ Thread.new &start_server
86
+
87
+ # Keep trying to connect to server until we succeed
88
+ begin
89
+ page = Net::HTTP.get URI.parse(root_url)
90
+ rescue Errno::ECONNREFUSED
91
+ sleep 1
92
+ retry
93
+ end
94
+ end
95
+
96
+ def self.run_suite
97
+ require 'celerity'
98
+
99
+ if @browser
100
+ @browser.refresh
101
+ else
102
+ @browser = Celerity::Browser.new
103
+ @browser.goto "#{root_url}#terminal"
104
+ end
105
+ puts @browser.text
106
+ end
107
+
108
+ def self.watch_for_changes
109
+ require 'watchr'
110
+
111
+ # Build a regexp to match .coffee files in any project paths
112
+ path_matcher = Regexp.new('^(' + all_paths.map{ |s| Regexp.escape s}.join('|') + ')\/.*\.coffee$')
113
+
114
+ script = Watchr::Script.new
115
+ script.watch(path_matcher) { run_suite }
116
+ controller = Watchr::Controller.new(script, Watchr.handler.new)
117
+ controller.run
118
+ end
119
+ end
120
+
121
+ require File.expand_path(File.dirname(__FILE__) + '/seaweed/server')
data/public/ie.js ADDED
@@ -0,0 +1,5 @@
1
+ if(typeof String.prototype.trim !== 'function') {
2
+ String.prototype.trim = function() {
3
+ return this.replace(/^\s+|\s+$/g, '');
4
+ }
5
+ }