seaweed 0.1.1 → 0.1.2

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/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
+ }