├── .DS_Store
├── .gitignore
├── Gruntfile.coffee
├── LICENSE.md
├── README.md
├── bower.json
├── coffee
├── banner.coffee
└── finch.coffee
├── finch.js
├── finch.min.js
├── package.json
├── scripts
├── .DS_Store
├── banner.js
└── jquery.min-1.7.1.js
└── tests
├── .DS_Store
├── index.html
├── project_advanced.html
├── project_basic.html
├── qunit.css
├── qunit.js
├── sinon-1.3.1.js
├── sinon-ie-1.3.1.js
├── tests.coffee
└── tests.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stoodder/finchjs/7c23ce7ea6ae9d222450daf7eab16a958f717637/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.sublime-project
2 | *.sublime-workspace
3 | TODO.txt
4 | .DS_Store
5 | node_modules
6 | ./**/.sass-cache
--------------------------------------------------------------------------------
/Gruntfile.coffee:
--------------------------------------------------------------------------------
1 | ###
2 | #=================================================
3 | #
4 | # Setup
5 | #
6 | #=================================================
7 |
8 | Install Ruby
9 | Install Node.js (http://nodejs.org/)
10 | npm install -g grunt-cli
11 | npm install coffee-script
12 | npm install grunt --save-dev
13 | npm install grunt-contrib-coffee --save-dev
14 | npm install grunt-contrib-uglify --save-dev
15 | npm install grunt-contrib-watch --save-dev
16 |
17 | ###
18 | module.exports = (grunt) ->
19 | grunt.loadNpmTasks('grunt-contrib-coffee')
20 | grunt.loadNpmTasks('grunt-contrib-uglify')
21 | grunt.loadNpmTasks('grunt-contrib-watch')
22 | grunt.registerTask('default', [
23 | 'coffee:banner'
24 | 'update_banner'
25 | 'coffee:dist'
26 | 'uglify:dist'
27 | 'coffee:test'
28 | 'watch'
29 | ])
30 |
31 | grunt.registerTask 'update_banner', 'updates the banner information', ->
32 | try
33 | banner = grunt.file.read('scripts/banner.js').toString()
34 | catch e
35 | banner = ""
36 | #END try
37 |
38 | uglfiy_cfg = grunt.config('uglify')
39 | uglfiy_cfg.dist.options.banner = banner
40 |
41 | grunt.config('uglify', uglfiy_cfg)
42 | #END registerTask
43 |
44 | grunt.initConfig
45 | 'pkg': grunt.file.readJSON('package.json')
46 |
47 | 'coffee':
48 | 'banner':
49 | options:
50 | bare: true
51 | #END options
52 |
53 | files:
54 | 'scripts/banner.js': ["coffee/banner.coffee"]
55 | #END files
56 | #END banner
57 |
58 | 'dist':
59 | options:
60 | join: true
61 | #END options
62 |
63 | files:
64 | '<%= pkg.name %>.js': [
65 | "coffee/banner.coffee"
66 | "coffee/<%= pkg.name %>.coffee"
67 | ]
68 | #END files
69 | #END coffee:dist
70 |
71 | 'test':
72 | files:
73 | "tests/tests.js": "tests/tests.coffee"
74 | #END files
75 | #END coffee:test
76 | #END coffee
77 |
78 | 'uglify':
79 | 'dist':
80 | options:
81 | 'banner': '' #Updated lated in the update_banner task
82 | #END options
83 | files:
84 | '<%= pkg.name %>.min.js': '<%= pkg.name %>.js'
85 | #END files
86 | #END uglifY:dist
87 | #END uglify
88 |
89 | 'watch':
90 | 'banner_coffee':
91 | 'files': ["coffee/banner.coffee"]
92 | 'tasks': ['coffee:banner', 'update_banner', 'coffee:dist', 'uglify:dist']
93 | #END watch:banner_coffee
94 |
95 | 'dist_coffee':
96 | 'files': ["coffee/<%= pkg.name %>.coffee"]
97 | 'tasks': ['coffee:dist', 'uglify:dist']
98 | #END watch:dist_coffee
99 |
100 | 'test_coffee':
101 | 'files': ['tests/tests.coffee']
102 | 'tasks': ['coffee:test']
103 | #END watch:test_coffee
104 | #END watch
105 | #END initConfig
106 | #END exports
107 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # Finch.js - Powerfully simple javascript routing
2 | ### by [Rick Allen](https://github.com/stoodder) and [Greg Smith](https://github.com/smrq)
3 |
4 | Available for use under the [MIT License](http://en.wikipedia.org/wiki/MIT_License)
5 |
6 | Copyright (C) 2011 by [RokkinCat](http://www.rokkincat.com)
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy of
9 | this software and associated documentation files (the "Software"), to deal in
10 | the Software without restriction, including without limitation the rights to
11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
12 | of the Software, and to permit persons to whom the Software is furnished to do
13 | so, subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Finch.js
2 |
3 | ## Powerfully Simple Javascript Routing
4 | Finch.js is a whole new take on handling routes in javascript web apps. It utilizes the natural hierarchy of routes, simplified pattern matching, and powerful parameter dependencies in order to speed up and organize web apps that rely highly on interecting with the browser's url.
5 |
6 | ## Installation
7 | First, [Download](http://rickallen.me/finchjs#download) Finch.js
8 |
9 | Once you've gotten the files, simply include the javascript in your html <head/>> tags.
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ... Stuff here ...
18 |
19 |
20 |
21 | Since Finch is a standalone library, this is all you'll need. From here, take a look at our [website](http://rickallen.me/finchjs) for more info on how to implement Finch.
22 |
23 | ## Documentation
24 | Take a look at our [website](http://rickallen.me/finchjs) for the most up-to-date documention.
25 |
26 | ## Licenese
27 | Finch is available for use under the [MIT License](https://github.com/stoodder/finchjs/blob/master/LICENSE.md)
28 |
29 | ## TODO List
30 | * __Splats__ - Sometimes we might want an undetermined number of parameters at the end of a url, splats are useful for grabbing any number of url bindings and must be the last binding in the route pattern. Example: "/home/news/:variables..."
31 | * __pushState__ - Add pushstate support to finch so that we don't always need to rely on the hash
32 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "finchjs",
3 | "version": "0.5.14",
4 | "homepage": "https://github.com/stoodder/finchjs",
5 | "authors": [
6 | "Rick Allen "
7 | ],
8 | "description": "Simple Hierarchical Javascript Routing",
9 | "main": [
10 | "finch.js"
11 | ],
12 | "keywords": [
13 | "Javascript",
14 | "Routing"
15 | ],
16 | "license": "MIT",
17 | "ignore": [
18 | "**/.*",
19 | "node_modules",
20 | "bower_components",
21 | "test",
22 | "tests",
23 | "scripts",
24 | "coffee",
25 | "finch.sublime-project",
26 | "finch.sublime-workspace",
27 | "Gruntfile.coffee",
28 | "package.json",
29 | ".gitignore"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/coffee/banner.coffee:
--------------------------------------------------------------------------------
1 | ###
2 | Finch.js - Powerfully simple javascript routing
3 | by Rick Allen (stoodder) and Greg Smith (smrq)
4 |
5 | Version 0.5.15
6 | Full source at https://github.com/stoodder/finchjs
7 | Copyright (c) 2011 RokkinCat, http://www.rokkincat.com
8 |
9 | MIT License, https://github.com/stoodder/finchjs/blob/master/LICENSE.md
10 | This file is generated by `cake build`, do not edit it by hand.
11 | ###
12 |
--------------------------------------------------------------------------------
/coffee/finch.coffee:
--------------------------------------------------------------------------------
1 | #------------------
2 | # Utility
3 | #------------------
4 |
5 | isObject = (object) -> (typeof object) is (typeof {}) and object isnt null
6 | isFunction = (object) -> Object::toString.call( object ) is "[object Function]"
7 | isBoolean = (object) -> Object::toString.call( object ) is "[object Boolean]"
8 | isArray = (object) -> Object::toString.call( object ) is "[object Array]"
9 | isString = (object) -> Object::toString.call( object ) is "[object String]"
10 | isNumber = (object) -> Object::toString.call( object ) is "[object Number]"
11 |
12 | trim = (str) -> str.replace(/^\s+/, '').replace(/\s+$/, '')
13 | trimSlashes = (str) -> str.replace(/^\//, '').replace(/\/$/, '')
14 | startsWith = (haystack, needle) -> haystack.indexOf(needle) is 0
15 | endsWith = (haystack, needle) -> haystack.indexOf(needle, haystack.length - needle.length) isnt -1
16 |
17 | contains = (haystack, needle) ->
18 | if isFunction( haystack.indexOf )
19 | return haystack.indexOf(needle) isnt -1
20 | else if isArray( haystack )
21 | for hay in haystack
22 | return true if hay is needle
23 | return false
24 | peek = (arr) -> arr[arr.length - 1]
25 |
26 | countSubstrings = (str, substr) -> str.split(substr).length - 1
27 |
28 | objectKeys = (obj) -> (key for key of obj)
29 | objectValues = (obj) -> (value for key, value of obj)
30 |
31 | extend = (obj, extender) ->
32 | obj = {} unless isObject(obj)
33 | extender = {} unless isObject(extender)
34 |
35 | obj[key] = value for key, value of extender
36 |
37 | return obj
38 |
39 | compact = (obj) ->
40 | obj = {} unless isObject(obj)
41 | newObj = {}
42 | (newObj[key] = value if value?) for key, value of obj
43 | return newObj
44 |
45 | objectsEqual = (obj1, obj2) ->
46 | for key, value of obj1
47 | return false if obj2[key] isnt value
48 | for key, value of obj2
49 | return false if obj1[key] isnt value
50 | return true
51 |
52 | arraysEqual = (arr1, arr2) ->
53 | return false if arr1.length isnt arr2.length
54 | for value, index in arr1
55 | return false if arr2[index] isnt value
56 | return true
57 |
58 | diffObjects = (oldObject = {}, newObject = {}) ->
59 | result = {}
60 | for key, value of oldObject
61 | result[key] = newObject[key] if newObject[key] != value
62 | for key, value of newObject
63 | result[key] = value if oldObject[key] != value
64 | return result
65 |
66 | #------------------
67 | # Ensure that console exists (for non-compatible browsers)
68 | #------------------
69 | console = window.console ? {}
70 | console.log ?= (->)
71 | console.warn ?= (->)
72 |
73 | #------------------
74 | # Classes
75 | #------------------
76 |
77 | class ParsedRouteString
78 | constructor: ({components, childIndex}) ->
79 | @components = components ? []
80 | @childIndex = childIndex ? 0
81 |
82 | class RouteNode
83 | constructor: ({name, nodeType, parent} = {}) ->
84 | # The name property is not used by code; it is included
85 | # for readability of the generated objects
86 | @name = name ? ""
87 | @nodeType = nodeType ? null
88 | @parent = parent ? null
89 | @routeSettings = null
90 | @childLiterals = {}
91 | @childVariable = null
92 | @bindings = []
93 |
94 | class RouteSettings
95 | constructor: ({setup, teardown, load, unload, context} = {}) ->
96 | @setup = if isFunction(setup) then setup else (->)
97 | @load = if isFunction(load) then load else (->)
98 | @unload = if isFunction(unload) then unload else (->)
99 | @teardown = if isFunction(teardown) then teardown else (->)
100 | @context = if isObject(context) then context else {}
101 |
102 | class RoutePath
103 | constructor: ({node, boundValues, parameterObservables} = {}) ->
104 | @node = node ? null
105 | @boundValues = boundValues ? []
106 | @parameterObservables = parameterObservables ? [[]]
107 |
108 | getBindings: ->
109 | bindings = {}
110 | for binding, index in @node.bindings
111 | bindings[binding] = @boundValues[index]
112 | return parseParameters( bindings )
113 |
114 | isEqual: (path) -> path? and @node is path.node and arraysEqual(@boundValues, path.boundValues)
115 |
116 | isRoot: -> not @node.parent?
117 |
118 | getParent: ->
119 | return null unless @node?
120 | bindingCount = @node.parent?.bindings.length ? 0
121 | boundValues = @boundValues.slice(0, bindingCount)
122 | parameterObservables = @parameterObservables.slice(0,-1)
123 | return new RoutePath(node: @node.parent, boundValues: boundValues, parameterObservables: parameterObservables)
124 |
125 | getChild: (targetPath) ->
126 | while targetPath? and not @isEqual(parent = targetPath.getParent())
127 | targetPath = parent
128 | targetPath.parameterObservables = @parameterObservables.slice(0)
129 | targetPath.parameterObservables.push([])
130 | return targetPath
131 |
132 | class ParameterObservable
133 | constructor: (callback) ->
134 | @callback = callback
135 | @callback = (->) unless isFunction(@callback)
136 | @dependencies = []
137 | @initialized = false
138 |
139 | notify: (updatedKeys) ->
140 | shouldTrigger = do =>
141 | return true if not @initialized
142 | for key in @dependencies
143 | return true if contains(updatedKeys, key)
144 | return false
145 | @trigger() if shouldTrigger
146 |
147 | trigger: ->
148 | @dependencies = []
149 | parameterAccessor = (key) =>
150 | @dependencies.push(key) unless contains(@dependencies, key)
151 | return CurrentParameters[key]
152 | @callback(parameterAccessor)
153 | @initialized = true
154 |
155 | #------------------
156 | # Constants
157 | #------------------
158 |
159 | NullPath = new RoutePath(node: null)
160 | NodeType = {
161 | Literal: 'Literal'
162 | Variable: 'Variable'
163 | }
164 |
165 | #------------------
166 | # Functions
167 | #------------------
168 |
169 | #---------------------------------------------------
170 | # Method: parseQueryString
171 | # Used to parse and objectize a query string
172 | #
173 | # Arguments:
174 | # queryString - The query string to split up into an object
175 | #
176 | # Returns:
177 | # object - An object of the split apart query string
178 | #---------------------------------------------------
179 | parseQueryString = (queryString) ->
180 |
181 | #Make sure the query string is valid
182 | queryString = if isString(queryString) then trim(queryString) else ""
183 |
184 | #setup the return parameters
185 | queryParameters = {}
186 |
187 | #iterate through the pieces of the query string
188 | if queryString != ""
189 | for piece in queryString.split("&")
190 | [key, value] = piece.split("=", 2)
191 |
192 | queryParameters[key] = value
193 |
194 | #return the result
195 | return parseParameters( queryParameters )
196 |
197 | #END parseQueryString
198 |
199 | #---------------------------------------------------
200 | # Method: getHash
201 | # Used to get the hash of a url in a standard way (that performs the same in all browsers)
202 | #
203 | # Returns:
204 | # string - the string of the current hash, including the '#'
205 | #---------------------------------------------------
206 | getHash = () ->
207 |
208 | return "#" + ( window.location.href.split("#", 2)[1] ? "" )
209 |
210 | #END getHash
211 |
212 | #---------------------------------------------------
213 | # Method: setHash
214 | # Used to set the current hash in a standard way
215 | #---------------------------------------------------
216 | setHash = (hash) ->
217 | hash = "" unless isString(hash)
218 | hash = trim(hash)
219 | hash = hash[1..] if hash[0..0] is '#'
220 | window.location.hash = hash
221 | #END setHash
222 |
223 | #---------------------------------------------------
224 | # Method: parseParameters
225 | # Used to 'smartly' parse through the parameters
226 | # - converts string bools to booleans
227 | # - converts string numbers to numbers
228 | #
229 | # Arguments:
230 | # params - The input parameters to patse through
231 | #
232 | # Returns:
233 | # object - The parsed parameters
234 | #---------------------------------------------------
235 | parseParameters = (params) ->
236 | params = {} unless isObject(params)
237 |
238 | #Try to parse through parameters and be smart about their values
239 | if Options.CoerceParameterTypes
240 | for key, value of params
241 |
242 | #Is this a boolean
243 | if value is "true"
244 | value = true
245 | else if value is "false"
246 | value = false
247 | #Is this an int
248 | else if /^[0-9]+$/.test(value)
249 | value = parseInt(value)
250 | #Is this a float
251 | else if /^[0-9]+\.[0-9]*$/.test(value)
252 | value = parseFloat(value)
253 | params[key] = value
254 |
255 | #Return the parameters
256 | return params
257 |
258 | #END parseParameters
259 |
260 |
261 | #---------------------------------------------------
262 | # Method: splitUri
263 | # Splits a uri string into its components.
264 | #
265 | # Arguments:
266 | # uri - The uri to split
267 | #
268 | # Returns:
269 | # array - The components of the uri
270 | #
271 | # Examples:
272 | # splitUri("") => ["/"]
273 | # splitUri("/") => ["/"]
274 | # splitUri("foo") => ["/", "foo"]
275 | # splitUri("/foo/bar/") => ["/", "foo", "bar"]
276 | #---------------------------------------------------
277 | splitUri = (uri) ->
278 | uri = trimSlashes(uri)
279 | components = if uri is "" then [] else uri.split("/")
280 | components.unshift("/")
281 | return components
282 |
283 | #---------------------------------------------------
284 | # Method: parseRouteString
285 | # Validates and parses a route string.
286 | #
287 | # Arguments:
288 | # routeString - The route string to parse
289 | #
290 | # Returns:
291 | # ParsedRouteString -the parsed route string,
292 | # or null if the route string was malformed.
293 | #---------------------------------------------------
294 | parseRouteString = (routeString) ->
295 |
296 | hasParent = contains(routeString, "[") or contains(routeString, "]")
297 |
298 | if hasParent then do ->
299 | # Validate []s match
300 | startCount = countSubstrings(routeString, "[")
301 | unless startCount is 1
302 | console.warn "[FINCH] Parsing failed on \"#{routeString}\": Extra [" if startCount > 1
303 | console.warn "[FINCH] Parsing failed on \"#{routeString}\": Missing [" if startCount < 1
304 | return null
305 |
306 | endCount = countSubstrings(routeString, "]")
307 | unless endCount is 1
308 | console.warn "[FINCH] Parsing failed on \"#{routeString}\": Extra ]" if endCount > 1
309 | console.warn "[FINCH] Parsing failed on \"#{routeString}\": Missing ]" if endCount < 1
310 | return null
311 |
312 | # Validate the string starts with [
313 | unless startsWith(routeString, "[")
314 | console.warn "[FINCH] Parsing failed on \"#{routeString}\": [ not at beginning"
315 | return null
316 |
317 | # Remove [] from string
318 | flatRouteString = routeString.replace(/[\[\]]/g, "")
319 |
320 | # Separate string into individual components
321 | if flatRouteString is "" then components = []
322 | else components = splitUri(flatRouteString)
323 |
324 | # Validate individual components
325 | for component in components
326 | if component is ""
327 | console.warn "[FINCH] Parsing failed on \"#{routeString}\": Blank component"
328 | return null
329 |
330 | # Find the index into the components list where the child route starts
331 | childIndex = 0
332 | if hasParent
333 | [parentString] = routeString.split("]")
334 | parentComponents = splitUri(parentString.replace("[", ""))
335 | if parentComponents[parentComponents.length-1] isnt components[parentComponents.length-1]
336 | console.warn "[FINCH] Parsing failed on \"#{routeString}\": ] in the middle of a component"
337 | return null
338 | if parentComponents.length is components.length
339 | console.warn "[FINCH] Parsing failed on \"#{routeString}\": No child components"
340 | return null
341 | childIndex = parentComponents.length
342 |
343 | return new ParsedRouteString({components, childIndex})
344 |
345 | #END parseRouteString
346 |
347 | #---------------------------------------------------
348 | # Method: getComponentType
349 | #---------------------------------------------------
350 | getComponentType = (routeStringComponent) ->
351 | return NodeType.Variable if startsWith(routeStringComponent, ":")
352 | return NodeType.Literal
353 |
354 | #END getComponentType
355 |
356 | #---------------------------------------------------
357 | # Method: getComponentName
358 | #---------------------------------------------------
359 | getComponentName = (routeStringComponent) ->
360 | switch getComponentType(routeStringComponent)
361 | when NodeType.Literal then routeStringComponent
362 | when NodeType.Variable then routeStringComponent[1..]
363 |
364 | #END getComponentName
365 |
366 | #---------------------------------------------------
367 | # Method: addRoute
368 | # Adds a new route node to the route tree, given a route string.
369 | #
370 | # Arguments:
371 | # rootNode - The root node of the route tree.
372 | # parsedRouteString - The parsed route string to add to the route tree.
373 | # settings - The settings for the new route
374 | #
375 | # Returns:
376 | # RouteSettings - The settings of the added route
377 | #---------------------------------------------------
378 | addRoute = (rootNode, parsedRouteString, settings) ->
379 |
380 | {components, childIndex} = parsedRouteString
381 | parentNode = rootNode
382 | bindings = []
383 |
384 | (recur = (currentNode, currentIndex) ->
385 | parentNode = currentNode if currentIndex is childIndex
386 |
387 | # Are we done traversing the route string?
388 | if parsedRouteString.components.length <= 0
389 | currentNode.parent = parentNode
390 | currentNode.bindings = bindings
391 | return currentNode.routeSettings = new RouteSettings(settings)
392 |
393 | component = components.shift()
394 | componentType = getComponentType(component)
395 | componentName = getComponentName(component)
396 |
397 | switch componentType
398 | when NodeType.Literal
399 | nextNode = currentNode.childLiterals[componentName] ?= new RouteNode(name: "#{currentNode.name}#{component}/", nodeType: componentType, parent: rootNode)
400 | when NodeType.Variable
401 | nextNode = currentNode.childVariable ?= new RouteNode(name: "#{currentNode.name}#{component}/", nodeType: componentType, parent: rootNode)
402 | # Push the variable name onto the end of the bindings list
403 | bindings.push(componentName)
404 |
405 | recur(nextNode, currentIndex+1)
406 | )(rootNode, 0)
407 |
408 | #END addRoute
409 |
410 | #---------------------------------------------------
411 | # Method: findPath
412 | # Finds a route in the route tree, given a URI.
413 | #
414 | # Arguments:
415 | # rootNode - The root node of the route tree.
416 | # uri - The uri to parse and match against the route tree.
417 | #
418 | # Returns:
419 | # RoutePath
420 | # node - The node that matches the URI
421 | # boundValues - An ordered list of values bound to each variable in the URI
422 | #---------------------------------------------------
423 | findPath = (rootNode, uri) ->
424 | uriComponents = splitUri(uri)
425 | boundValues = []
426 |
427 | (recur = (currentNode, uriComponents) ->
428 | # Are we done traversing the uri?
429 | if uriComponents.length <= 0 and currentNode.routeSettings?
430 | return new RoutePath( node: currentNode, boundValues: boundValues )
431 |
432 | component = uriComponents[0]
433 |
434 | # Try to find a matching literal component
435 | if currentNode.childLiterals[component]?
436 | result = recur(currentNode.childLiterals[component], uriComponents[1..])
437 | return result if result?
438 |
439 | # Try to find a matching variable component
440 | if currentNode.childVariable?
441 | boundValues.push(component)
442 | result = recur(currentNode.childVariable, uriComponents[1..])
443 | return result if result?
444 | boundValues.pop()
445 |
446 | # No matching route found in this traversal branch
447 | return null
448 | )(rootNode, uriComponents)
449 |
450 | #END findPath
451 |
452 | #---------------------------------------------------
453 | # Method: findNearestCommonAncestor
454 | # Finds the nearest common ancestor route node of two routes.
455 | #
456 | # Arguments:
457 | # path1, path2 - The two paths to compare.
458 | #
459 | # Returns:
460 | # RoutePath - The nearest common ancestor path of the two paths, or
461 | # null if there is no common ancestor.
462 | #---------------------------------------------------
463 | findNearestCommonAncestor = (path1, path2) ->
464 | # Enumerate all ancestors of path2 in order
465 | ancestors = []
466 | currentRoute = path2
467 | while currentRoute?
468 | ancestors.push currentRoute
469 | currentRoute = currentRoute.getParent()
470 |
471 | # Find the first ancestor of path1 that is also an ancestor of path2
472 | currentRoute = path1
473 | while currentRoute?
474 | for ancestor in ancestors
475 | return currentRoute if currentRoute.isEqual(ancestor)
476 | currentRoute = currentRoute.getParent()
477 |
478 | # No common ancestors. (Do these nodes belong to different trees?)
479 | return null
480 |
481 | #END findNearestCommonAncestor
482 |
483 | #---------------------------------------------------
484 | # Globals
485 | #---------------------------------------------------
486 | RootNode = CurrentPath = CurrentTargetPath = null
487 | PreviousParameters = CurrentParameters = null
488 | HashInterval = CurrentHash = null
489 | HashListening = false
490 | IgnoreObservables = SetupCalled = false # Used to handle cases of same load/setup methods
491 | LoadCompleted = false
492 | Options = {
493 | CoerceParameterTypes: false
494 | }
495 |
496 | do resetGlobals = ->
497 | RootNode = new RouteNode(name: "*")
498 | CurrentPath = NullPath
499 | PreviousParameters = {}
500 | CurrentParameters = {}
501 | CurrentTargetPath = null
502 | HashInterval = null
503 | CurrentHash = null
504 | HashListening = false
505 | IgnoreObservables = false
506 | SetupCalled = false
507 | LoadCompleted = false
508 |
509 | #END Globals
510 |
511 | #---------------------------------------------------
512 | # Method: step
513 | #---------------------------------------------------
514 | step = ->
515 | #If there is no current target path, only step through the observables
516 | if CurrentTargetPath is null
517 |
518 | #Execute the observables
519 | runObservables()
520 |
521 | #Otherwise, if this is our first call since the last 'load' was called,
522 | #Call the unload method
523 | else if LoadCompleted
524 | stepUnload()
525 |
526 | #Otherwise, we're currently stepping and if we're at our destination. run the load method
527 | else if CurrentTargetPath.isEqual(CurrentPath)
528 |
529 | #Execute this path's load method
530 | stepLoad()
531 |
532 | #Otherwise step through a teardown/setup
533 | else
534 | # Find the nearest common ancestor of the current and new path
535 | ancestorPath = findNearestCommonAncestor(CurrentPath, CurrentTargetPath)
536 |
537 | # If the current path is an ancestor of the new path, then setup towards the new path;
538 | # otherwise, teardown towards the common ancestor
539 | if CurrentPath.isEqual(ancestorPath) then stepSetup() else stepTeardown()
540 |
541 | #END step
542 |
543 | #---------------------------------------------------
544 | # Method: stepSetup
545 | # Used to execute a setup method on a node
546 | #---------------------------------------------------
547 | stepSetup = ->
548 | SetupCalled = true
549 |
550 | #Try and get the parent context if we can
551 | {context: parentContext} = CurrentPath.node?.routeSettings ? {context: null}
552 |
553 | # During setup and teardown, CurrentPath should always be the path to the
554 | # node getting setup or torn down.
555 | # In the setup case: CurrentPath must be set before the setup function is called.
556 | CurrentPath = CurrentPath.getChild(CurrentTargetPath)
557 |
558 | {context, setup, load} = CurrentPath.node.routeSettings ? {}
559 | context ?= {}
560 | context.parent = parentContext
561 | setup ?= (->)
562 | load ?= (->)
563 | bindings = CurrentPath.getBindings()
564 | recur = -> step()
565 |
566 | # If the setup/teardown takes two parameters, then it is an asynchronous call
567 | if setup.length is 2
568 | setup.call(context, bindings, recur)
569 |
570 | # Otherwise it is a synchronous call
571 | else
572 | setup.call(context, bindings)
573 | recur()
574 |
575 | #END stepSetup
576 |
577 | #---------------------------------------------------
578 | # Method: stepLoad
579 | # Used to execute a load method on a node
580 | #---------------------------------------------------
581 | stepLoad = ->
582 | #Setup the recurrance method
583 | recur = ->
584 | # End the step process
585 | LoadCompleted = true
586 | CurrentTargetPath = null
587 | step()
588 |
589 | #Stop executing if we don't have a current node
590 | return recur() unless CurrentPath.node?
591 |
592 | {context, setup, load} = CurrentPath.node.routeSettings ? {}
593 | context ?= {}
594 | setup ?= (->)
595 | load ?= (->)
596 | bindings = CurrentPath.getBindings()
597 |
598 | #Is the load method asynchronous?
599 | if load.length is 2
600 | load.call(context, bindings, recur)
601 |
602 | #Execute it synchronously
603 | else
604 | load.call(context, bindings)
605 | recur()
606 |
607 | #END stepLoad
608 |
609 | #---------------------------------------------------
610 | # Method: stepUnload
611 | # Used to execute a unload method on a node
612 | #---------------------------------------------------
613 | stepUnload = ->
614 | LoadCompleted = false
615 |
616 | recur = ->
617 | step()
618 |
619 | {context, unload} = CurrentPath.node.routeSettings ? {}
620 | context ?= {}
621 | unload ?= (->)
622 | bindings = CurrentPath.getBindings()
623 |
624 | #If the unload method takes two parameters, it is an asynchronous method
625 | if unload.length is 2
626 | unload.call(context, bindings, recur)
627 |
628 | #Otherwise call it synchronously
629 | else
630 | unload.call(context, bindings)
631 | recur()
632 |
633 | #END stepUnload
634 |
635 | #---------------------------------------------------
636 | # Method: stepTeardown
637 | # Used to execute a teardown method on a node
638 | #---------------------------------------------------
639 | stepTeardown = ->
640 | SetupCalled = false
641 |
642 | {context, teardown} = CurrentPath.node.routeSettings ? {}
643 | context ?= {}
644 | teardown ?= (->)
645 | bindings = CurrentPath.getBindings()
646 | recur = ->
647 | # During setup and teardown, CurrentPath should always be the path to the
648 | # node getting setup or torn down.
649 | # In the teardown case: CurrentPath must be set after the teardown function is called.
650 | CurrentPath = CurrentPath.getParent()
651 | step()
652 |
653 | # If the setup/teardown takes two parameters, then it is an asynchronous call
654 | if teardown.length is 2
655 | teardown.call(context, bindings, recur)
656 |
657 | # Otherwise it is a synchronous call
658 | else
659 | teardown.call(context, bindings)
660 | recur()
661 |
662 | #END stepTeardown
663 |
664 | #---------------------------------------------------
665 | # Method: runObservables
666 | # Used to iterate through the observables
667 | #---------------------------------------------------
668 | runObservables = ->
669 | # Run observables
670 | keys = objectKeys( diffObjects( PreviousParameters, CurrentParameters ))
671 | PreviousParameters = CurrentParameters
672 | for observableList in CurrentPath.parameterObservables
673 | for observable in observableList
674 | observable.notify(keys)
675 |
676 | #END runObservables
677 |
678 | #---------------------------------------------------
679 | # Method: hashChangeListener
680 | # Used to respond to hash changes
681 | #---------------------------------------------------
682 | hashChangeListener = (event) ->
683 | hash = getHash()
684 | hash = hash.slice(1) if startsWith(hash, "#")
685 | hash = unescape(hash)
686 |
687 | #Only try to run Finch.call if the hash actually changed
688 | if hash isnt CurrentHash
689 |
690 | #Run Finch.call, if successful save the current hash
691 | if Finch.call(hash)
692 | CurrentHash = hash
693 |
694 | #If not successful revert
695 | else
696 | setHash(CurrentHash ? "")
697 |
698 | #END hashChangeListener
699 |
700 | #---------------------------------------------------
701 | # Class: Finch
702 | #
703 | # Methods:
704 | # Finch.route - Assigns a new route pattern
705 | # Finch.call - Calls a specific route and operates accordingly
706 | # Finch.listen - Listens to changes in the hash portion of the window.location
707 | # Finch.ignore - Ignored hash responses
708 | # Finch.navigate - Navigates the page (updates the hash)
709 | # Finch.reset - resets Finch
710 | #---------------------------------------------------
711 | Finch = {
712 | #---------------------------------------------------
713 | # Method: Finch.getCurrentHash
714 | # Obtain the hash of the current route
715 | #---------------------------------------------------
716 | getCurrentHash: () ->
717 | return CurrentHash
718 |
719 | #END Finch.getCurrentHash
720 |
721 | #---------------------------------------------------
722 | # Method: Finch.route
723 | # Used to setup a new route
724 | #
725 | # Arguments:
726 | # pattern - The pattern to add
727 | # settings - The settings for when this route is executed
728 | #---------------------------------------------------
729 | route: (pattern, settings) ->
730 |
731 | #Check if the input parameter was a function, assign it to the setup method
732 | #if it was
733 | if isFunction(settings)
734 |
735 | #Store some scoped variables
736 | cb = settings
737 | settings = {setup: cb}
738 |
739 | #if the callback was asynchronous, setup the setting as such
740 | if cb.length is 2
741 | settings.load = (bindings, next) ->
742 | if not SetupCalled
743 | IgnoreObservables = true
744 | cb(bindings, next)
745 | else
746 | next()
747 |
748 | #Otherwise set them up synchronously
749 | else
750 | settings.load = (bindings) ->
751 | if not SetupCalled
752 | IgnoreObservables = true
753 | cb(bindings)
754 |
755 | settings = {} unless isObject(settings)
756 |
757 | # Make sure we have valid inputs
758 | pattern = "" unless isString(pattern)
759 | pattern = trim(pattern)
760 | pattern = "/" unless pattern.length > 0
761 |
762 | # Parse the route, and return false if it was invalid
763 | parsedRouteString = parseRouteString(pattern)
764 | return false unless parsedRouteString?
765 |
766 | # Add the new route to the route tree
767 | addRoute(RootNode, parsedRouteString, settings)
768 |
769 | return this
770 |
771 | #END Finch.route()
772 |
773 | #---------------------------------------------------
774 | # Method: Finch.call
775 | #
776 | # Arguments:
777 | # route - The route to try and call
778 | #---------------------------------------------------
779 | call: (uri) ->
780 | #Make sure we have valid arguments
781 | uri = "/" unless isString(uri)
782 | uri = "/" if uri is ""
783 |
784 | #Extract the route and query parameters from the uri
785 | [uri, queryString] = uri.split("?", 2)
786 |
787 | # Find matching route in route tree, returning false if there is none
788 | newPath = findPath(RootNode, uri)
789 | unless newPath?
790 | console.warn "[FINCH] Could not find route for: #{uri}"
791 | return false
792 | #END unless
793 |
794 | queryParameters = parseQueryString(queryString)
795 | bindings = newPath.getBindings()
796 | CurrentParameters = extend(queryParameters, bindings)
797 |
798 | #If we're not in the middle of executing and the current path is the same
799 | #as the one we're trying to go to, just execute the observables so we
800 | #avoid calling the load method again
801 | if CurrentTargetPath is null and CurrentPath.isEqual(newPath)
802 | step()
803 |
804 | #Otherwise, start stepping towards our target
805 | else
806 | previousTargetPath = CurrentTargetPath
807 | CurrentTargetPath = newPath
808 |
809 | # Start the process of teardowns/setups if we were not already doing so
810 | step() unless previousTargetPath?
811 |
812 | return true;
813 |
814 | #END Finch.call()
815 |
816 | #---------------------------------------------------
817 | # Method: Finch.reload
818 | # Reruns the 'load' method
819 | #---------------------------------------------------
820 | reload: () ->
821 | return this unless LoadCompleted
822 | return this unless CurrentPath? and CurrentPath.node?
823 |
824 | saveCurrentPath = CurrentPath
825 |
826 | CurrentTargetPath = NullPath
827 | step()
828 |
829 | LoadCompleted = false
830 | CurrentTargetPath = CurrentPath = saveCurrentPath
831 | step()
832 |
833 | return this
834 | #END Finch.reload()
835 |
836 | #---------------------------------------------------
837 | # Method: Finch.observe
838 | # Used to set up observers on the query string.
839 | #
840 | # Form 1:
841 | # Finch.observe(key, key, ..., callback(keys...))
842 | # Arguments:
843 | # keys... - A list of parameter keys
844 | # callback(keys...) - A callback function to execute with the values bound to each key in order.
845 | #
846 | # Form 2:
847 | # Finch.observe([key, key, ...], callback(keys...))
848 | # Arguments:
849 | # keys[] - An array of parameter keys
850 | # callback(keys...) - A callback function to execute with the values bound to each key in order.
851 | #
852 | # Form 3:
853 | # Finch.observe(callback(accessor))
854 | # Arguments:
855 | # callback(accessor) - A callback function to execute with a parameter accessor.
856 | #---------------------------------------------------
857 | observe: (args...) ->
858 | #Don't worry about this if we're ignoring the params
859 | if IgnoreObservables
860 | return IgnoreObservables = false
861 |
862 | # The callback is alwaysthe last parameter
863 | callback = args.pop()
864 | callback = (->) unless isFunction(callback)
865 |
866 | # Handle argument form 1/2
867 | if args.length > 0
868 |
869 | if args.length is 1 and isArray(args[0])
870 | keys = args[0]
871 | else
872 | keys = args
873 | return Finch.observe (paramAccessor) ->
874 | values = (paramAccessor(key) for key in keys)
875 | callback(values...)
876 |
877 | #Handle form 3
878 | else
879 | observable = new ParameterObservable(callback)
880 | peek(CurrentPath.parameterObservables).push(observable)
881 |
882 | #END Finch.observe()
883 |
884 | #---------------------------------------------------
885 | # Method: Finch.abort
886 | # Used to abort a current call and hand control back to finch.
887 | # This can be especially useful when doing an asynchronous call that
888 | # for some reason (perhaps an ajax fail) doesn't ever call the continuation
889 | # method, therefore hanging the entire app.
890 | #---------------------------------------------------
891 | abort: () ->
892 | #Simply abort by clearing the current target path
893 | CurrentTargetPath = null
894 |
895 | #END abort
896 |
897 | #---------------------------------------------------
898 | # Method: Finch.listen
899 | # Used to listen to changes in the window hash, will respond with Finch.call
900 | #
901 | # Returns:
902 | # boolean - Is Finch listening?
903 | #---------------------------------------------------
904 | listen: () ->
905 | #Only do this if we're currently not listening
906 | if not HashListening
907 | #Check if the window has an onhashcnage event
908 | if "onhashchange" of window
909 | if isFunction(window.addEventListener)
910 | window.addEventListener("hashchange", hashChangeListener, true)
911 | HashListening = true
912 |
913 | else if isFunction(window.attachEvent)
914 | window.attachEvent("hashchange", hashChangeListener)
915 | HashListening = true
916 |
917 | # if we're still not listening fallback to a set interval
918 | if not HashListening
919 | HashInterval = setInterval(hashChangeListener, 33)
920 | HashListening = true
921 |
922 | #Perform an initial hash change
923 | hashChangeListener()
924 |
925 | return HashListening
926 |
927 | #END Finch.listen()
928 |
929 | #---------------------------------------------------
930 | # Method: Finch.ignore
931 | # Used to stop listening to changes in the hash
932 | #
933 | # Returns:
934 | # boolean - Is Finch done listening?
935 | #---------------------------------------------------
936 | ignore: () ->
937 | #Only continue if we're listening
938 | if HashListening
939 |
940 | #Are we suing set interval? if so, clear it
941 | if HashInterval isnt null
942 | clearInterval(HashInterval)
943 | HashInterval = null
944 | HashListening = false
945 |
946 | #Otherwise if the window has onhashchange, try to remove the event listener
947 | else if "onhashchange" of window
948 |
949 | if isFunction(window.removeEventListener)
950 | window.removeEventListener("hashchange", hashChangeListener, true)
951 | HashListening = false
952 |
953 | else if isFunction(window.detachEvent)
954 | window.detachEvent("hashchange", hashChangeListener)
955 | HashListening = false
956 |
957 | return not HashListening
958 |
959 | #END Finch.ignore()
960 |
961 | #---------------------------------------------------
962 | # Method: Finch.navigate
963 | # Method used to 'navigate' to a new/update the existing hash route
964 | #
965 | # Form 1:
966 | # Finch.navigate('/my/favorite/route', {hello: 'world'}, true)
967 | #
968 | # Arguments:
969 | # uri (string) - string of a uri to browse to
970 | # queryParams (object) - The query parameters to add the to the uri
971 | # doUpdate (boolean) - Should we replace the current hash or just updates it?
972 | #
973 | #
974 | # Form 2:
975 | # Finch.navigate('my/second/favorite/url', true)
976 | #
977 | # Updates the url keeping the current query params
978 | #
979 | # Arguments:
980 | # uri (string) - string of a uri to browse to
981 | # doUpdate (boolean) - Should we replace the current hash or just updates it?
982 | #
983 | #
984 | # Form 3:
985 | # Finch.navigate({hello: 'world', foo: 'bar'}, true)
986 | #
987 | # Updates the query params keeping the current url
988 | #
989 | # Arguments:
990 | # queryParams (object) - The query parameters to add the to the uri
991 | # doUpdate (boolean) - Should we replace the current hash or just updates it?
992 | #---------------------------------------------------
993 | navigate: (uri, queryParams, doUpdate) ->
994 |
995 | #Get the current uri and params
996 | [ currentUri, currentQueryString ] = getHash().split("?", 2)
997 | currentUri ?= ""
998 | currentQueryString ?= ""
999 |
1000 | #format the current uri appropriately
1001 | currentUri = currentUri[1..] if currentUri[0..0] is "#"
1002 | currentUri = unescape(currentUri)
1003 |
1004 | #format the currentParams
1005 | currentQueryParams = parseQueryString( currentQueryString )
1006 |
1007 | #Make sure our arguments are valid
1008 | doUpdate = queryParams if isBoolean(queryParams)
1009 | queryParams = uri if isObject(uri)
1010 |
1011 | uri = "" unless isString(uri)
1012 | queryParams = {} unless isObject(queryParams)
1013 | doUpdate = false unless isBoolean(doUpdate)
1014 |
1015 | uri = trim(uri)
1016 | uri = null if uri.length is 0
1017 |
1018 | #If we're just updating, extend the currnet params
1019 | if doUpdate
1020 | #Unescape things fromthe current query params
1021 | do ->
1022 | newQueryParams = {}
1023 | for key, value of currentQueryParams
1024 | newQueryParams[unescape(key)] = unescape(value)
1025 | currentQueryParams = newQueryParams
1026 |
1027 | #udpate the query params
1028 | queryParams = extend(currentQueryParams, queryParams)
1029 |
1030 |
1031 | #Start trying to create the new uri
1032 | uri = currentUri if uri is null
1033 | [uri, uriParamString] = uri.split("?", 2)
1034 | uri = uri[1..] if uri[0..0] is "#"
1035 |
1036 | #Check if they're trying to use relative routing
1037 | if startsWith(uri, "./") or startsWith(uri, "../")
1038 | builtUri = currentUri
1039 |
1040 | while startsWith(uri, "./") or startsWith(uri, "../")
1041 | slashIndex = uri.indexOf("/")
1042 | piece = uri.slice(0, slashIndex)
1043 | uri = uri.slice(slashIndex+1)
1044 | builtUri = builtUri.slice(0, builtUri.lastIndexOf("/")) if piece is ".."
1045 |
1046 | uri = if uri.length > 0 then "#{builtUri}/#{uri}" else builtUri
1047 |
1048 | #Make sure the uri param string is valid
1049 | uriQueryParams = if isString(uriParamString) then parseQueryString(uriParamString) else {}
1050 |
1051 | #Get and format the query params
1052 | queryParams = extend(uriQueryParams, queryParams)
1053 | queryParams = compact(queryParams)
1054 |
1055 | #Build the final uri
1056 | uri = escape(uri)
1057 |
1058 | #Generate a query string
1059 | queryString = (escape(key) + "=" + escape(value) for key, value of queryParams).join("&")
1060 |
1061 | #try to attach the query string
1062 | uri += "?" + queryString if queryString.length > 0
1063 |
1064 | #update the hash
1065 | setHash(uri)
1066 |
1067 | #END Finch.navigate()
1068 |
1069 | #---------------------------------------------------
1070 | # Method: Finch.reset
1071 | # Tears down the current stack and resets the routes
1072 | #
1073 | # Arguments:
1074 | # none
1075 | #---------------------------------------------------
1076 | reset: ->
1077 | # Tear down the entire route
1078 | Finch.options(CoerceParameterTypes: false)
1079 | CurrentTargetPath = NullPath
1080 | step()
1081 | Finch.ignore()
1082 | resetGlobals()
1083 | return
1084 |
1085 | #END Finch.reset()
1086 |
1087 | #---------------------------------------------------
1088 | # Method: Finch.options
1089 | # Sets up configurable options for Finch.
1090 | #
1091 | # Arguments:
1092 | # newOptions (object) - The new options to set.
1093 | #
1094 | # Options:
1095 | # CoerceParameterTypes (boolean, default: false)
1096 | # Whether to coerce parameters from strings into other types. For example,
1097 | # the route /home/news/:id called with /home/news/1234 will fill the
1098 | # "id" parameter with the integer 1234 instead of the string "1234".
1099 | #---------------------------------------------------
1100 | options: (newOptions) ->
1101 | extend Options, newOptions
1102 |
1103 | #END Finch.options()
1104 | }
1105 |
1106 | #Expose Finch to the window
1107 | @Finch = Finch
1108 |
--------------------------------------------------------------------------------
/finch.js:
--------------------------------------------------------------------------------
1 | /*
2 | Finch.js - Powerfully simple javascript routing
3 | by Rick Allen (stoodder) and Greg Smith (smrq)
4 |
5 | Version 0.5.15
6 | Full source at https://github.com/stoodder/finchjs
7 | Copyright (c) 2011 RokkinCat, http://www.rokkincat.com
8 |
9 | MIT License, https://github.com/stoodder/finchjs/blob/master/LICENSE.md
10 | This file is generated by `cake build`, do not edit it by hand.
11 | */
12 |
13 |
14 | (function() {
15 | var CurrentHash, CurrentParameters, CurrentPath, CurrentTargetPath, Finch, HashInterval, HashListening, IgnoreObservables, LoadCompleted, NodeType, NullPath, Options, ParameterObservable, ParsedRouteString, PreviousParameters, RootNode, RouteNode, RoutePath, RouteSettings, SetupCalled, addRoute, arraysEqual, compact, console, contains, countSubstrings, diffObjects, endsWith, extend, findNearestCommonAncestor, findPath, getComponentName, getComponentType, getHash, hashChangeListener, isArray, isBoolean, isFunction, isNumber, isObject, isString, objectKeys, objectValues, objectsEqual, parseParameters, parseQueryString, parseRouteString, peek, resetGlobals, runObservables, setHash, splitUri, startsWith, step, stepLoad, stepSetup, stepTeardown, stepUnload, trim, trimSlashes, _ref,
16 | __slice = [].slice;
17 |
18 | isObject = function(object) {
19 | return (typeof object) === (typeof {}) && object !== null;
20 | };
21 |
22 | isFunction = function(object) {
23 | return Object.prototype.toString.call(object) === "[object Function]";
24 | };
25 |
26 | isBoolean = function(object) {
27 | return Object.prototype.toString.call(object) === "[object Boolean]";
28 | };
29 |
30 | isArray = function(object) {
31 | return Object.prototype.toString.call(object) === "[object Array]";
32 | };
33 |
34 | isString = function(object) {
35 | return Object.prototype.toString.call(object) === "[object String]";
36 | };
37 |
38 | isNumber = function(object) {
39 | return Object.prototype.toString.call(object) === "[object Number]";
40 | };
41 |
42 | trim = function(str) {
43 | return str.replace(/^\s+/, '').replace(/\s+$/, '');
44 | };
45 |
46 | trimSlashes = function(str) {
47 | return str.replace(/^\//, '').replace(/\/$/, '');
48 | };
49 |
50 | startsWith = function(haystack, needle) {
51 | return haystack.indexOf(needle) === 0;
52 | };
53 |
54 | endsWith = function(haystack, needle) {
55 | return haystack.indexOf(needle, haystack.length - needle.length) !== -1;
56 | };
57 |
58 | contains = function(haystack, needle) {
59 | var hay, _i, _len;
60 | if (isFunction(haystack.indexOf)) {
61 | return haystack.indexOf(needle) !== -1;
62 | } else if (isArray(haystack)) {
63 | for (_i = 0, _len = haystack.length; _i < _len; _i++) {
64 | hay = haystack[_i];
65 | if (hay === needle) {
66 | return true;
67 | }
68 | }
69 | }
70 | return false;
71 | };
72 |
73 | peek = function(arr) {
74 | return arr[arr.length - 1];
75 | };
76 |
77 | countSubstrings = function(str, substr) {
78 | return str.split(substr).length - 1;
79 | };
80 |
81 | objectKeys = function(obj) {
82 | var key, _results;
83 | _results = [];
84 | for (key in obj) {
85 | _results.push(key);
86 | }
87 | return _results;
88 | };
89 |
90 | objectValues = function(obj) {
91 | var key, value, _results;
92 | _results = [];
93 | for (key in obj) {
94 | value = obj[key];
95 | _results.push(value);
96 | }
97 | return _results;
98 | };
99 |
100 | extend = function(obj, extender) {
101 | var key, value;
102 | if (!isObject(obj)) {
103 | obj = {};
104 | }
105 | if (!isObject(extender)) {
106 | extender = {};
107 | }
108 | for (key in extender) {
109 | value = extender[key];
110 | obj[key] = value;
111 | }
112 | return obj;
113 | };
114 |
115 | compact = function(obj) {
116 | var key, newObj, value;
117 | if (!isObject(obj)) {
118 | obj = {};
119 | }
120 | newObj = {};
121 | for (key in obj) {
122 | value = obj[key];
123 | if (value != null) {
124 | newObj[key] = value;
125 | }
126 | }
127 | return newObj;
128 | };
129 |
130 | objectsEqual = function(obj1, obj2) {
131 | var key, value;
132 | for (key in obj1) {
133 | value = obj1[key];
134 | if (obj2[key] !== value) {
135 | return false;
136 | }
137 | }
138 | for (key in obj2) {
139 | value = obj2[key];
140 | if (obj1[key] !== value) {
141 | return false;
142 | }
143 | }
144 | return true;
145 | };
146 |
147 | arraysEqual = function(arr1, arr2) {
148 | var index, value, _i, _len;
149 | if (arr1.length !== arr2.length) {
150 | return false;
151 | }
152 | for (index = _i = 0, _len = arr1.length; _i < _len; index = ++_i) {
153 | value = arr1[index];
154 | if (arr2[index] !== value) {
155 | return false;
156 | }
157 | }
158 | return true;
159 | };
160 |
161 | diffObjects = function(oldObject, newObject) {
162 | var key, result, value;
163 | if (oldObject == null) {
164 | oldObject = {};
165 | }
166 | if (newObject == null) {
167 | newObject = {};
168 | }
169 | result = {};
170 | for (key in oldObject) {
171 | value = oldObject[key];
172 | if (newObject[key] !== value) {
173 | result[key] = newObject[key];
174 | }
175 | }
176 | for (key in newObject) {
177 | value = newObject[key];
178 | if (oldObject[key] !== value) {
179 | result[key] = value;
180 | }
181 | }
182 | return result;
183 | };
184 |
185 | console = (_ref = window.console) != null ? _ref : {};
186 |
187 | if (console.log == null) {
188 | console.log = (function() {});
189 | }
190 |
191 | if (console.warn == null) {
192 | console.warn = (function() {});
193 | }
194 |
195 | ParsedRouteString = (function() {
196 | function ParsedRouteString(_arg) {
197 | var childIndex, components;
198 | components = _arg.components, childIndex = _arg.childIndex;
199 | this.components = components != null ? components : [];
200 | this.childIndex = childIndex != null ? childIndex : 0;
201 | }
202 |
203 | return ParsedRouteString;
204 |
205 | })();
206 |
207 | RouteNode = (function() {
208 | function RouteNode(_arg) {
209 | var name, nodeType, parent, _ref1;
210 | _ref1 = _arg != null ? _arg : {}, name = _ref1.name, nodeType = _ref1.nodeType, parent = _ref1.parent;
211 | this.name = name != null ? name : "";
212 | this.nodeType = nodeType != null ? nodeType : null;
213 | this.parent = parent != null ? parent : null;
214 | this.routeSettings = null;
215 | this.childLiterals = {};
216 | this.childVariable = null;
217 | this.bindings = [];
218 | }
219 |
220 | return RouteNode;
221 |
222 | })();
223 |
224 | RouteSettings = (function() {
225 | function RouteSettings(_arg) {
226 | var context, load, setup, teardown, unload, _ref1;
227 | _ref1 = _arg != null ? _arg : {}, setup = _ref1.setup, teardown = _ref1.teardown, load = _ref1.load, unload = _ref1.unload, context = _ref1.context;
228 | this.setup = isFunction(setup) ? setup : (function() {});
229 | this.load = isFunction(load) ? load : (function() {});
230 | this.unload = isFunction(unload) ? unload : (function() {});
231 | this.teardown = isFunction(teardown) ? teardown : (function() {});
232 | this.context = isObject(context) ? context : {};
233 | }
234 |
235 | return RouteSettings;
236 |
237 | })();
238 |
239 | RoutePath = (function() {
240 | function RoutePath(_arg) {
241 | var boundValues, node, parameterObservables, _ref1;
242 | _ref1 = _arg != null ? _arg : {}, node = _ref1.node, boundValues = _ref1.boundValues, parameterObservables = _ref1.parameterObservables;
243 | this.node = node != null ? node : null;
244 | this.boundValues = boundValues != null ? boundValues : [];
245 | this.parameterObservables = parameterObservables != null ? parameterObservables : [[]];
246 | }
247 |
248 | RoutePath.prototype.getBindings = function() {
249 | var binding, bindings, index, _i, _len, _ref1;
250 | bindings = {};
251 | _ref1 = this.node.bindings;
252 | for (index = _i = 0, _len = _ref1.length; _i < _len; index = ++_i) {
253 | binding = _ref1[index];
254 | bindings[binding] = this.boundValues[index];
255 | }
256 | return parseParameters(bindings);
257 | };
258 |
259 | RoutePath.prototype.isEqual = function(path) {
260 | return (path != null) && this.node === path.node && arraysEqual(this.boundValues, path.boundValues);
261 | };
262 |
263 | RoutePath.prototype.isRoot = function() {
264 | return this.node.parent == null;
265 | };
266 |
267 | RoutePath.prototype.getParent = function() {
268 | var bindingCount, boundValues, parameterObservables, _ref1, _ref2;
269 | if (this.node == null) {
270 | return null;
271 | }
272 | bindingCount = (_ref1 = (_ref2 = this.node.parent) != null ? _ref2.bindings.length : void 0) != null ? _ref1 : 0;
273 | boundValues = this.boundValues.slice(0, bindingCount);
274 | parameterObservables = this.parameterObservables.slice(0, -1);
275 | return new RoutePath({
276 | node: this.node.parent,
277 | boundValues: boundValues,
278 | parameterObservables: parameterObservables
279 | });
280 | };
281 |
282 | RoutePath.prototype.getChild = function(targetPath) {
283 | var parent;
284 | while ((targetPath != null) && !this.isEqual(parent = targetPath.getParent())) {
285 | targetPath = parent;
286 | }
287 | targetPath.parameterObservables = this.parameterObservables.slice(0);
288 | targetPath.parameterObservables.push([]);
289 | return targetPath;
290 | };
291 |
292 | return RoutePath;
293 |
294 | })();
295 |
296 | ParameterObservable = (function() {
297 | function ParameterObservable(callback) {
298 | this.callback = callback;
299 | if (!isFunction(this.callback)) {
300 | this.callback = (function() {});
301 | }
302 | this.dependencies = [];
303 | this.initialized = false;
304 | }
305 |
306 | ParameterObservable.prototype.notify = function(updatedKeys) {
307 | var shouldTrigger,
308 | _this = this;
309 | shouldTrigger = (function() {
310 | var key, _i, _len, _ref1;
311 | if (!_this.initialized) {
312 | return true;
313 | }
314 | _ref1 = _this.dependencies;
315 | for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
316 | key = _ref1[_i];
317 | if (contains(updatedKeys, key)) {
318 | return true;
319 | }
320 | }
321 | return false;
322 | })();
323 | if (shouldTrigger) {
324 | return this.trigger();
325 | }
326 | };
327 |
328 | ParameterObservable.prototype.trigger = function() {
329 | var parameterAccessor,
330 | _this = this;
331 | this.dependencies = [];
332 | parameterAccessor = function(key) {
333 | if (!contains(_this.dependencies, key)) {
334 | _this.dependencies.push(key);
335 | }
336 | return CurrentParameters[key];
337 | };
338 | this.callback(parameterAccessor);
339 | return this.initialized = true;
340 | };
341 |
342 | return ParameterObservable;
343 |
344 | })();
345 |
346 | NullPath = new RoutePath({
347 | node: null
348 | });
349 |
350 | NodeType = {
351 | Literal: 'Literal',
352 | Variable: 'Variable'
353 | };
354 |
355 | parseQueryString = function(queryString) {
356 | var key, piece, queryParameters, value, _i, _len, _ref1, _ref2;
357 | queryString = isString(queryString) ? trim(queryString) : "";
358 | queryParameters = {};
359 | if (queryString !== "") {
360 | _ref1 = queryString.split("&");
361 | for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
362 | piece = _ref1[_i];
363 | _ref2 = piece.split("=", 2), key = _ref2[0], value = _ref2[1];
364 | queryParameters[key] = value;
365 | }
366 | }
367 | return parseParameters(queryParameters);
368 | };
369 |
370 | getHash = function() {
371 | var _ref1;
372 | return "#" + ((_ref1 = window.location.href.split("#", 2)[1]) != null ? _ref1 : "");
373 | };
374 |
375 | setHash = function(hash) {
376 | if (!isString(hash)) {
377 | hash = "";
378 | }
379 | hash = trim(hash);
380 | if (hash.slice(0, 1) === '#') {
381 | hash = hash.slice(1);
382 | }
383 | return window.location.hash = hash;
384 | };
385 |
386 | parseParameters = function(params) {
387 | var key, value;
388 | if (!isObject(params)) {
389 | params = {};
390 | }
391 | if (Options.CoerceParameterTypes) {
392 | for (key in params) {
393 | value = params[key];
394 | if (value === "true") {
395 | value = true;
396 | } else if (value === "false") {
397 | value = false;
398 | } else if (/^[0-9]+$/.test(value)) {
399 | value = parseInt(value);
400 | } else if (/^[0-9]+\.[0-9]*$/.test(value)) {
401 | value = parseFloat(value);
402 | }
403 | params[key] = value;
404 | }
405 | }
406 | return params;
407 | };
408 |
409 | splitUri = function(uri) {
410 | var components;
411 | uri = trimSlashes(uri);
412 | components = uri === "" ? [] : uri.split("/");
413 | components.unshift("/");
414 | return components;
415 | };
416 |
417 | parseRouteString = function(routeString) {
418 | var childIndex, component, components, flatRouteString, hasParent, parentComponents, parentString, _i, _len;
419 | hasParent = contains(routeString, "[") || contains(routeString, "]");
420 | if (hasParent) {
421 | (function() {
422 | var endCount, startCount;
423 | startCount = countSubstrings(routeString, "[");
424 | if (startCount !== 1) {
425 | if (startCount > 1) {
426 | console.warn("[FINCH] Parsing failed on \"" + routeString + "\": Extra [");
427 | }
428 | if (startCount < 1) {
429 | console.warn("[FINCH] Parsing failed on \"" + routeString + "\": Missing [");
430 | }
431 | return null;
432 | }
433 | endCount = countSubstrings(routeString, "]");
434 | if (endCount !== 1) {
435 | if (endCount > 1) {
436 | console.warn("[FINCH] Parsing failed on \"" + routeString + "\": Extra ]");
437 | }
438 | if (endCount < 1) {
439 | console.warn("[FINCH] Parsing failed on \"" + routeString + "\": Missing ]");
440 | }
441 | return null;
442 | }
443 | if (!startsWith(routeString, "[")) {
444 | console.warn("[FINCH] Parsing failed on \"" + routeString + "\": [ not at beginning");
445 | return null;
446 | }
447 | })();
448 | }
449 | flatRouteString = routeString.replace(/[\[\]]/g, "");
450 | if (flatRouteString === "") {
451 | components = [];
452 | } else {
453 | components = splitUri(flatRouteString);
454 | }
455 | for (_i = 0, _len = components.length; _i < _len; _i++) {
456 | component = components[_i];
457 | if (component === "") {
458 | console.warn("[FINCH] Parsing failed on \"" + routeString + "\": Blank component");
459 | return null;
460 | }
461 | }
462 | childIndex = 0;
463 | if (hasParent) {
464 | parentString = routeString.split("]")[0];
465 | parentComponents = splitUri(parentString.replace("[", ""));
466 | if (parentComponents[parentComponents.length - 1] !== components[parentComponents.length - 1]) {
467 | console.warn("[FINCH] Parsing failed on \"" + routeString + "\": ] in the middle of a component");
468 | return null;
469 | }
470 | if (parentComponents.length === components.length) {
471 | console.warn("[FINCH] Parsing failed on \"" + routeString + "\": No child components");
472 | return null;
473 | }
474 | childIndex = parentComponents.length;
475 | }
476 | return new ParsedRouteString({
477 | components: components,
478 | childIndex: childIndex
479 | });
480 | };
481 |
482 | getComponentType = function(routeStringComponent) {
483 | if (startsWith(routeStringComponent, ":")) {
484 | return NodeType.Variable;
485 | }
486 | return NodeType.Literal;
487 | };
488 |
489 | getComponentName = function(routeStringComponent) {
490 | switch (getComponentType(routeStringComponent)) {
491 | case NodeType.Literal:
492 | return routeStringComponent;
493 | case NodeType.Variable:
494 | return routeStringComponent.slice(1);
495 | }
496 | };
497 |
498 | addRoute = function(rootNode, parsedRouteString, settings) {
499 | var bindings, childIndex, components, parentNode, recur;
500 | components = parsedRouteString.components, childIndex = parsedRouteString.childIndex;
501 | parentNode = rootNode;
502 | bindings = [];
503 | return (recur = function(currentNode, currentIndex) {
504 | var component, componentName, componentType, nextNode, _base;
505 | if (currentIndex === childIndex) {
506 | parentNode = currentNode;
507 | }
508 | if (parsedRouteString.components.length <= 0) {
509 | currentNode.parent = parentNode;
510 | currentNode.bindings = bindings;
511 | return currentNode.routeSettings = new RouteSettings(settings);
512 | }
513 | component = components.shift();
514 | componentType = getComponentType(component);
515 | componentName = getComponentName(component);
516 | switch (componentType) {
517 | case NodeType.Literal:
518 | nextNode = (_base = currentNode.childLiterals)[componentName] != null ? (_base = currentNode.childLiterals)[componentName] : _base[componentName] = new RouteNode({
519 | name: "" + currentNode.name + component + "/",
520 | nodeType: componentType,
521 | parent: rootNode
522 | });
523 | break;
524 | case NodeType.Variable:
525 | nextNode = currentNode.childVariable != null ? currentNode.childVariable : currentNode.childVariable = new RouteNode({
526 | name: "" + currentNode.name + component + "/",
527 | nodeType: componentType,
528 | parent: rootNode
529 | });
530 | bindings.push(componentName);
531 | }
532 | return recur(nextNode, currentIndex + 1);
533 | })(rootNode, 0);
534 | };
535 |
536 | findPath = function(rootNode, uri) {
537 | var boundValues, recur, uriComponents;
538 | uriComponents = splitUri(uri);
539 | boundValues = [];
540 | return (recur = function(currentNode, uriComponents) {
541 | var component, result;
542 | if (uriComponents.length <= 0 && (currentNode.routeSettings != null)) {
543 | return new RoutePath({
544 | node: currentNode,
545 | boundValues: boundValues
546 | });
547 | }
548 | component = uriComponents[0];
549 | if (currentNode.childLiterals[component] != null) {
550 | result = recur(currentNode.childLiterals[component], uriComponents.slice(1));
551 | if (result != null) {
552 | return result;
553 | }
554 | }
555 | if (currentNode.childVariable != null) {
556 | boundValues.push(component);
557 | result = recur(currentNode.childVariable, uriComponents.slice(1));
558 | if (result != null) {
559 | return result;
560 | }
561 | boundValues.pop();
562 | }
563 | return null;
564 | })(rootNode, uriComponents);
565 | };
566 |
567 | findNearestCommonAncestor = function(path1, path2) {
568 | var ancestor, ancestors, currentRoute, _i, _len;
569 | ancestors = [];
570 | currentRoute = path2;
571 | while (currentRoute != null) {
572 | ancestors.push(currentRoute);
573 | currentRoute = currentRoute.getParent();
574 | }
575 | currentRoute = path1;
576 | while (currentRoute != null) {
577 | for (_i = 0, _len = ancestors.length; _i < _len; _i++) {
578 | ancestor = ancestors[_i];
579 | if (currentRoute.isEqual(ancestor)) {
580 | return currentRoute;
581 | }
582 | }
583 | currentRoute = currentRoute.getParent();
584 | }
585 | return null;
586 | };
587 |
588 | RootNode = CurrentPath = CurrentTargetPath = null;
589 |
590 | PreviousParameters = CurrentParameters = null;
591 |
592 | HashInterval = CurrentHash = null;
593 |
594 | HashListening = false;
595 |
596 | IgnoreObservables = SetupCalled = false;
597 |
598 | LoadCompleted = false;
599 |
600 | Options = {
601 | CoerceParameterTypes: false
602 | };
603 |
604 | (resetGlobals = function() {
605 | RootNode = new RouteNode({
606 | name: "*"
607 | });
608 | CurrentPath = NullPath;
609 | PreviousParameters = {};
610 | CurrentParameters = {};
611 | CurrentTargetPath = null;
612 | HashInterval = null;
613 | CurrentHash = null;
614 | HashListening = false;
615 | IgnoreObservables = false;
616 | SetupCalled = false;
617 | return LoadCompleted = false;
618 | })();
619 |
620 | step = function() {
621 | var ancestorPath;
622 | if (CurrentTargetPath === null) {
623 | return runObservables();
624 | } else if (LoadCompleted) {
625 | return stepUnload();
626 | } else if (CurrentTargetPath.isEqual(CurrentPath)) {
627 | return stepLoad();
628 | } else {
629 | ancestorPath = findNearestCommonAncestor(CurrentPath, CurrentTargetPath);
630 | if (CurrentPath.isEqual(ancestorPath)) {
631 | return stepSetup();
632 | } else {
633 | return stepTeardown();
634 | }
635 | }
636 | };
637 |
638 | stepSetup = function() {
639 | var bindings, context, load, parentContext, recur, setup, _ref1, _ref2, _ref3, _ref4;
640 | SetupCalled = true;
641 | parentContext = ((_ref1 = (_ref2 = CurrentPath.node) != null ? _ref2.routeSettings : void 0) != null ? _ref1 : {
642 | context: null
643 | }).context;
644 | CurrentPath = CurrentPath.getChild(CurrentTargetPath);
645 | _ref4 = (_ref3 = CurrentPath.node.routeSettings) != null ? _ref3 : {}, context = _ref4.context, setup = _ref4.setup, load = _ref4.load;
646 | if (context == null) {
647 | context = {};
648 | }
649 | context.parent = parentContext;
650 | if (setup == null) {
651 | setup = (function() {});
652 | }
653 | if (load == null) {
654 | load = (function() {});
655 | }
656 | bindings = CurrentPath.getBindings();
657 | recur = function() {
658 | return step();
659 | };
660 | if (setup.length === 2) {
661 | return setup.call(context, bindings, recur);
662 | } else {
663 | setup.call(context, bindings);
664 | return recur();
665 | }
666 | };
667 |
668 | stepLoad = function() {
669 | var bindings, context, load, recur, setup, _ref1, _ref2;
670 | recur = function() {
671 | LoadCompleted = true;
672 | CurrentTargetPath = null;
673 | return step();
674 | };
675 | if (CurrentPath.node == null) {
676 | return recur();
677 | }
678 | _ref2 = (_ref1 = CurrentPath.node.routeSettings) != null ? _ref1 : {}, context = _ref2.context, setup = _ref2.setup, load = _ref2.load;
679 | if (context == null) {
680 | context = {};
681 | }
682 | if (setup == null) {
683 | setup = (function() {});
684 | }
685 | if (load == null) {
686 | load = (function() {});
687 | }
688 | bindings = CurrentPath.getBindings();
689 | if (load.length === 2) {
690 | return load.call(context, bindings, recur);
691 | } else {
692 | load.call(context, bindings);
693 | return recur();
694 | }
695 | };
696 |
697 | stepUnload = function() {
698 | var bindings, context, recur, unload, _ref1, _ref2;
699 | LoadCompleted = false;
700 | recur = function() {
701 | return step();
702 | };
703 | _ref2 = (_ref1 = CurrentPath.node.routeSettings) != null ? _ref1 : {}, context = _ref2.context, unload = _ref2.unload;
704 | if (context == null) {
705 | context = {};
706 | }
707 | if (unload == null) {
708 | unload = (function() {});
709 | }
710 | bindings = CurrentPath.getBindings();
711 | if (unload.length === 2) {
712 | return unload.call(context, bindings, recur);
713 | } else {
714 | unload.call(context, bindings);
715 | return recur();
716 | }
717 | };
718 |
719 | stepTeardown = function() {
720 | var bindings, context, recur, teardown, _ref1, _ref2;
721 | SetupCalled = false;
722 | _ref2 = (_ref1 = CurrentPath.node.routeSettings) != null ? _ref1 : {}, context = _ref2.context, teardown = _ref2.teardown;
723 | if (context == null) {
724 | context = {};
725 | }
726 | if (teardown == null) {
727 | teardown = (function() {});
728 | }
729 | bindings = CurrentPath.getBindings();
730 | recur = function() {
731 | CurrentPath = CurrentPath.getParent();
732 | return step();
733 | };
734 | if (teardown.length === 2) {
735 | return teardown.call(context, bindings, recur);
736 | } else {
737 | teardown.call(context, bindings);
738 | return recur();
739 | }
740 | };
741 |
742 | runObservables = function() {
743 | var keys, observable, observableList, _i, _len, _ref1, _results;
744 | keys = objectKeys(diffObjects(PreviousParameters, CurrentParameters));
745 | PreviousParameters = CurrentParameters;
746 | _ref1 = CurrentPath.parameterObservables;
747 | _results = [];
748 | for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
749 | observableList = _ref1[_i];
750 | _results.push((function() {
751 | var _j, _len1, _results1;
752 | _results1 = [];
753 | for (_j = 0, _len1 = observableList.length; _j < _len1; _j++) {
754 | observable = observableList[_j];
755 | _results1.push(observable.notify(keys));
756 | }
757 | return _results1;
758 | })());
759 | }
760 | return _results;
761 | };
762 |
763 | hashChangeListener = function(event) {
764 | var hash;
765 | hash = getHash();
766 | if (startsWith(hash, "#")) {
767 | hash = hash.slice(1);
768 | }
769 | hash = unescape(hash);
770 | if (hash !== CurrentHash) {
771 | if (Finch.call(hash)) {
772 | return CurrentHash = hash;
773 | } else {
774 | return setHash(CurrentHash != null ? CurrentHash : "");
775 | }
776 | }
777 | };
778 |
779 | Finch = {
780 | route: function(pattern, settings) {
781 | var cb, parsedRouteString;
782 | if (isFunction(settings)) {
783 | cb = settings;
784 | settings = {
785 | setup: cb
786 | };
787 | if (cb.length === 2) {
788 | settings.load = function(bindings, next) {
789 | if (!SetupCalled) {
790 | IgnoreObservables = true;
791 | return cb(bindings, next);
792 | } else {
793 | return next();
794 | }
795 | };
796 | } else {
797 | settings.load = function(bindings) {
798 | if (!SetupCalled) {
799 | IgnoreObservables = true;
800 | return cb(bindings);
801 | }
802 | };
803 | }
804 | }
805 | if (!isObject(settings)) {
806 | settings = {};
807 | }
808 | if (!isString(pattern)) {
809 | pattern = "";
810 | }
811 | pattern = trim(pattern);
812 | if (!(pattern.length > 0)) {
813 | pattern = "/";
814 | }
815 | parsedRouteString = parseRouteString(pattern);
816 | if (parsedRouteString == null) {
817 | return false;
818 | }
819 | addRoute(RootNode, parsedRouteString, settings);
820 | return this;
821 | },
822 | call: function(uri) {
823 | var bindings, newPath, previousTargetPath, queryParameters, queryString, _ref1;
824 | if (!isString(uri)) {
825 | uri = "/";
826 | }
827 | if (uri === "") {
828 | uri = "/";
829 | }
830 | _ref1 = uri.split("?", 2), uri = _ref1[0], queryString = _ref1[1];
831 | newPath = findPath(RootNode, uri);
832 | if (newPath == null) {
833 | console.warn("[FINCH] Could not find route for: " + uri);
834 | return false;
835 | }
836 | queryParameters = parseQueryString(queryString);
837 | bindings = newPath.getBindings();
838 | CurrentParameters = extend(queryParameters, bindings);
839 | if (CurrentTargetPath === null && CurrentPath.isEqual(newPath)) {
840 | step();
841 | } else {
842 | previousTargetPath = CurrentTargetPath;
843 | CurrentTargetPath = newPath;
844 | if (previousTargetPath == null) {
845 | step();
846 | }
847 | }
848 | return true;
849 | },
850 | reload: function() {
851 | var saveCurrentPath;
852 | if (!LoadCompleted) {
853 | return this;
854 | }
855 | if (!((CurrentPath != null) && (CurrentPath.node != null))) {
856 | return this;
857 | }
858 | saveCurrentPath = CurrentPath;
859 | CurrentTargetPath = NullPath;
860 | step();
861 | LoadCompleted = false;
862 | CurrentTargetPath = CurrentPath = saveCurrentPath;
863 | step();
864 | return this;
865 | },
866 | observe: function() {
867 | var args, callback, keys, observable;
868 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
869 | if (IgnoreObservables) {
870 | return IgnoreObservables = false;
871 | }
872 | callback = args.pop();
873 | if (!isFunction(callback)) {
874 | callback = (function() {});
875 | }
876 | if (args.length > 0) {
877 | if (args.length === 1 && isArray(args[0])) {
878 | keys = args[0];
879 | } else {
880 | keys = args;
881 | }
882 | return Finch.observe(function(paramAccessor) {
883 | var key, values;
884 | values = (function() {
885 | var _i, _len, _results;
886 | _results = [];
887 | for (_i = 0, _len = keys.length; _i < _len; _i++) {
888 | key = keys[_i];
889 | _results.push(paramAccessor(key));
890 | }
891 | return _results;
892 | })();
893 | return callback.apply(null, values);
894 | });
895 | } else {
896 | observable = new ParameterObservable(callback);
897 | return peek(CurrentPath.parameterObservables).push(observable);
898 | }
899 | },
900 | abort: function() {
901 | return CurrentTargetPath = null;
902 | },
903 | listen: function() {
904 | if (!HashListening) {
905 | if ("onhashchange" in window) {
906 | if (isFunction(window.addEventListener)) {
907 | window.addEventListener("hashchange", hashChangeListener, true);
908 | HashListening = true;
909 | } else if (isFunction(window.attachEvent)) {
910 | window.attachEvent("hashchange", hashChangeListener);
911 | HashListening = true;
912 | }
913 | }
914 | if (!HashListening) {
915 | HashInterval = setInterval(hashChangeListener, 33);
916 | HashListening = true;
917 | }
918 | hashChangeListener();
919 | }
920 | return HashListening;
921 | },
922 | ignore: function() {
923 | if (HashListening) {
924 | if (HashInterval !== null) {
925 | clearInterval(HashInterval);
926 | HashInterval = null;
927 | HashListening = false;
928 | } else if ("onhashchange" in window) {
929 | if (isFunction(window.removeEventListener)) {
930 | window.removeEventListener("hashchange", hashChangeListener, true);
931 | HashListening = false;
932 | } else if (isFunction(window.detachEvent)) {
933 | window.detachEvent("hashchange", hashChangeListener);
934 | HashListening = false;
935 | }
936 | }
937 | }
938 | return !HashListening;
939 | },
940 | navigate: function(uri, queryParams, doUpdate) {
941 | var builtUri, currentQueryParams, currentQueryString, currentUri, key, piece, queryString, slashIndex, uriParamString, uriQueryParams, value, _ref1, _ref2;
942 | _ref1 = getHash().split("?", 2), currentUri = _ref1[0], currentQueryString = _ref1[1];
943 | if (currentUri == null) {
944 | currentUri = "";
945 | }
946 | if (currentQueryString == null) {
947 | currentQueryString = "";
948 | }
949 | if (currentUri.slice(0, 1) === "#") {
950 | currentUri = currentUri.slice(1);
951 | }
952 | currentUri = unescape(currentUri);
953 | currentQueryParams = parseQueryString(currentQueryString);
954 | if (isBoolean(queryParams)) {
955 | doUpdate = queryParams;
956 | }
957 | if (isObject(uri)) {
958 | queryParams = uri;
959 | }
960 | if (!isString(uri)) {
961 | uri = "";
962 | }
963 | if (!isObject(queryParams)) {
964 | queryParams = {};
965 | }
966 | if (!isBoolean(doUpdate)) {
967 | doUpdate = false;
968 | }
969 | uri = trim(uri);
970 | if (uri.length === 0) {
971 | uri = null;
972 | }
973 | if (doUpdate) {
974 | (function() {
975 | var key, newQueryParams, value;
976 | newQueryParams = {};
977 | for (key in currentQueryParams) {
978 | value = currentQueryParams[key];
979 | newQueryParams[unescape(key)] = unescape(value);
980 | }
981 | return currentQueryParams = newQueryParams;
982 | })();
983 | queryParams = extend(currentQueryParams, queryParams);
984 | }
985 | if (uri === null) {
986 | uri = currentUri;
987 | }
988 | _ref2 = uri.split("?", 2), uri = _ref2[0], uriParamString = _ref2[1];
989 | if (uri.slice(0, 1) === "#") {
990 | uri = uri.slice(1);
991 | }
992 | if (startsWith(uri, "./") || startsWith(uri, "../")) {
993 | builtUri = currentUri;
994 | while (startsWith(uri, "./") || startsWith(uri, "../")) {
995 | slashIndex = uri.indexOf("/");
996 | piece = uri.slice(0, slashIndex);
997 | uri = uri.slice(slashIndex + 1);
998 | if (piece === "..") {
999 | builtUri = builtUri.slice(0, builtUri.lastIndexOf("/"));
1000 | }
1001 | }
1002 | uri = uri.length > 0 ? "" + builtUri + "/" + uri : builtUri;
1003 | }
1004 | uriQueryParams = isString(uriParamString) ? parseQueryString(uriParamString) : {};
1005 | queryParams = extend(uriQueryParams, queryParams);
1006 | queryParams = compact(queryParams);
1007 | uri = escape(uri);
1008 | queryString = ((function() {
1009 | var _results;
1010 | _results = [];
1011 | for (key in queryParams) {
1012 | value = queryParams[key];
1013 | _results.push(escape(key) + "=" + escape(value));
1014 | }
1015 | return _results;
1016 | })()).join("&");
1017 | if (queryString.length > 0) {
1018 | uri += "?" + queryString;
1019 | }
1020 | return setHash(uri);
1021 | },
1022 | reset: function() {
1023 | Finch.options({
1024 | CoerceParameterTypes: false
1025 | });
1026 | CurrentTargetPath = NullPath;
1027 | step();
1028 | Finch.ignore();
1029 | resetGlobals();
1030 | },
1031 | options: function(newOptions) {
1032 | return extend(Options, newOptions);
1033 | }
1034 | };
1035 |
1036 | this.Finch = Finch;
1037 |
1038 | }).call(this);
1039 |
--------------------------------------------------------------------------------
/finch.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | Finch.js - Powerfully simple javascript routing
3 | by Rick Allen (stoodder) and Greg Smith (smrq)
4 |
5 | Version 0.5.15
6 | Full source at https://github.com/stoodder/finchjs
7 | Copyright (c) 2011 RokkinCat, http://www.rokkincat.com
8 |
9 | MIT License, https://github.com/stoodder/finchjs/blob/master/LICENSE.md
10 | This file is generated by `cake build`, do not edit it by hand.
11 | */
12 |
13 |
14 | (function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,$,_,ab,bb,cb,db,eb,fb,gb,hb=[].slice;N=function(a){return typeof a==typeof{}&&null!==a},L=function(a){return"[object Function]"===Object.prototype.toString.call(a)},K=function(a){return"[object Boolean]"===Object.prototype.toString.call(a)},J=function(a){return"[object Array]"===Object.prototype.toString.call(a)},O=function(a){return"[object String]"===Object.prototype.toString.call(a)},M=function(a){return"[object Number]"===Object.prototype.toString.call(a)},eb=function(a){return a.replace(/^\s+/,"").replace(/\s+$/,"")},fb=function(a){return a.replace(/^\//,"").replace(/\/$/,"")},$=function(a,b){return 0===a.indexOf(b)},B=function(a,b){return-1!==a.indexOf(b,a.length-b.length)},y=function(a,b){var c,d,e;if(L(a.indexOf))return-1!==a.indexOf(b);if(J(a))for(d=0,e=a.length;e>d;d++)if(c=a[d],c===b)return!0;return!1},V=function(a){return a[a.length-1]},z=function(a,b){return a.split(b).length-1},P=function(a){var b,c;c=[];for(b in a)c.push(b);return c},Q=function(a){var b,c,d;d=[];for(b in a)c=a[b],d.push(c);return d},C=function(a,b){var c,d;N(a)||(a={}),N(b)||(b={});for(c in b)d=b[c],a[c]=d;return a},w=function(a){var b,c,d;N(a)||(a={}),c={};for(b in a)d=a[b],null!=d&&(c[b]=d);return c},R=function(a,b){var c,d;for(c in a)if(d=a[c],b[c]!==d)return!1;for(c in b)if(d=b[c],a[c]!==d)return!1;return!0},v=function(a,b){var c,d,e,f;if(a.length!==b.length)return!1;for(c=e=0,f=a.length;f>e;c=++e)if(d=a[c],b[c]!==d)return!1;return!0},A=function(a,b){var c,d,e;null==a&&(a={}),null==b&&(b={}),d={};for(c in a)e=a[c],b[c]!==e&&(d[c]=b[c]);for(c in b)e=b[c],a[c]!==e&&(d[c]=e);return d},x=null!=(gb=window.console)?gb:{},null==x.log&&(x.log=function(){}),null==x.warn&&(x.warn=function(){}),n=function(){function a(a){var b,c;c=a.components,b=a.childIndex,this.components=null!=c?c:[],this.childIndex=null!=b?b:0}return a}(),q=function(){function a(a){var b,c,d,e;e=null!=a?a:{},b=e.name,c=e.nodeType,d=e.parent,this.name=null!=b?b:"",this.nodeType=null!=c?c:null,this.parent=null!=d?d:null,this.routeSettings=null,this.childLiterals={},this.childVariable=null,this.bindings=[]}return a}(),s=function(){function a(a){var b,c,d,e,f,g;g=null!=a?a:{},d=g.setup,e=g.teardown,c=g.load,f=g.unload,b=g.context,this.setup=L(d)?d:function(){},this.load=L(c)?c:function(){},this.unload=L(f)?f:function(){},this.teardown=L(e)?e:function(){},this.context=N(b)?b:{}}return a}(),r=function(){function a(a){var b,c,d,e;e=null!=a?a:{},c=e.node,b=e.boundValues,d=e.parameterObservables,this.node=null!=c?c:null,this.boundValues=null!=b?b:[],this.parameterObservables=null!=d?d:[[]]}return a.prototype.getBindings=function(){var a,b,c,d,e,f;for(b={},f=this.node.bindings,c=d=0,e=f.length;e>d;c=++d)a=f[c],b[a]=this.boundValues[c];return S(b)},a.prototype.isEqual=function(a){return null!=a&&this.node===a.node&&v(this.boundValues,a.boundValues)},a.prototype.isRoot=function(){return null==this.node.parent},a.prototype.getParent=function(){var b,c,d,e,f;return null==this.node?null:(b=null!=(e=null!=(f=this.node.parent)?f.bindings.length:void 0)?e:0,c=this.boundValues.slice(0,b),d=this.parameterObservables.slice(0,-1),new a({node:this.node.parent,boundValues:c,parameterObservables:d}))},a.prototype.getChild=function(a){for(var b;null!=a&&!this.isEqual(b=a.getParent());)a=b;return a.parameterObservables=this.parameterObservables.slice(0),a.parameterObservables.push([]),a},a}(),m=function(){function a(a){this.callback=a,L(this.callback)||(this.callback=function(){}),this.dependencies=[],this.initialized=!1}return a.prototype.notify=function(a){var b,c=this;return b=function(){var b,d,e,f;if(!c.initialized)return!0;for(f=c.dependencies,d=0,e=f.length;e>d;d++)if(b=f[d],y(a,b))return!0;return!1}(),b?this.trigger():void 0},a.prototype.trigger=function(){var a,c=this;return this.dependencies=[],a=function(a){return y(c.dependencies,a)||c.dependencies.push(a),b[a]},this.callback(a),this.initialized=!0},a}(),k=new r({node:null}),j={Literal:"Literal",Variable:"Variable"},T=function(a){var b,c,d,e,f,g,h,i;if(a=O(a)?eb(a):"",d={},""!==a)for(h=a.split("&"),f=0,g=h.length;g>f;f++)c=h[f],i=c.split("=",2),b=i[0],e=i[1],d[b]=e;return S(d)},H=function(){var a;return"#"+(null!=(a=window.location.href.split("#",2)[1])?a:"")},Y=function(a){return O(a)||(a=""),a=eb(a),"#"===a.slice(0,1)&&(a=a.slice(1)),window.location.hash=a},S=function(a){var b,c;if(N(a)||(a={}),l.CoerceParameterTypes)for(b in a)c=a[b],"true"===c?c=!0:"false"===c?c=!1:/^[0-9]+$/.test(c)?c=parseInt(c):/^[0-9]+\.[0-9]*$/.test(c)&&(c=parseFloat(c)),a[b]=c;return a},Z=function(a){var b;return a=fb(a),b=""===a?[]:a.split("/"),b.unshift("/"),b},U=function(a){var b,c,d,e,f,g,h,i,j;for(f=y(a,"[")||y(a,"]"),f&&!function(){var b,c;return c=z(a,"["),1!==c?(c>1&&x.warn('[FINCH] Parsing failed on "'+a+'": Extra ['),1>c&&x.warn('[FINCH] Parsing failed on "'+a+'": Missing ['),null):(b=z(a,"]"),1!==b?(b>1&&x.warn('[FINCH] Parsing failed on "'+a+'": Extra ]'),1>b&&x.warn('[FINCH] Parsing failed on "'+a+'": Missing ]'),null):$(a,"[")?void 0:(x.warn('[FINCH] Parsing failed on "'+a+'": [ not at beginning'),null))}(),e=a.replace(/[\[\]]/g,""),d=""===e?[]:Z(e),i=0,j=d.length;j>i;i++)if(c=d[i],""===c)return x.warn('[FINCH] Parsing failed on "'+a+'": Blank component'),null;if(b=0,f){if(h=a.split("]")[0],g=Z(h.replace("[","")),g[g.length-1]!==d[g.length-1])return x.warn('[FINCH] Parsing failed on "'+a+'": ] in the middle of a component'),null;if(g.length===d.length)return x.warn('[FINCH] Parsing failed on "'+a+'": No child components'),null;b=g.length}return new n({components:d,childIndex:b})},G=function(a){return $(a,":")?j.Variable:j.Literal},F=function(a){switch(G(a)){case j.Literal:return a;case j.Variable:return a.slice(1)}},u=function(a,b,c){var d,e,f,g,h;return f=b.components,e=b.childIndex,g=a,d=[],(h=function(i,k){var l,m,n,o,p;if(k===e&&(g=i),b.components.length<=0)return i.parent=g,i.bindings=d,i.routeSettings=new s(c);switch(l=f.shift(),n=G(l),m=F(l),n){case j.Literal:o=null!=(p=i.childLiterals)[m]?(p=i.childLiterals)[m]:p[m]=new q({name:""+i.name+l+"/",nodeType:n,parent:a});break;case j.Variable:o=null!=i.childVariable?i.childVariable:i.childVariable=new q({name:""+i.name+l+"/",nodeType:n,parent:a}),d.push(m)}return h(o,k+1)})(a,0)},E=function(a,b){var c,d,e;return e=Z(b),c=[],(d=function(a,b){var e,f;if(b.length<=0&&null!=a.routeSettings)return new r({node:a,boundValues:c});if(e=b[0],null!=a.childLiterals[e]&&(f=d(a.childLiterals[e],b.slice(1)),null!=f))return f;if(null!=a.childVariable){if(c.push(e),f=d(a.childVariable,b.slice(1)),null!=f)return f;c.pop()}return null})(a,e)},D=function(a,b){var c,d,e,f,g;for(d=[],e=b;null!=e;)d.push(e),e=e.getParent();for(e=a;null!=e;){for(f=0,g=d.length;g>f;f++)if(c=d[f],e.isEqual(c))return e;e=e.getParent()}return null},p=c=d=null,o=b=null,f=a=null,g=!1,h=t=!1,i=!1,l={CoerceParameterTypes:!1},(W=function(){return p=new q({name:"*"}),c=k,o={},b={},d=null,f=null,a=null,g=!1,h=!1,t=!1,i=!1})(),_=function(){var a;return null===d?X():i?db():d.isEqual(c)?ab():(a=D(c,d),c.isEqual(a)?bb():cb())},bb=function(){var a,b,e,f,g,h,i,j,k,l;return t=!0,f=(null!=(i=null!=(j=c.node)?j.routeSettings:void 0)?i:{context:null}).context,c=c.getChild(d),l=null!=(k=c.node.routeSettings)?k:{},b=l.context,h=l.setup,e=l.load,null==b&&(b={}),b.parent=f,null==h&&(h=function(){}),null==e&&(e=function(){}),a=c.getBindings(),g=function(){return _()},2===h.length?h.call(b,a,g):(h.call(b,a),g())},ab=function(){var a,b,e,f,g,h,j;return f=function(){return i=!0,d=null,_()},null==c.node?f():(j=null!=(h=c.node.routeSettings)?h:{},b=j.context,g=j.setup,e=j.load,null==b&&(b={}),null==g&&(g=function(){}),null==e&&(e=function(){}),a=c.getBindings(),2===e.length?e.call(b,a,f):(e.call(b,a),f()))},db=function(){var a,b,d,e,f,g;return i=!1,d=function(){return _()},g=null!=(f=c.node.routeSettings)?f:{},b=g.context,e=g.unload,null==b&&(b={}),null==e&&(e=function(){}),a=c.getBindings(),2===e.length?e.call(b,a,d):(e.call(b,a),d())},cb=function(){var a,b,d,e,f,g;return t=!1,g=null!=(f=c.node.routeSettings)?f:{},b=g.context,e=g.teardown,null==b&&(b={}),null==e&&(e=function(){}),a=c.getBindings(),d=function(){return c=c.getParent(),_()},2===e.length?e.call(b,a,d):(e.call(b,a),d())},X=function(){var a,d,e,f,g,h,i;for(a=P(A(o,b)),o=b,h=c.parameterObservables,i=[],f=0,g=h.length;g>f;f++)e=h[f],i.push(function(){var b,c,f;for(f=[],b=0,c=e.length;c>b;b++)d=e[b],f.push(d.notify(a));return f}());return i},I=function(){var b;return b=H(),$(b,"#")&&(b=b.slice(1)),b=unescape(b),b!==a?e.call(b)?a=b:Y(null!=a?a:""):void 0},e={route:function(a,b){var c,d;return L(b)&&(c=b,b={setup:c},b.load=2===c.length?function(a,b){return t?b():(h=!0,c(a,b))}:function(a){return t?void 0:(h=!0,c(a))}),N(b)||(b={}),O(a)||(a=""),a=eb(a),a.length>0||(a="/"),d=U(a),null==d?!1:(u(p,d,b),this)},call:function(a){var e,f,g,h,i,j;return O(a)||(a="/"),""===a&&(a="/"),j=a.split("?",2),a=j[0],i=j[1],f=E(p,a),null==f?(x.warn("[FINCH] Could not find route for: "+a),!1):(h=T(i),e=f.getBindings(),b=C(h,e),null===d&&c.isEqual(f)?_():(g=d,d=f,null==g&&_()),!0)},reload:function(){var a;return i?null==c||null==c.node?this:(a=c,d=k,_(),i=!1,d=c=a,_(),this):this},observe:function(){var a,b,d,f;return a=1<=arguments.length?hb.call(arguments,0):[],h?h=!1:(b=a.pop(),L(b)||(b=function(){}),a.length>0?(d=1===a.length&&J(a[0])?a[0]:a,e.observe(function(a){var c,e;return e=function(){var b,e,f;for(f=[],b=0,e=d.length;e>b;b++)c=d[b],f.push(a(c));return f}(),b.apply(null,e)})):(f=new m(b),V(c.parameterObservables).push(f)))},abort:function(){return d=null},listen:function(){return g||("onhashchange"in window&&(L(window.addEventListener)?(window.addEventListener("hashchange",I,!0),g=!0):L(window.attachEvent)&&(window.attachEvent("hashchange",I),g=!0)),g||(f=setInterval(I,33),g=!0),I()),g},ignore:function(){return g&&(null!==f?(clearInterval(f),f=null,g=!1):"onhashchange"in window&&(L(window.removeEventListener)?(window.removeEventListener("hashchange",I,!0),g=!1):L(window.detachEvent)&&(window.detachEvent("hashchange",I),g=!1))),!g},navigate:function(a,b,c){var d,e,f,g,h,i,j,k,l,m,n,o,p;if(o=H().split("?",2),g=o[0],f=o[1],null==g&&(g=""),null==f&&(f=""),"#"===g.slice(0,1)&&(g=g.slice(1)),g=unescape(g),e=T(f),K(b)&&(c=b),N(a)&&(b=a),O(a)||(a=""),N(b)||(b={}),K(c)||(c=!1),a=eb(a),0===a.length&&(a=null),c&&(!function(){var a,b,c;b={};for(a in e)c=e[a],b[unescape(a)]=unescape(c);return e=b}(),b=C(e,b)),null===a&&(a=g),p=a.split("?",2),a=p[0],l=p[1],"#"===a.slice(0,1)&&(a=a.slice(1)),$(a,"./")||$(a,"../")){for(d=g;$(a,"./")||$(a,"../");)k=a.indexOf("/"),i=a.slice(0,k),a=a.slice(k+1),".."===i&&(d=d.slice(0,d.lastIndexOf("/")));a=a.length>0?""+d+"/"+a:d}return m=O(l)?T(l):{},b=C(m,b),b=w(b),a=escape(a),j=function(){var a;a=[];for(h in b)n=b[h],a.push(escape(h)+"="+escape(n));return a}().join("&"),j.length>0&&(a+="?"+j),Y(a)},reset:function(){e.options({CoerceParameterTypes:!1}),d=k,_(),e.ignore(),W()},options:function(a){return C(l,a)}},this.Finch=e}).call(this);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "finch",
3 | "version": "0.5.14",
4 | "engines": {
5 | "node": ">= 0.10.10"
6 | },
7 | "dependencies": {
8 | "coffee-script": "~1.6.2"
9 | },
10 | "devDependencies": {
11 | "grunt": "~0.4.2",
12 | "grunt-contrib-uglify": "~0.2.7",
13 | "grunt-contrib-watch": "~0.5.3",
14 | "grunt-contrib-coffee": "~0.8.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/scripts/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stoodder/finchjs/7c23ce7ea6ae9d222450daf7eab16a958f717637/scripts/.DS_Store
--------------------------------------------------------------------------------
/scripts/banner.js:
--------------------------------------------------------------------------------
1 | /*
2 | Finch.js - Powerfully simple javascript routing
3 | by Rick Allen (stoodder) and Greg Smith (smrq)
4 |
5 | Version 0.5.15
6 | Full source at https://github.com/stoodder/finchjs
7 | Copyright (c) 2011 RokkinCat, http://www.rokkincat.com
8 |
9 | MIT License, https://github.com/stoodder/finchjs/blob/master/LICENSE.md
10 | This file is generated by `cake build`, do not edit it by hand.
11 | */
12 |
13 |
14 |
--------------------------------------------------------------------------------
/tests/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stoodder/finchjs/7c23ce7ea6ae9d222450daf7eab16a958f717637/tests/.DS_Store
--------------------------------------------------------------------------------
/tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tests for Finch.js
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/project_advanced.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
86 |
87 |
88 | clear |
89 | project/1234 |
90 | project/1234/milestone |
91 | project/1234/miletsone/5678 |
92 | invalid route
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/tests/project_basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
44 |
45 |
46 | clear |
47 | project/1234 |
48 | project/1234/milestone |
49 | project/1234/miletsone/5678 |
50 | invalid route
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/tests/qunit.css:
--------------------------------------------------------------------------------
1 | /**
2 | * QUnit v1.3.0pre - A JavaScript Unit Testing Framework
3 | *
4 | * http://docs.jquery.com/QUnit
5 | *
6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer
7 | * Dual licensed under the MIT (MIT-LICENSE.txt)
8 | * or GPL (GPL-LICENSE.txt) licenses.
9 | */
10 |
11 | /** Font Family and Sizes */
12 |
13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
15 | }
16 |
17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
18 | #qunit-tests { font-size: smaller; }
19 |
20 |
21 | /** Resets */
22 |
23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult {
24 | margin: 0;
25 | padding: 0;
26 | }
27 |
28 |
29 | /** Header */
30 |
31 | #qunit-header {
32 | padding: 0.5em 0 0.5em 1em;
33 |
34 | color: #8699a4;
35 | background-color: #0d3349;
36 |
37 | font-size: 1.5em;
38 | line-height: 1em;
39 | font-weight: normal;
40 |
41 | border-radius: 15px 15px 0 0;
42 | -moz-border-radius: 15px 15px 0 0;
43 | -webkit-border-top-right-radius: 15px;
44 | -webkit-border-top-left-radius: 15px;
45 | }
46 |
47 | #qunit-header a {
48 | text-decoration: none;
49 | color: #c2ccd1;
50 | }
51 |
52 | #qunit-header a:hover,
53 | #qunit-header a:focus {
54 | color: #fff;
55 | }
56 |
57 | #qunit-banner {
58 | height: 5px;
59 | }
60 |
61 | #qunit-testrunner-toolbar {
62 | padding: 0.5em 0 0.5em 2em;
63 | color: #5E740B;
64 | background-color: #eee;
65 | }
66 |
67 | #qunit-userAgent {
68 | padding: 0.5em 0 0.5em 2.5em;
69 | background-color: #2b81af;
70 | color: #fff;
71 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
72 | }
73 |
74 |
75 | /** Tests: Pass/Fail */
76 |
77 | #qunit-tests {
78 | list-style-position: inside;
79 | }
80 |
81 | #qunit-tests li {
82 | padding: 0.4em 0.5em 0.4em 2.5em;
83 | border-bottom: 1px solid #fff;
84 | list-style-position: inside;
85 | }
86 |
87 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
88 | display: none;
89 | }
90 |
91 | #qunit-tests li strong {
92 | cursor: pointer;
93 | }
94 |
95 | #qunit-tests li a {
96 | padding: 0.5em;
97 | color: #c2ccd1;
98 | text-decoration: none;
99 | }
100 | #qunit-tests li a:hover,
101 | #qunit-tests li a:focus {
102 | color: #000;
103 | }
104 |
105 | #qunit-tests ol {
106 | margin-top: 0.5em;
107 | padding: 0.5em;
108 |
109 | background-color: #fff;
110 |
111 | border-radius: 15px;
112 | -moz-border-radius: 15px;
113 | -webkit-border-radius: 15px;
114 |
115 | box-shadow: inset 0px 2px 13px #999;
116 | -moz-box-shadow: inset 0px 2px 13px #999;
117 | -webkit-box-shadow: inset 0px 2px 13px #999;
118 | }
119 |
120 | #qunit-tests table {
121 | border-collapse: collapse;
122 | margin-top: .2em;
123 | }
124 |
125 | #qunit-tests th {
126 | text-align: right;
127 | vertical-align: top;
128 | padding: 0 .5em 0 0;
129 | }
130 |
131 | #qunit-tests td {
132 | vertical-align: top;
133 | }
134 |
135 | #qunit-tests pre {
136 | margin: 0;
137 | white-space: pre-wrap;
138 | word-wrap: break-word;
139 | }
140 |
141 | #qunit-tests del {
142 | background-color: #e0f2be;
143 | color: #374e0c;
144 | text-decoration: none;
145 | }
146 |
147 | #qunit-tests ins {
148 | background-color: #ffcaca;
149 | color: #500;
150 | text-decoration: none;
151 | }
152 |
153 | /*** Test Counts */
154 |
155 | #qunit-tests b.counts { color: black; }
156 | #qunit-tests b.passed { color: #5E740B; }
157 | #qunit-tests b.failed { color: #710909; }
158 |
159 | #qunit-tests li li {
160 | margin: 0.5em;
161 | padding: 0.4em 0.5em 0.4em 0.5em;
162 | background-color: #fff;
163 | border-bottom: none;
164 | list-style-position: inside;
165 | }
166 |
167 | /*** Passing Styles */
168 |
169 | #qunit-tests li li.pass {
170 | color: #5E740B;
171 | background-color: #fff;
172 | border-left: 26px solid #C6E746;
173 | }
174 |
175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
176 | #qunit-tests .pass .test-name { color: #366097; }
177 |
178 | #qunit-tests .pass .test-actual,
179 | #qunit-tests .pass .test-expected { color: #999999; }
180 |
181 | #qunit-banner.qunit-pass { background-color: #C6E746; }
182 |
183 | /*** Failing Styles */
184 |
185 | #qunit-tests li li.fail {
186 | color: #710909;
187 | background-color: #fff;
188 | border-left: 26px solid #EE5757;
189 | white-space: pre;
190 | }
191 |
192 | #qunit-tests > li:last-child {
193 | border-radius: 0 0 15px 15px;
194 | -moz-border-radius: 0 0 15px 15px;
195 | -webkit-border-bottom-right-radius: 15px;
196 | -webkit-border-bottom-left-radius: 15px;
197 | }
198 |
199 | #qunit-tests .fail { color: #000000; background-color: #EE5757; }
200 | #qunit-tests .fail .test-name,
201 | #qunit-tests .fail .module-name { color: #000000; }
202 |
203 | #qunit-tests .fail .test-actual { color: #EE5757; }
204 | #qunit-tests .fail .test-expected { color: green; }
205 |
206 | #qunit-banner.qunit-fail { background-color: #EE5757; }
207 |
208 |
209 | /** Result */
210 |
211 | #qunit-testresult {
212 | padding: 0.5em 0.5em 0.5em 2.5em;
213 |
214 | color: #2b81af;
215 | background-color: #D2E0E6;
216 |
217 | border-bottom: 1px solid white;
218 | }
219 |
220 | /** Fixture */
221 |
222 | #qunit-fixture {
223 | position: absolute;
224 | top: -10000px;
225 | left: -10000px;
226 | }
227 |
--------------------------------------------------------------------------------
/tests/qunit.js:
--------------------------------------------------------------------------------
1 | /**
2 | * QUnit v1.3.0pre - A JavaScript Unit Testing Framework
3 | *
4 | * http://docs.jquery.com/QUnit
5 | *
6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer
7 | * Dual licensed under the MIT (MIT-LICENSE.txt)
8 | * or GPL (GPL-LICENSE.txt) licenses.
9 | */
10 |
11 | (function(window) {
12 |
13 | var defined = {
14 | setTimeout: typeof window.setTimeout !== "undefined",
15 | sessionStorage: (function() {
16 | try {
17 | return !!sessionStorage.getItem;
18 | } catch(e) {
19 | return false;
20 | }
21 | })()
22 | };
23 |
24 | var testId = 0,
25 | toString = Object.prototype.toString,
26 | hasOwn = Object.prototype.hasOwnProperty;
27 |
28 | var Test = function(name, testName, expected, testEnvironmentArg, async, callback) {
29 | this.name = name;
30 | this.testName = testName;
31 | this.expected = expected;
32 | this.testEnvironmentArg = testEnvironmentArg;
33 | this.async = async;
34 | this.callback = callback;
35 | this.assertions = [];
36 | };
37 | Test.prototype = {
38 | init: function() {
39 | var tests = id("qunit-tests");
40 | if (tests) {
41 | var b = document.createElement("strong");
42 | b.innerHTML = "Running " + this.name;
43 | var li = document.createElement("li");
44 | li.appendChild( b );
45 | li.className = "running";
46 | li.id = this.id = "test-output" + testId++;
47 | tests.appendChild( li );
48 | }
49 | },
50 | setup: function() {
51 | if (this.module != config.previousModule) {
52 | if ( config.previousModule ) {
53 | runLoggingCallbacks('moduleDone', QUnit, {
54 | name: config.previousModule,
55 | failed: config.moduleStats.bad,
56 | passed: config.moduleStats.all - config.moduleStats.bad,
57 | total: config.moduleStats.all
58 | } );
59 | }
60 | config.previousModule = this.module;
61 | config.moduleStats = { all: 0, bad: 0 };
62 | runLoggingCallbacks( 'moduleStart', QUnit, {
63 | name: this.module
64 | } );
65 | }
66 |
67 | config.current = this;
68 | this.testEnvironment = extend({
69 | setup: function() {},
70 | teardown: function() {}
71 | }, this.moduleTestEnvironment);
72 | if (this.testEnvironmentArg) {
73 | extend(this.testEnvironment, this.testEnvironmentArg);
74 | }
75 |
76 | runLoggingCallbacks( 'testStart', QUnit, {
77 | name: this.testName,
78 | module: this.module
79 | });
80 |
81 | // allow utility functions to access the current test environment
82 | // TODO why??
83 | QUnit.current_testEnvironment = this.testEnvironment;
84 |
85 | try {
86 | if ( !config.pollution ) {
87 | saveGlobal();
88 | }
89 |
90 | this.testEnvironment.setup.call(this.testEnvironment);
91 | } catch(e) {
92 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message );
93 | }
94 | },
95 | run: function() {
96 | config.current = this;
97 | if ( this.async ) {
98 | QUnit.stop();
99 | }
100 |
101 | if ( config.notrycatch ) {
102 | this.callback.call(this.testEnvironment);
103 | return;
104 | }
105 | try {
106 | this.callback.call(this.testEnvironment);
107 | } catch(e) {
108 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback);
109 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) );
110 | // else next test will carry the responsibility
111 | saveGlobal();
112 |
113 | // Restart the tests if they're blocking
114 | if ( config.blocking ) {
115 | QUnit.start();
116 | }
117 | }
118 | },
119 | teardown: function() {
120 | config.current = this;
121 | try {
122 | this.testEnvironment.teardown.call(this.testEnvironment);
123 | checkPollution();
124 | } catch(e) {
125 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message );
126 | }
127 | },
128 | finish: function() {
129 | config.current = this;
130 | if ( this.expected != null && this.expected != this.assertions.length ) {
131 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" );
132 | }
133 |
134 | var good = 0, bad = 0,
135 | tests = id("qunit-tests");
136 |
137 | config.stats.all += this.assertions.length;
138 | config.moduleStats.all += this.assertions.length;
139 |
140 | if ( tests ) {
141 | var ol = document.createElement("ol");
142 |
143 | for ( var i = 0; i < this.assertions.length; i++ ) {
144 | var assertion = this.assertions[i];
145 |
146 | var li = document.createElement("li");
147 | li.className = assertion.result ? "pass" : "fail";
148 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed");
149 | ol.appendChild( li );
150 |
151 | if ( assertion.result ) {
152 | good++;
153 | } else {
154 | bad++;
155 | config.stats.bad++;
156 | config.moduleStats.bad++;
157 | }
158 | }
159 |
160 | // store result when possible
161 | if ( QUnit.config.reorder && defined.sessionStorage ) {
162 | if (bad) {
163 | sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad);
164 | } else {
165 | sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName);
166 | }
167 | }
168 |
169 | if (bad == 0) {
170 | ol.style.display = "none";
171 | }
172 |
173 | var b = document.createElement("strong");
174 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")";
175 |
176 | var a = document.createElement("a");
177 | a.innerHTML = "Rerun";
178 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") });
179 |
180 | addEvent(b, "click", function() {
181 | var next = b.nextSibling.nextSibling,
182 | display = next.style.display;
183 | next.style.display = display === "none" ? "block" : "none";
184 | });
185 |
186 | addEvent(b, "dblclick", function(e) {
187 | var target = e && e.target ? e.target : window.event.srcElement;
188 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) {
189 | target = target.parentNode;
190 | }
191 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
192 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") });
193 | }
194 | });
195 |
196 | var li = id(this.id);
197 | li.className = bad ? "fail" : "pass";
198 | li.removeChild( li.firstChild );
199 | li.appendChild( b );
200 | li.appendChild( a );
201 | li.appendChild( ol );
202 |
203 | } else {
204 | for ( var i = 0; i < this.assertions.length; i++ ) {
205 | if ( !this.assertions[i].result ) {
206 | bad++;
207 | config.stats.bad++;
208 | config.moduleStats.bad++;
209 | }
210 | }
211 | }
212 |
213 | try {
214 | QUnit.reset();
215 | } catch(e) {
216 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset);
217 | }
218 |
219 | runLoggingCallbacks( 'testDone', QUnit, {
220 | name: this.testName,
221 | module: this.module,
222 | failed: bad,
223 | passed: this.assertions.length - bad,
224 | total: this.assertions.length
225 | } );
226 | },
227 |
228 | queue: function() {
229 | var test = this;
230 | synchronize(function() {
231 | test.init();
232 | });
233 | function run() {
234 | // each of these can by async
235 | synchronize(function() {
236 | test.setup();
237 | });
238 | synchronize(function() {
239 | test.run();
240 | });
241 | synchronize(function() {
242 | test.teardown();
243 | });
244 | synchronize(function() {
245 | test.finish();
246 | });
247 | }
248 | // defer when previous test run passed, if storage is available
249 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName);
250 | if (bad) {
251 | run();
252 | } else {
253 | synchronize(run, true);
254 | };
255 | }
256 |
257 | };
258 |
259 | var QUnit = {
260 |
261 | // call on start of module test to prepend name to all tests
262 | module: function(name, testEnvironment) {
263 | config.currentModule = name;
264 | config.currentModuleTestEnviroment = testEnvironment;
265 | },
266 |
267 | asyncTest: function(testName, expected, callback) {
268 | if ( arguments.length === 2 ) {
269 | callback = expected;
270 | expected = null;
271 | }
272 |
273 | QUnit.test(testName, expected, callback, true);
274 | },
275 |
276 | test: function(testName, expected, callback, async) {
277 | var name = '' + testName + '', testEnvironmentArg;
278 |
279 | if ( arguments.length === 2 ) {
280 | callback = expected;
281 | expected = null;
282 | }
283 | // is 2nd argument a testEnvironment?
284 | if ( expected && typeof expected === 'object') {
285 | testEnvironmentArg = expected;
286 | expected = null;
287 | }
288 |
289 | if ( config.currentModule ) {
290 | name = '' + config.currentModule + ": " + name;
291 | }
292 |
293 | if ( !validTest(config.currentModule + ": " + testName) ) {
294 | return;
295 | }
296 |
297 | var test = new Test(name, testName, expected, testEnvironmentArg, async, callback);
298 | test.module = config.currentModule;
299 | test.moduleTestEnvironment = config.currentModuleTestEnviroment;
300 | test.queue();
301 | },
302 |
303 | /**
304 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
305 | */
306 | expect: function(asserts) {
307 | config.current.expected = asserts;
308 | },
309 |
310 | /**
311 | * Asserts true.
312 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
313 | */
314 | ok: function(a, msg) {
315 | a = !!a;
316 | var details = {
317 | result: a,
318 | message: msg
319 | };
320 | msg = escapeInnerText(msg);
321 | runLoggingCallbacks( 'log', QUnit, details );
322 | config.current.assertions.push({
323 | result: a,
324 | message: msg
325 | });
326 | },
327 |
328 | /**
329 | * Checks that the first two arguments are equal, with an optional message.
330 | * Prints out both actual and expected values.
331 | *
332 | * Prefered to ok( actual == expected, message )
333 | *
334 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." );
335 | *
336 | * @param Object actual
337 | * @param Object expected
338 | * @param String message (optional)
339 | */
340 | equal: function(actual, expected, message) {
341 | QUnit.push(expected == actual, actual, expected, message);
342 | },
343 |
344 | notEqual: function(actual, expected, message) {
345 | QUnit.push(expected != actual, actual, expected, message);
346 | },
347 |
348 | deepEqual: function(actual, expected, message) {
349 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message);
350 | },
351 |
352 | notDeepEqual: function(actual, expected, message) {
353 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message);
354 | },
355 |
356 | strictEqual: function(actual, expected, message) {
357 | QUnit.push(expected === actual, actual, expected, message);
358 | },
359 |
360 | notStrictEqual: function(actual, expected, message) {
361 | QUnit.push(expected !== actual, actual, expected, message);
362 | },
363 |
364 | raises: function(block, expected, message) {
365 | var actual, ok = false;
366 |
367 | if (typeof expected === 'string') {
368 | message = expected;
369 | expected = null;
370 | }
371 |
372 | try {
373 | block();
374 | } catch (e) {
375 | actual = e;
376 | }
377 |
378 | if (actual) {
379 | // we don't want to validate thrown error
380 | if (!expected) {
381 | ok = true;
382 | // expected is a regexp
383 | } else if (QUnit.objectType(expected) === "regexp") {
384 | ok = expected.test(actual);
385 | // expected is a constructor
386 | } else if (actual instanceof expected) {
387 | ok = true;
388 | // expected is a validation function which returns true is validation passed
389 | } else if (expected.call({}, actual) === true) {
390 | ok = true;
391 | }
392 | }
393 |
394 | QUnit.ok(ok, message);
395 | },
396 |
397 | start: function(count) {
398 | config.semaphore -= count || 1;
399 | if (config.semaphore > 0) {
400 | // don't start until equal number of stop-calls
401 | return;
402 | }
403 | if (config.semaphore < 0) {
404 | // ignore if start is called more often then stop
405 | config.semaphore = 0;
406 | }
407 | // A slight delay, to avoid any current callbacks
408 | if ( defined.setTimeout ) {
409 | window.setTimeout(function() {
410 | if (config.semaphore > 0) {
411 | return;
412 | }
413 | if ( config.timeout ) {
414 | clearTimeout(config.timeout);
415 | }
416 |
417 | config.blocking = false;
418 | process(true);
419 | }, 13);
420 | } else {
421 | config.blocking = false;
422 | process(true);
423 | }
424 | },
425 |
426 | stop: function(count) {
427 | config.semaphore += count || 1;
428 | config.blocking = true;
429 |
430 | if ( config.testTimeout && defined.setTimeout ) {
431 | clearTimeout(config.timeout);
432 | config.timeout = window.setTimeout(function() {
433 | QUnit.ok( false, "Test timed out" );
434 | config.semaphore = 1;
435 | QUnit.start();
436 | }, config.testTimeout);
437 | }
438 | }
439 | };
440 |
441 | //We want access to the constructor's prototype
442 | (function() {
443 | function F(){};
444 | F.prototype = QUnit;
445 | QUnit = new F();
446 | //Make F QUnit's constructor so that we can add to the prototype later
447 | QUnit.constructor = F;
448 | })();
449 |
450 | // Backwards compatibility, deprecated
451 | QUnit.equals = QUnit.equal;
452 | QUnit.same = QUnit.deepEqual;
453 |
454 | // Maintain internal state
455 | var config = {
456 | // The queue of tests to run
457 | queue: [],
458 |
459 | // block until document ready
460 | blocking: true,
461 |
462 | // when enabled, show only failing tests
463 | // gets persisted through sessionStorage and can be changed in UI via checkbox
464 | hidepassed: false,
465 |
466 | // by default, run previously failed tests first
467 | // very useful in combination with "Hide passed tests" checked
468 | reorder: true,
469 |
470 | // by default, modify document.title when suite is done
471 | altertitle: true,
472 |
473 | urlConfig: ['noglobals', 'notrycatch'],
474 |
475 | //logging callback queues
476 | begin: [],
477 | done: [],
478 | log: [],
479 | testStart: [],
480 | testDone: [],
481 | moduleStart: [],
482 | moduleDone: []
483 | };
484 |
485 | // Load paramaters
486 | (function() {
487 | var location = window.location || { search: "", protocol: "file:" },
488 | params = location.search.slice( 1 ).split( "&" ),
489 | length = params.length,
490 | urlParams = {},
491 | current;
492 |
493 | if ( params[ 0 ] ) {
494 | for ( var i = 0; i < length; i++ ) {
495 | current = params[ i ].split( "=" );
496 | current[ 0 ] = decodeURIComponent( current[ 0 ] );
497 | // allow just a key to turn on a flag, e.g., test.html?noglobals
498 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
499 | urlParams[ current[ 0 ] ] = current[ 1 ];
500 | }
501 | }
502 |
503 | QUnit.urlParams = urlParams;
504 | config.filter = urlParams.filter;
505 |
506 | // Figure out if we're running the tests from a server or not
507 | QUnit.isLocal = !!(location.protocol === 'file:');
508 | })();
509 |
510 | // Expose the API as global variables, unless an 'exports'
511 | // object exists, in that case we assume we're in CommonJS
512 | if ( typeof exports === "undefined" || typeof require === "undefined" ) {
513 | extend(window, QUnit);
514 | window.QUnit = QUnit;
515 | } else {
516 | extend(exports, QUnit);
517 | exports.QUnit = QUnit;
518 | }
519 |
520 | // define these after exposing globals to keep them in these QUnit namespace only
521 | extend(QUnit, {
522 | config: config,
523 |
524 | // Initialize the configuration options
525 | init: function() {
526 | extend(config, {
527 | stats: { all: 0, bad: 0 },
528 | moduleStats: { all: 0, bad: 0 },
529 | started: +new Date,
530 | updateRate: 1000,
531 | blocking: false,
532 | autostart: true,
533 | autorun: false,
534 | filter: "",
535 | queue: [],
536 | semaphore: 0
537 | });
538 |
539 | var tests = id( "qunit-tests" ),
540 | banner = id( "qunit-banner" ),
541 | result = id( "qunit-testresult" );
542 |
543 | if ( tests ) {
544 | tests.innerHTML = "";
545 | }
546 |
547 | if ( banner ) {
548 | banner.className = "";
549 | }
550 |
551 | if ( result ) {
552 | result.parentNode.removeChild( result );
553 | }
554 |
555 | if ( tests ) {
556 | result = document.createElement( "p" );
557 | result.id = "qunit-testresult";
558 | result.className = "result";
559 | tests.parentNode.insertBefore( result, tests );
560 | result.innerHTML = 'Running...
';
561 | }
562 | },
563 |
564 | /**
565 | * Resets the test setup. Useful for tests that modify the DOM.
566 | *
567 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML.
568 | */
569 | reset: function() {
570 | if ( window.jQuery ) {
571 | jQuery( "#qunit-fixture" ).html( config.fixture );
572 | } else {
573 | var main = id( 'qunit-fixture' );
574 | if ( main ) {
575 | main.innerHTML = config.fixture;
576 | }
577 | }
578 | },
579 |
580 | /**
581 | * Trigger an event on an element.
582 | *
583 | * @example triggerEvent( document.body, "click" );
584 | *
585 | * @param DOMElement elem
586 | * @param String type
587 | */
588 | triggerEvent: function( elem, type, event ) {
589 | if ( document.createEvent ) {
590 | event = document.createEvent("MouseEvents");
591 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
592 | 0, 0, 0, 0, 0, false, false, false, false, 0, null);
593 | elem.dispatchEvent( event );
594 |
595 | } else if ( elem.fireEvent ) {
596 | elem.fireEvent("on"+type);
597 | }
598 | },
599 |
600 | // Safe object type checking
601 | is: function( type, obj ) {
602 | return QUnit.objectType( obj ) == type;
603 | },
604 |
605 | objectType: function( obj ) {
606 | if (typeof obj === "undefined") {
607 | return "undefined";
608 |
609 | // consider: typeof null === object
610 | }
611 | if (obj === null) {
612 | return "null";
613 | }
614 |
615 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || '';
616 |
617 | switch (type) {
618 | case 'Number':
619 | if (isNaN(obj)) {
620 | return "nan";
621 | } else {
622 | return "number";
623 | }
624 | case 'String':
625 | case 'Boolean':
626 | case 'Array':
627 | case 'Date':
628 | case 'RegExp':
629 | case 'Function':
630 | return type.toLowerCase();
631 | }
632 | if (typeof obj === "object") {
633 | return "object";
634 | }
635 | return undefined;
636 | },
637 |
638 | push: function(result, actual, expected, message) {
639 | var details = {
640 | result: result,
641 | message: message,
642 | actual: actual,
643 | expected: expected
644 | };
645 |
646 | message = escapeInnerText(message) || (result ? "okay" : "failed");
647 | message = '' + message + "";
648 | expected = escapeInnerText(QUnit.jsDump.parse(expected));
649 | actual = escapeInnerText(QUnit.jsDump.parse(actual));
650 | var output = message + 'Expected: | ' + expected + ' |
';
651 | if (actual != expected) {
652 | output += 'Result: | ' + actual + ' |
';
653 | output += 'Diff: | ' + QUnit.diff(expected, actual) +' |
';
654 | }
655 | if (!result) {
656 | var source = sourceFromStacktrace();
657 | if (source) {
658 | details.source = source;
659 | output += 'Source: | ' + escapeInnerText(source) + ' |
';
660 | }
661 | }
662 | output += "
";
663 |
664 | runLoggingCallbacks( 'log', QUnit, details );
665 |
666 | config.current.assertions.push({
667 | result: !!result,
668 | message: output
669 | });
670 | },
671 |
672 | url: function( params ) {
673 | params = extend( extend( {}, QUnit.urlParams ), params );
674 | var querystring = "?",
675 | key;
676 | for ( key in params ) {
677 | if ( !hasOwn.call( params, key ) ) {
678 | continue;
679 | }
680 | querystring += encodeURIComponent( key ) + "=" +
681 | encodeURIComponent( params[ key ] ) + "&";
682 | }
683 | return window.location.pathname + querystring.slice( 0, -1 );
684 | },
685 |
686 | extend: extend,
687 | id: id,
688 | addEvent: addEvent
689 | });
690 |
691 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later
692 | //Doing this allows us to tell if the following methods have been overwritten on the actual
693 | //QUnit object, which is a deprecated way of using the callbacks.
694 | extend(QUnit.constructor.prototype, {
695 | // Logging callbacks; all receive a single argument with the listed properties
696 | // run test/logs.html for any related changes
697 | begin: registerLoggingCallback('begin'),
698 | // done: { failed, passed, total, runtime }
699 | done: registerLoggingCallback('done'),
700 | // log: { result, actual, expected, message }
701 | log: registerLoggingCallback('log'),
702 | // testStart: { name }
703 | testStart: registerLoggingCallback('testStart'),
704 | // testDone: { name, failed, passed, total }
705 | testDone: registerLoggingCallback('testDone'),
706 | // moduleStart: { name }
707 | moduleStart: registerLoggingCallback('moduleStart'),
708 | // moduleDone: { name, failed, passed, total }
709 | moduleDone: registerLoggingCallback('moduleDone')
710 | });
711 |
712 | if ( typeof document === "undefined" || document.readyState === "complete" ) {
713 | config.autorun = true;
714 | }
715 |
716 | QUnit.load = function() {
717 | runLoggingCallbacks( 'begin', QUnit, {} );
718 |
719 | // Initialize the config, saving the execution queue
720 | var oldconfig = extend({}, config);
721 | QUnit.init();
722 | extend(config, oldconfig);
723 |
724 | config.blocking = false;
725 |
726 | var urlConfigHtml = '', len = config.urlConfig.length;
727 | for ( var i = 0, val; i < len, val = config.urlConfig[i]; i++ ) {
728 | config[val] = QUnit.urlParams[val];
729 | urlConfigHtml += '';
730 | }
731 |
732 | var userAgent = id("qunit-userAgent");
733 | if ( userAgent ) {
734 | userAgent.innerHTML = navigator.userAgent;
735 | }
736 | var banner = id("qunit-header");
737 | if ( banner ) {
738 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml;
739 | addEvent( banner, "change", function( event ) {
740 | var params = {};
741 | params[ event.target.name ] = event.target.checked ? true : undefined;
742 | window.location = QUnit.url( params );
743 | });
744 | }
745 |
746 | var toolbar = id("qunit-testrunner-toolbar");
747 | if ( toolbar ) {
748 | var filter = document.createElement("input");
749 | filter.type = "checkbox";
750 | filter.id = "qunit-filter-pass";
751 | addEvent( filter, "click", function() {
752 | var ol = document.getElementById("qunit-tests");
753 | if ( filter.checked ) {
754 | ol.className = ol.className + " hidepass";
755 | } else {
756 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " ";
757 | ol.className = tmp.replace(/ hidepass /, " ");
758 | }
759 | if ( defined.sessionStorage ) {
760 | if (filter.checked) {
761 | sessionStorage.setItem("qunit-filter-passed-tests", "true");
762 | } else {
763 | sessionStorage.removeItem("qunit-filter-passed-tests");
764 | }
765 | }
766 | });
767 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) {
768 | filter.checked = true;
769 | var ol = document.getElementById("qunit-tests");
770 | ol.className = ol.className + " hidepass";
771 | }
772 | toolbar.appendChild( filter );
773 |
774 | var label = document.createElement("label");
775 | label.setAttribute("for", "qunit-filter-pass");
776 | label.innerHTML = "Hide passed tests";
777 | toolbar.appendChild( label );
778 | }
779 |
780 | var main = id('qunit-fixture');
781 | if ( main ) {
782 | config.fixture = main.innerHTML;
783 | }
784 |
785 | if (config.autostart) {
786 | QUnit.start();
787 | }
788 | };
789 |
790 | addEvent(window, "load", QUnit.load);
791 |
792 | // addEvent(window, "error") gives us a useless event object
793 | window.onerror = function( message, file, line ) {
794 | if ( QUnit.config.current ) {
795 | ok( false, message + ", " + file + ":" + line );
796 | } else {
797 | test( "global failure", function() {
798 | ok( false, message + ", " + file + ":" + line );
799 | });
800 | }
801 | };
802 |
803 | function done() {
804 | config.autorun = true;
805 |
806 | // Log the last module results
807 | if ( config.currentModule ) {
808 | runLoggingCallbacks( 'moduleDone', QUnit, {
809 | name: config.currentModule,
810 | failed: config.moduleStats.bad,
811 | passed: config.moduleStats.all - config.moduleStats.bad,
812 | total: config.moduleStats.all
813 | } );
814 | }
815 |
816 | var banner = id("qunit-banner"),
817 | tests = id("qunit-tests"),
818 | runtime = +new Date - config.started,
819 | passed = config.stats.all - config.stats.bad,
820 | html = [
821 | 'Tests completed in ',
822 | runtime,
823 | ' milliseconds.
',
824 | '',
825 | passed,
826 | ' tests of ',
827 | config.stats.all,
828 | ' passed, ',
829 | config.stats.bad,
830 | ' failed.'
831 | ].join('');
832 |
833 | if ( banner ) {
834 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass");
835 | }
836 |
837 | if ( tests ) {
838 | id( "qunit-testresult" ).innerHTML = html;
839 | }
840 |
841 | if ( config.altertitle && typeof document !== "undefined" && document.title ) {
842 | // show ✖ for good, ✔ for bad suite result in title
843 | // use escape sequences in case file gets loaded with non-utf-8-charset
844 | document.title = [
845 | (config.stats.bad ? "\u2716" : "\u2714"),
846 | document.title.replace(/^[\u2714\u2716] /i, "")
847 | ].join(" ");
848 | }
849 |
850 | runLoggingCallbacks( 'done', QUnit, {
851 | failed: config.stats.bad,
852 | passed: passed,
853 | total: config.stats.all,
854 | runtime: runtime
855 | } );
856 | }
857 |
858 | function validTest( name ) {
859 | var filter = config.filter,
860 | run = false;
861 |
862 | if ( !filter ) {
863 | return true;
864 | }
865 |
866 | var not = filter.charAt( 0 ) === "!";
867 | if ( not ) {
868 | filter = filter.slice( 1 );
869 | }
870 |
871 | if ( name.indexOf( filter ) !== -1 ) {
872 | return !not;
873 | }
874 |
875 | if ( not ) {
876 | run = true;
877 | }
878 |
879 | return run;
880 | }
881 |
882 | // so far supports only Firefox, Chrome and Opera (buggy)
883 | // could be extended in the future to use something like https://github.com/csnover/TraceKit
884 | function sourceFromStacktrace() {
885 | try {
886 | throw new Error();
887 | } catch ( e ) {
888 | if (e.stacktrace) {
889 | // Opera
890 | return e.stacktrace.split("\n")[6];
891 | } else if (e.stack) {
892 | // Firefox, Chrome
893 | return e.stack.split("\n")[4];
894 | } else if (e.sourceURL) {
895 | // Safari, PhantomJS
896 | // TODO sourceURL points at the 'throw new Error' line above, useless
897 | //return e.sourceURL + ":" + e.line;
898 | }
899 | }
900 | }
901 |
902 | function escapeInnerText(s) {
903 | if (!s) {
904 | return "";
905 | }
906 | s = s + "";
907 | return s.replace(/[\&<>]/g, function(s) {
908 | switch(s) {
909 | case "&": return "&";
910 | case "<": return "<";
911 | case ">": return ">";
912 | default: return s;
913 | }
914 | });
915 | }
916 |
917 | function synchronize( callback, last ) {
918 | config.queue.push( callback );
919 |
920 | if ( config.autorun && !config.blocking ) {
921 | process(last);
922 | }
923 | }
924 |
925 | function process( last ) {
926 | var start = new Date().getTime();
927 | config.depth = config.depth ? config.depth + 1 : 1;
928 |
929 | while ( config.queue.length && !config.blocking ) {
930 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) {
931 | config.queue.shift()();
932 | } else {
933 | window.setTimeout( function(){
934 | process( last );
935 | }, 13 );
936 | break;
937 | }
938 | }
939 | config.depth--;
940 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
941 | done();
942 | }
943 | }
944 |
945 | function saveGlobal() {
946 | config.pollution = [];
947 |
948 | if ( config.noglobals ) {
949 | for ( var key in window ) {
950 | if ( !hasOwn.call( window, key ) ) {
951 | continue;
952 | }
953 | config.pollution.push( key );
954 | }
955 | }
956 | }
957 |
958 | function checkPollution( name ) {
959 | var old = config.pollution;
960 | saveGlobal();
961 |
962 | var newGlobals = diff( config.pollution, old );
963 | if ( newGlobals.length > 0 ) {
964 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") );
965 | }
966 |
967 | var deletedGlobals = diff( old, config.pollution );
968 | if ( deletedGlobals.length > 0 ) {
969 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") );
970 | }
971 | }
972 |
973 | // returns a new Array with the elements that are in a but not in b
974 | function diff( a, b ) {
975 | var result = a.slice();
976 | for ( var i = 0; i < result.length; i++ ) {
977 | for ( var j = 0; j < b.length; j++ ) {
978 | if ( result[i] === b[j] ) {
979 | result.splice(i, 1);
980 | i--;
981 | break;
982 | }
983 | }
984 | }
985 | return result;
986 | }
987 |
988 | function fail(message, exception, callback) {
989 | if ( typeof console !== "undefined" && console.error && console.warn ) {
990 | console.error(message);
991 | console.error(exception);
992 | console.warn(callback.toString());
993 |
994 | } else if ( window.opera && opera.postError ) {
995 | opera.postError(message, exception, callback.toString);
996 | }
997 | }
998 |
999 | function extend(a, b) {
1000 | for ( var prop in b ) {
1001 | if ( b[prop] === undefined ) {
1002 | delete a[prop];
1003 |
1004 | // Avoid "Member not found" error in IE8 caused by setting window.constructor
1005 | } else if ( prop !== "constructor" || a !== window ) {
1006 | a[prop] = b[prop];
1007 | }
1008 | }
1009 |
1010 | return a;
1011 | }
1012 |
1013 | function addEvent(elem, type, fn) {
1014 | if ( elem.addEventListener ) {
1015 | elem.addEventListener( type, fn, false );
1016 | } else if ( elem.attachEvent ) {
1017 | elem.attachEvent( "on" + type, fn );
1018 | } else {
1019 | fn();
1020 | }
1021 | }
1022 |
1023 | function id(name) {
1024 | return !!(typeof document !== "undefined" && document && document.getElementById) &&
1025 | document.getElementById( name );
1026 | }
1027 |
1028 | function registerLoggingCallback(key){
1029 | return function(callback){
1030 | config[key].push( callback );
1031 | };
1032 | }
1033 |
1034 | // Supports deprecated method of completely overwriting logging callbacks
1035 | function runLoggingCallbacks(key, scope, args) {
1036 | //debugger;
1037 | var callbacks;
1038 | if ( QUnit.hasOwnProperty(key) ) {
1039 | QUnit[key].call(scope, args);
1040 | } else {
1041 | callbacks = config[key];
1042 | for( var i = 0; i < callbacks.length; i++ ) {
1043 | callbacks[i].call( scope, args );
1044 | }
1045 | }
1046 | }
1047 |
1048 | // Test for equality any JavaScript type.
1049 | // Author: Philippe Rathé
1050 | QUnit.equiv = function () {
1051 |
1052 | var innerEquiv; // the real equiv function
1053 | var callers = []; // stack to decide between skip/abort functions
1054 | var parents = []; // stack to avoiding loops from circular referencing
1055 |
1056 | // Call the o related callback with the given arguments.
1057 | function bindCallbacks(o, callbacks, args) {
1058 | var prop = QUnit.objectType(o);
1059 | if (prop) {
1060 | if (QUnit.objectType(callbacks[prop]) === "function") {
1061 | return callbacks[prop].apply(callbacks, args);
1062 | } else {
1063 | return callbacks[prop]; // or undefined
1064 | }
1065 | }
1066 | }
1067 |
1068 | var getProto = Object.getPrototypeOf || function (obj) {
1069 | return obj.__proto__;
1070 | };
1071 |
1072 | var callbacks = function () {
1073 |
1074 | // for string, boolean, number and null
1075 | function useStrictEquality(b, a) {
1076 | if (b instanceof a.constructor || a instanceof b.constructor) {
1077 | // to catch short annotaion VS 'new' annotation of a
1078 | // declaration
1079 | // e.g. var i = 1;
1080 | // var j = new Number(1);
1081 | return a == b;
1082 | } else {
1083 | return a === b;
1084 | }
1085 | }
1086 |
1087 | return {
1088 | "string" : useStrictEquality,
1089 | "boolean" : useStrictEquality,
1090 | "number" : useStrictEquality,
1091 | "null" : useStrictEquality,
1092 | "undefined" : useStrictEquality,
1093 |
1094 | "nan" : function(b) {
1095 | return isNaN(b);
1096 | },
1097 |
1098 | "date" : function(b, a) {
1099 | return QUnit.objectType(b) === "date"
1100 | && a.valueOf() === b.valueOf();
1101 | },
1102 |
1103 | "regexp" : function(b, a) {
1104 | return QUnit.objectType(b) === "regexp"
1105 | && a.source === b.source && // the regex itself
1106 | a.global === b.global && // and its modifers
1107 | // (gmi) ...
1108 | a.ignoreCase === b.ignoreCase
1109 | && a.multiline === b.multiline;
1110 | },
1111 |
1112 | // - skip when the property is a method of an instance (OOP)
1113 | // - abort otherwise,
1114 | // initial === would have catch identical references anyway
1115 | "function" : function() {
1116 | var caller = callers[callers.length - 1];
1117 | return caller !== Object && typeof caller !== "undefined";
1118 | },
1119 |
1120 | "array" : function(b, a) {
1121 | var i, j, loop;
1122 | var len;
1123 |
1124 | // b could be an object literal here
1125 | if (!(QUnit.objectType(b) === "array")) {
1126 | return false;
1127 | }
1128 |
1129 | len = a.length;
1130 | if (len !== b.length) { // safe and faster
1131 | return false;
1132 | }
1133 |
1134 | // track reference to avoid circular references
1135 | parents.push(a);
1136 | for (i = 0; i < len; i++) {
1137 | loop = false;
1138 | for (j = 0; j < parents.length; j++) {
1139 | if (parents[j] === a[i]) {
1140 | loop = true;// dont rewalk array
1141 | }
1142 | }
1143 | if (!loop && !innerEquiv(a[i], b[i])) {
1144 | parents.pop();
1145 | return false;
1146 | }
1147 | }
1148 | parents.pop();
1149 | return true;
1150 | },
1151 |
1152 | "object" : function(b, a) {
1153 | var i, j, loop;
1154 | var eq = true; // unless we can proove it
1155 | var aProperties = [], bProperties = []; // collection of
1156 | // strings
1157 |
1158 | // comparing constructors is more strict than using
1159 | // instanceof
1160 | if (a.constructor !== b.constructor) {
1161 | // Allow objects with no prototype to be equivalent to
1162 | // objects with Object as their constructor.
1163 | if (!((getProto(a) === null && getProto(b) === Object.prototype) ||
1164 | (getProto(b) === null && getProto(a) === Object.prototype)))
1165 | {
1166 | return false;
1167 | }
1168 | }
1169 |
1170 | // stack constructor before traversing properties
1171 | callers.push(a.constructor);
1172 | // track reference to avoid circular references
1173 | parents.push(a);
1174 |
1175 | for (i in a) { // be strict: don't ensures hasOwnProperty
1176 | // and go deep
1177 | loop = false;
1178 | for (j = 0; j < parents.length; j++) {
1179 | if (parents[j] === a[i])
1180 | loop = true; // don't go down the same path
1181 | // twice
1182 | }
1183 | aProperties.push(i); // collect a's properties
1184 |
1185 | if (!loop && !innerEquiv(a[i], b[i])) {
1186 | eq = false;
1187 | break;
1188 | }
1189 | }
1190 |
1191 | callers.pop(); // unstack, we are done
1192 | parents.pop();
1193 |
1194 | for (i in b) {
1195 | bProperties.push(i); // collect b's properties
1196 | }
1197 |
1198 | // Ensures identical properties name
1199 | return eq
1200 | && innerEquiv(aProperties.sort(), bProperties
1201 | .sort());
1202 | }
1203 | };
1204 | }();
1205 |
1206 | innerEquiv = function() { // can take multiple arguments
1207 | var args = Array.prototype.slice.apply(arguments);
1208 | if (args.length < 2) {
1209 | return true; // end transition
1210 | }
1211 |
1212 | return (function(a, b) {
1213 | if (a === b) {
1214 | return true; // catch the most you can
1215 | } else if (a === null || b === null || typeof a === "undefined"
1216 | || typeof b === "undefined"
1217 | || QUnit.objectType(a) !== QUnit.objectType(b)) {
1218 | return false; // don't lose time with error prone cases
1219 | } else {
1220 | return bindCallbacks(a, callbacks, [ b, a ]);
1221 | }
1222 |
1223 | // apply transition with (1..n) arguments
1224 | })(args[0], args[1])
1225 | && arguments.callee.apply(this, args.splice(1,
1226 | args.length - 1));
1227 | };
1228 |
1229 | return innerEquiv;
1230 |
1231 | }();
1232 |
1233 | /**
1234 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com |
1235 | * http://flesler.blogspot.com Licensed under BSD
1236 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008
1237 | *
1238 | * @projectDescription Advanced and extensible data dumping for Javascript.
1239 | * @version 1.0.0
1240 | * @author Ariel Flesler
1241 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
1242 | */
1243 | QUnit.jsDump = (function() {
1244 | function quote( str ) {
1245 | return '"' + str.toString().replace(/"/g, '\\"') + '"';
1246 | };
1247 | function literal( o ) {
1248 | return o + '';
1249 | };
1250 | function join( pre, arr, post ) {
1251 | var s = jsDump.separator(),
1252 | base = jsDump.indent(),
1253 | inner = jsDump.indent(1);
1254 | if ( arr.join )
1255 | arr = arr.join( ',' + s + inner );
1256 | if ( !arr )
1257 | return pre + post;
1258 | return [ pre, inner + arr, base + post ].join(s);
1259 | };
1260 | function array( arr, stack ) {
1261 | var i = arr.length, ret = Array(i);
1262 | this.up();
1263 | while ( i-- )
1264 | ret[i] = this.parse( arr[i] , undefined , stack);
1265 | this.down();
1266 | return join( '[', ret, ']' );
1267 | };
1268 |
1269 | var reName = /^function (\w+)/;
1270 |
1271 | var jsDump = {
1272 | parse:function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance
1273 | stack = stack || [ ];
1274 | var parser = this.parsers[ type || this.typeOf(obj) ];
1275 | type = typeof parser;
1276 | var inStack = inArray(obj, stack);
1277 | if (inStack != -1) {
1278 | return 'recursion('+(inStack - stack.length)+')';
1279 | }
1280 | //else
1281 | if (type == 'function') {
1282 | stack.push(obj);
1283 | var res = parser.call( this, obj, stack );
1284 | stack.pop();
1285 | return res;
1286 | }
1287 | // else
1288 | return (type == 'string') ? parser : this.parsers.error;
1289 | },
1290 | typeOf:function( obj ) {
1291 | var type;
1292 | if ( obj === null ) {
1293 | type = "null";
1294 | } else if (typeof obj === "undefined") {
1295 | type = "undefined";
1296 | } else if (QUnit.is("RegExp", obj)) {
1297 | type = "regexp";
1298 | } else if (QUnit.is("Date", obj)) {
1299 | type = "date";
1300 | } else if (QUnit.is("Function", obj)) {
1301 | type = "function";
1302 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") {
1303 | type = "window";
1304 | } else if (obj.nodeType === 9) {
1305 | type = "document";
1306 | } else if (obj.nodeType) {
1307 | type = "node";
1308 | } else if (
1309 | // native arrays
1310 | toString.call( obj ) === "[object Array]" ||
1311 | // NodeList objects
1312 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) )
1313 | ) {
1314 | type = "array";
1315 | } else {
1316 | type = typeof obj;
1317 | }
1318 | return type;
1319 | },
1320 | separator:function() {
1321 | return this.multiline ? this.HTML ? '
' : '\n' : this.HTML ? ' ' : ' ';
1322 | },
1323 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
1324 | if ( !this.multiline )
1325 | return '';
1326 | var chr = this.indentChar;
1327 | if ( this.HTML )
1328 | chr = chr.replace(/\t/g,' ').replace(/ /g,' ');
1329 | return Array( this._depth_ + (extra||0) ).join(chr);
1330 | },
1331 | up:function( a ) {
1332 | this._depth_ += a || 1;
1333 | },
1334 | down:function( a ) {
1335 | this._depth_ -= a || 1;
1336 | },
1337 | setParser:function( name, parser ) {
1338 | this.parsers[name] = parser;
1339 | },
1340 | // The next 3 are exposed so you can use them
1341 | quote:quote,
1342 | literal:literal,
1343 | join:join,
1344 | //
1345 | _depth_: 1,
1346 | // This is the list of parsers, to modify them, use jsDump.setParser
1347 | parsers:{
1348 | window: '[Window]',
1349 | document: '[Document]',
1350 | error:'[ERROR]', //when no parser is found, shouldn't happen
1351 | unknown: '[Unknown]',
1352 | 'null':'null',
1353 | 'undefined':'undefined',
1354 | 'function':function( fn ) {
1355 | var ret = 'function',
1356 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
1357 | if ( name )
1358 | ret += ' ' + name;
1359 | ret += '(';
1360 |
1361 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join('');
1362 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' );
1363 | },
1364 | array: array,
1365 | nodelist: array,
1366 | arguments: array,
1367 | object:function( map, stack ) {
1368 | var ret = [ ];
1369 | QUnit.jsDump.up();
1370 | for ( var key in map ) {
1371 | var val = map[key];
1372 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(val, undefined, stack));
1373 | }
1374 | QUnit.jsDump.down();
1375 | return join( '{', ret, '}' );
1376 | },
1377 | node:function( node ) {
1378 | var open = QUnit.jsDump.HTML ? '<' : '<',
1379 | close = QUnit.jsDump.HTML ? '>' : '>';
1380 |
1381 | var tag = node.nodeName.toLowerCase(),
1382 | ret = open + tag;
1383 |
1384 | for ( var a in QUnit.jsDump.DOMAttrs ) {
1385 | var val = node[QUnit.jsDump.DOMAttrs[a]];
1386 | if ( val )
1387 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' );
1388 | }
1389 | return ret + close + open + '/' + tag + close;
1390 | },
1391 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function
1392 | var l = fn.length;
1393 | if ( !l ) return '';
1394 |
1395 | var args = Array(l);
1396 | while ( l-- )
1397 | args[l] = String.fromCharCode(97+l);//97 is 'a'
1398 | return ' ' + args.join(', ') + ' ';
1399 | },
1400 | key:quote, //object calls it internally, the key part of an item in a map
1401 | functionCode:'[code]', //function calls it internally, it's the content of the function
1402 | attribute:quote, //node calls it internally, it's an html attribute value
1403 | string:quote,
1404 | date:quote,
1405 | regexp:literal, //regex
1406 | number:literal,
1407 | 'boolean':literal
1408 | },
1409 | DOMAttrs:{//attributes to dump from nodes, name=>realName
1410 | id:'id',
1411 | name:'name',
1412 | 'class':'className'
1413 | },
1414 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
1415 | indentChar:' ',//indentation unit
1416 | multiline:true //if true, items in a collection, are separated by a \n, else just a space.
1417 | };
1418 |
1419 | return jsDump;
1420 | })();
1421 |
1422 | // from Sizzle.js
1423 | function getText( elems ) {
1424 | var ret = "", elem;
1425 |
1426 | for ( var i = 0; elems[i]; i++ ) {
1427 | elem = elems[i];
1428 |
1429 | // Get the text from text nodes and CDATA nodes
1430 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
1431 | ret += elem.nodeValue;
1432 |
1433 | // Traverse everything else, except comment nodes
1434 | } else if ( elem.nodeType !== 8 ) {
1435 | ret += getText( elem.childNodes );
1436 | }
1437 | }
1438 |
1439 | return ret;
1440 | };
1441 |
1442 | //from jquery.js
1443 | function inArray( elem, array ) {
1444 | if ( array.indexOf ) {
1445 | return array.indexOf( elem );
1446 | }
1447 |
1448 | for ( var i = 0, length = array.length; i < length; i++ ) {
1449 | if ( array[ i ] === elem ) {
1450 | return i;
1451 | }
1452 | }
1453 |
1454 | return -1;
1455 | }
1456 |
1457 | /*
1458 | * Javascript Diff Algorithm
1459 | * By John Resig (http://ejohn.org/)
1460 | * Modified by Chu Alan "sprite"
1461 | *
1462 | * Released under the MIT license.
1463 | *
1464 | * More Info:
1465 | * http://ejohn.org/projects/javascript-diff-algorithm/
1466 | *
1467 | * Usage: QUnit.diff(expected, actual)
1468 | *
1469 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over"
1470 | */
1471 | QUnit.diff = (function() {
1472 | function diff(o, n) {
1473 | var ns = {};
1474 | var os = {};
1475 |
1476 | for (var i = 0; i < n.length; i++) {
1477 | if (ns[n[i]] == null)
1478 | ns[n[i]] = {
1479 | rows: [],
1480 | o: null
1481 | };
1482 | ns[n[i]].rows.push(i);
1483 | }
1484 |
1485 | for (var i = 0; i < o.length; i++) {
1486 | if (os[o[i]] == null)
1487 | os[o[i]] = {
1488 | rows: [],
1489 | n: null
1490 | };
1491 | os[o[i]].rows.push(i);
1492 | }
1493 |
1494 | for (var i in ns) {
1495 | if ( !hasOwn.call( ns, i ) ) {
1496 | continue;
1497 | }
1498 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) {
1499 | n[ns[i].rows[0]] = {
1500 | text: n[ns[i].rows[0]],
1501 | row: os[i].rows[0]
1502 | };
1503 | o[os[i].rows[0]] = {
1504 | text: o[os[i].rows[0]],
1505 | row: ns[i].rows[0]
1506 | };
1507 | }
1508 | }
1509 |
1510 | for (var i = 0; i < n.length - 1; i++) {
1511 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null &&
1512 | n[i + 1] == o[n[i].row + 1]) {
1513 | n[i + 1] = {
1514 | text: n[i + 1],
1515 | row: n[i].row + 1
1516 | };
1517 | o[n[i].row + 1] = {
1518 | text: o[n[i].row + 1],
1519 | row: i + 1
1520 | };
1521 | }
1522 | }
1523 |
1524 | for (var i = n.length - 1; i > 0; i--) {
1525 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null &&
1526 | n[i - 1] == o[n[i].row - 1]) {
1527 | n[i - 1] = {
1528 | text: n[i - 1],
1529 | row: n[i].row - 1
1530 | };
1531 | o[n[i].row - 1] = {
1532 | text: o[n[i].row - 1],
1533 | row: i - 1
1534 | };
1535 | }
1536 | }
1537 |
1538 | return {
1539 | o: o,
1540 | n: n
1541 | };
1542 | }
1543 |
1544 | return function(o, n) {
1545 | o = o.replace(/\s+$/, '');
1546 | n = n.replace(/\s+$/, '');
1547 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/));
1548 |
1549 | var str = "";
1550 |
1551 | var oSpace = o.match(/\s+/g);
1552 | if (oSpace == null) {
1553 | oSpace = [" "];
1554 | }
1555 | else {
1556 | oSpace.push(" ");
1557 | }
1558 | var nSpace = n.match(/\s+/g);
1559 | if (nSpace == null) {
1560 | nSpace = [" "];
1561 | }
1562 | else {
1563 | nSpace.push(" ");
1564 | }
1565 |
1566 | if (out.n.length == 0) {
1567 | for (var i = 0; i < out.o.length; i++) {
1568 | str += '' + out.o[i] + oSpace[i] + "";
1569 | }
1570 | }
1571 | else {
1572 | if (out.n[0].text == null) {
1573 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) {
1574 | str += '' + out.o[n] + oSpace[n] + "";
1575 | }
1576 | }
1577 |
1578 | for (var i = 0; i < out.n.length; i++) {
1579 | if (out.n[i].text == null) {
1580 | str += '' + out.n[i] + nSpace[i] + "";
1581 | }
1582 | else {
1583 | var pre = "";
1584 |
1585 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) {
1586 | pre += '' + out.o[n] + oSpace[n] + "";
1587 | }
1588 | str += " " + out.n[i].text + nSpace[i] + pre;
1589 | }
1590 | }
1591 | }
1592 |
1593 | return str;
1594 | };
1595 | })();
1596 |
1597 | })(this);
1598 |
--------------------------------------------------------------------------------
/tests/sinon-ie-1.3.1.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Sinon.JS 1.3.1, 2012/01/04
3 | *
4 | * @author Christian Johansen (christian@cjohansen.no)
5 | *
6 | * (The BSD License)
7 | *
8 | * Copyright (c) 2010-2011, Christian Johansen, christian@cjohansen.no
9 | * All rights reserved.
10 | *
11 | * Redistribution and use in source and binary forms, with or without modification,
12 | * are permitted provided that the following conditions are met:
13 | *
14 | * * Redistributions of source code must retain the above copyright notice,
15 | * this list of conditions and the following disclaimer.
16 | * * Redistributions in binary form must reproduce the above copyright notice,
17 | * this list of conditions and the following disclaimer in the documentation
18 | * and/or other materials provided with the distribution.
19 | * * Neither the name of Christian Johansen nor the names of his contributors
20 | * may be used to endorse or promote products derived from this software
21 | * without specific prior written permission.
22 | *
23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
32 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 | */
34 |
35 | "use strict";
36 | /*global sinon, setTimeout, setInterval, clearTimeout, clearInterval, Date*/
37 | /**
38 | * Helps IE run the fake timers. By defining global functions, IE allows
39 | * them to be overwritten at a later point. If these are not defined like
40 | * this, overwriting them will result in anything from an exception to browser
41 | * crash.
42 | *
43 | * If you don't require fake timers to work in IE, don't include this file.
44 | *
45 | * @author Christian Johansen (christian@cjohansen.no)
46 | * @license BSD
47 | *
48 | * Copyright (c) 2010-2011 Christian Johansen
49 | */
50 | function setTimeout() {}
51 | function clearTimeout() {}
52 | function setInterval() {}
53 | function clearInterval() {}
54 | function Date() {}
55 |
56 | // Reassign the original functions. Now their writable attribute
57 | // should be true. Hackish, I know, but it works.
58 | setTimeout = sinon.timers.setTimeout;
59 | clearTimeout = sinon.timers.clearTimeout;
60 | setInterval = sinon.timers.setInterval;
61 | clearInterval = sinon.timers.clearInterval;
62 | Date = sinon.timers.Date;
63 |
64 | /*global sinon*/
65 | /**
66 | * Helps IE run the fake XMLHttpRequest. By defining global functions, IE allows
67 | * them to be overwritten at a later point. If these are not defined like
68 | * this, overwriting them will result in anything from an exception to browser
69 | * crash.
70 | *
71 | * If you don't require fake XHR to work in IE, don't include this file.
72 | *
73 | * @author Christian Johansen (christian@cjohansen.no)
74 | * @license BSD
75 | *
76 | * Copyright (c) 2010-2011 Christian Johansen
77 | */
78 | function XMLHttpRequest() {}
79 |
80 | // Reassign the original function. Now its writable attribute
81 | // should be true. Hackish, I know, but it works.
82 | XMLHttpRequest = sinon.xhr.XMLHttpRequest || undefined;
83 |
--------------------------------------------------------------------------------
/tests/tests.coffee:
--------------------------------------------------------------------------------
1 | calledOnce = (fake, message) ->
2 | QUnit.push fake.calledOnce, fake.callCount, 1, message
3 | neverCalled = (fake, message) ->
4 | QUnit.push !fake.called, fake.callCount, 0, message
5 | lastCalledWithExactly = (fake, expectedArgs, message) ->
6 | result = fake.lastCall? and QUnit.equiv(fake.lastCall.args, expectedArgs)
7 | actualArgs = fake.lastCall?.args
8 | QUnit.push result, actualArgs, expectedArgs, message
9 |
10 | callbackGroup = () ->
11 | group = {}
12 | group.reset = () ->
13 | for key, value of group
14 | value.reset() if Object::toString.call(value.reset) is "[object Function]"
15 | return group
16 |
17 | module "Finch",
18 | teardown: ->
19 | Finch.reset()
20 |
21 | test "Trivial routing", sinon.test ->
22 |
23 | Finch.route "foo/bar", foo_bar = @stub()
24 | Finch.route "baz/quux", baz_quux = @stub()
25 |
26 | # Test routes
27 |
28 | Finch.call "/foo/bar"
29 |
30 | calledOnce foo_bar, "foo/bar called"
31 |
32 | Finch.call "/baz/quux"
33 |
34 | calledOnce baz_quux, "baz/quux called"
35 |
36 | test "Simple hierarchical routing", sinon.test ->
37 |
38 | Finch.route "foo", foo = @stub()
39 | Finch.route "[foo]/bar", foo_bar = @stub()
40 | Finch.route "[foo/bar]/:id", foo_bar_id = @stub()
41 | Finch.route "[foo]/baz", foo_baz = @stub()
42 | Finch.route "[foo/baz]/:id", foo_baz_id = @stub()
43 | Finch.route "quux", quux = @stub()
44 | Finch.route "[quux]/:id", quux_id = @stub()
45 |
46 | # Test routes
47 |
48 | Finch.call "/foo/bar"
49 |
50 | calledOnce foo, "foo called once"
51 | lastCalledWithExactly foo, [{}], "foo called with correct bindings"
52 | calledOnce foo_bar, "foo/bar called once"
53 | lastCalledWithExactly foo_bar, [{}], "foo called with correct bindings"
54 | ok foo.calledBefore(foo_bar), "foo called before foo/bar"
55 | foo.reset()
56 | foo_bar.reset()
57 |
58 | Finch.call "/foo/bar/123"
59 |
60 | neverCalled foo, "foo not called again"
61 | neverCalled foo_bar, "foo/bar not called again"
62 | calledOnce foo_bar_id, "foo/bar/id called once"
63 | lastCalledWithExactly foo_bar_id, [{ id: "123" }], "foo/bar/id bindings"
64 | foo_bar_id.reset()
65 |
66 | Finch.call "/foo/bar/123"
67 |
68 | neverCalled foo, "foo not called again"
69 | neverCalled foo_bar, "foo/bar not called again"
70 | neverCalled foo_bar_id, "foo/bar/id not called again"
71 |
72 | Finch.call "/foo/bar/123?x=Hello&y=World"
73 |
74 | neverCalled foo, "foo not called again"
75 | neverCalled foo_bar, "foo/bar not called again"
76 | neverCalled foo_bar_id, "foo/bar/id not called again"
77 |
78 | Finch.call "/foo/baz/456"
79 |
80 | neverCalled foo, "foo not called again"
81 | calledOnce foo_baz, "foo/baz called"
82 | calledOnce foo_baz_id, "foo/baz/id called"
83 | ok foo_baz.calledBefore(foo_baz_id), "foo/baz called before foo/baz/id"
84 | lastCalledWithExactly foo_baz_id, [{ id: "456" }], "foo/baz/id bindings"
85 | foo_baz.reset()
86 | foo_baz_id.reset()
87 |
88 | Finch.call "/quux/789?band=Sunn O)))&genre=Post-Progressive Fridgecore"
89 |
90 | calledOnce quux, "quux called"
91 | calledOnce quux_id, "quux/id called"
92 | ok quux.calledBefore(quux_id), "quux called before quux/id"
93 | lastCalledWithExactly quux_id, [{ id: "789" }], "quux/id bindings"
94 |
95 | test "More hierarchical routing", sinon.test ->
96 |
97 | Finch.route "foo", foo = @stub()
98 | Finch.route "[foo]/bar/baz", foo_bar_baz = @stub()
99 | Finch.route "foo/bar", foo_bar = @stub()
100 | Finch.route "[foo/bar]/quux", foo_bar_quux = @stub()
101 |
102 | # Test routes
103 |
104 | Finch.call "/foo/bar/baz"
105 |
106 | calledOnce foo, "foo called"
107 | calledOnce foo_bar_baz, "foo/bar/baz called"
108 | ok foo.calledBefore(foo_bar_baz), "foo called before foo/bar/baz"
109 | neverCalled foo_bar, "foo/bar NOT called"
110 | foo.reset()
111 | foo_bar_baz.reset()
112 |
113 | Finch.call "/foo/bar/quux"
114 | calledOnce foo_bar, "foo/bar called"
115 | calledOnce foo_bar_quux, "foo/bar/quux called"
116 | ok foo_bar.calledBefore(foo_bar_quux), "foo/bar called before foo/bar/quux"
117 | neverCalled foo, "foo NOT called"
118 |
119 | test "Even more hierarchical routing", sinon.test ->
120 |
121 | Finch.route "foo", foo = @stub()
122 | Finch.route "[foo]/bar", foo_bar = @stub()
123 |
124 | # Test routes
125 |
126 | Finch.call "/foo"
127 |
128 | calledOnce foo, "foo called"
129 | neverCalled foo_bar, "foo/bar not called"
130 | foo.reset()
131 | foo_bar.reset()
132 |
133 | Finch.call "/foo/bar"
134 |
135 | neverCalled foo, "foo called"
136 | calledOnce foo_bar, "foo/bar called"
137 | foo.reset()
138 | foo_bar.reset()
139 |
140 | Finch.call "/foo"
141 |
142 | calledOnce foo, "foo called"
143 | neverCalled foo_bar, "foo/bar not called"
144 |
145 | test "Hierarchical routing with /", sinon.test ->
146 |
147 | Finch.route "/", slash = @stub()
148 | Finch.route "[/]foo", foo = @stub()
149 | Finch.route "[/foo]/bar", bar = @stub()
150 |
151 | # Test routes
152 |
153 | Finch.call "/foo"
154 |
155 | calledOnce slash, "/ called once"
156 | calledOnce foo, "foo called once"
157 | neverCalled bar, "bar never called"
158 |
159 | slash.reset()
160 | foo.reset()
161 | bar.reset()
162 |
163 | Finch.call "/"
164 | calledOnce slash, "/ called once"
165 | neverCalled foo, "foo never called"
166 | neverCalled bar, "bar never called"
167 |
168 | test "Simple routing with setup, load, and teardown", sinon.test ->
169 |
170 | cb = callbackGroup()
171 |
172 | Finch.route "/",
173 | setup: cb.setup_slash = @stub()
174 | load: cb.load_slash = @stub()
175 | teardown: cb.teardown_slash = @stub()
176 | Finch.route "/foo",
177 | setup: cb.setup_foo = @stub()
178 | load: cb.load_foo = @stub()
179 | teardown: cb.teardown_foo = @stub()
180 | Finch.route "foo/bar",
181 | setup: cb.setup_foo_bar = @stub()
182 | load: cb.load_foo_bar = @stub()
183 | teardown: cb.teardown_foo_bar = @stub()
184 |
185 | # Test routes
186 |
187 | Finch.call "/"
188 |
189 | calledOnce cb.setup_slash, '/: / setup called once'
190 | calledOnce cb.load_slash, '/: / load called once'
191 | neverCalled cb.teardown_slash, '/: / teardown not called'
192 | cb.reset()
193 |
194 | Finch.call "/foo"
195 |
196 | neverCalled cb.setup_slash, '/foo: / setup not called'
197 | neverCalled cb.load_slash, '/foo: / load called once'
198 | calledOnce cb.teardown_slash, '/foo: / teardown called once'
199 | calledOnce cb.setup_foo, '/foo: foo setup called once'
200 | calledOnce cb.load_foo, '/foo: foo load called once'
201 | neverCalled cb.teardown_foo, '/foo: foo teardown not called'
202 | cb.reset()
203 |
204 | Finch.call "/foo/bar"
205 |
206 | neverCalled cb.setup_slash, '/foo/bar: / setup not called'
207 | neverCalled cb.load_slash, '/foo/bar: / teardown not called'
208 | neverCalled cb.teardown_slash, '/foo/bar: / teardown not called'
209 | neverCalled cb.setup_foo, '/foo/bar: foo setup not called'
210 | neverCalled cb.load_foo, '/foo/bar: foo load called once'
211 | calledOnce cb.teardown_foo, '/foo/bar: foo teardown called once'
212 | calledOnce cb.setup_foo_bar, '/foo/bar: foo/bar setup called once'
213 | calledOnce cb.load_foo_bar, '/foo/bar: foo/bar load called once'
214 | neverCalled cb.teardown_foo_bar, '/foo/bar: foo/bar teardown not called'
215 | cb.reset()
216 |
217 | Finch.call "/foo/bar?baz=quux"
218 |
219 | neverCalled cb.setup_slash, '/foo/bar?baz=quux: / setup not called'
220 | neverCalled cb.load_slash, '/foo/bar?baz=quux: / load not called'
221 | neverCalled cb.teardown_slash, '/foo/bar?baz=quux: / teardown not called'
222 | neverCalled cb.setup_foo, '/foo/bar?baz=quux: foo setup not called'
223 | neverCalled cb.load_foo, '/foo/bar?baz=quux: foo load not called'
224 | neverCalled cb.teardown_foo, '/foo/bar?baz=quux: foo teardown not called'
225 | neverCalled cb.setup_foo_bar, '/foo/bar?baz=quux: foo/bar setup not called'
226 | neverCalled cb.load_foo_bar, '/foo/bar?baz=quux: foo/bar load not called'
227 | neverCalled cb.teardown_foo_bar, '/foo/bar?baz=quux: foo/bar teardown not called'
228 | cb.reset()
229 |
230 | Finch.call "/foo/bar?baz=xyzzy"
231 |
232 | neverCalled cb.setup_slash, '/foo/bar?baz=xyzzy: / setup not called'
233 | neverCalled cb.load_slash, '/foo/bar?baz=xyzzy: / load not called'
234 | neverCalled cb.teardown_slash, '/foo/bar?baz=xyzzy: / teardown not called'
235 | neverCalled cb.setup_foo, '/foo/bar?baz=xyzzy: foo setup not called'
236 | neverCalled cb.load_foo, '/foo/bar?baz=xyzzy: foo load not called'
237 | neverCalled cb.teardown_foo, '/foo/bar?baz=xyzzy: foo teardown not called'
238 | neverCalled cb.setup_foo_bar, '/foo/bar?baz=xyzzy: foo/bar setup not called'
239 | neverCalled cb.load_foo_bar, '/foo/bar?baz=xyzzy: foo/bar load not called'
240 | neverCalled cb.teardown_foo_bar, '/foo/bar?baz=xyzzy: foo/bar teardown not called'
241 | cb.reset()
242 |
243 | test "Hierarchical routing with setup, load, and teardown", sinon.test ->
244 |
245 | cb = callbackGroup()
246 |
247 | Finch.route "foo",
248 | setup: cb.setup_foo = @stub()
249 | load: cb.load_foo = @stub()
250 | unload: cb.unload_foo = @stub()
251 | teardown: cb.teardown_foo = @stub()
252 | Finch.route "[foo]/bar",
253 | setup: cb.setup_foo_bar = @stub()
254 | load: cb.load_foo_bar = @stub()
255 | unload: cb.unload_foo_bar = @stub()
256 | teardown: cb.teardown_foo_bar = @stub()
257 | Finch.route "[foo/bar]/:id",
258 | setup: cb.setup_foo_bar_id = @stub()
259 | load: cb.load_foo_bar_id = @stub()
260 | unload: cb.unload_foo_bar_id = @stub()
261 | teardown: cb.teardown_foo_bar_id = @stub()
262 | Finch.route "[foo]/baz",
263 | setup: cb.setup_foo_baz = @stub()
264 | load: cb.load_foo_baz = @stub()
265 | unload: cb.unload_foo_baz = @stub()
266 | teardown: cb.teardown_foo_baz = @stub()
267 | Finch.route "[foo/baz]/:id",
268 | setup: cb.setup_foo_baz_id = @stub()
269 | load: cb.load_foo_baz_id = @stub()
270 | unload: cb.unload_foo_baz_id = @stub()
271 | teardown: cb.teardown_foo_baz_id = @stub()
272 |
273 | # Test routes
274 |
275 | Finch.call "/foo"
276 |
277 | calledOnce cb.setup_foo, "/foo: foo setup"
278 | calledOnce cb.load_foo, "/foo: foo load"
279 | cb.reset()
280 |
281 | Finch.call "/foo/bar"
282 |
283 | calledOnce cb.unload_foo, "/foo/bar: foo unload"
284 | neverCalled cb.setup_foo, "/foo/bar: no foo setup"
285 | neverCalled cb.load_foo, "/foo/bar: no foo load"
286 | neverCalled cb.teardown_foo, "/foo/bar: no foo teardown"
287 | calledOnce cb.setup_foo_bar, "/foo/bar: foo/bar setup"
288 | calledOnce cb.load_foo_bar, "/foo/bar: foo/bar load"
289 | cb.reset()
290 |
291 | Finch.call "/foo"
292 |
293 | calledOnce cb.unload_foo_bar, "/foo: foo/bar unload"
294 | calledOnce cb.teardown_foo_bar, "/foo return: foo/bar teardown"
295 | calledOnce cb.load_foo, "/foo return: no foo load"
296 | neverCalled cb.setup_foo, "/foo return: no foo setup"
297 | cb.reset()
298 |
299 | Finch.call "/foo/bar/123?x=abc"
300 | calledOnce cb.unload_foo, "/foo/bar/123: foo unload"
301 | neverCalled cb.unload_foo_bar, "/foo/bar/123: no foo/bar unload"
302 | neverCalled cb.teardown_foo, "/foo/bar/123: no foo teardown"
303 | neverCalled cb.load_foo, "/foo/bar/123: no foo load"
304 | neverCalled cb.setup_foo, "/foo/bar/123: no foo setup"
305 | calledOnce cb.setup_foo_bar, "/foo/bar/123: foo/bar setup"
306 | neverCalled cb.load_foo_bar, "/foo/bar/123: foo/bar load"
307 | calledOnce cb.setup_foo_bar_id, "/foo/bar/123: foo/bar/id setup"
308 | calledOnce cb.load_foo_bar_id, "/foo/bar/123: foo/bar/id load"
309 | cb.reset()
310 |
311 | Finch.call "/foo/bar/456?x=aaa&y=zzz"
312 |
313 | calledOnce cb.unload_foo_bar_id, "/foo/bar/456?x=aaa&y=zzz: foo/bar/id unload"
314 | calledOnce cb.teardown_foo_bar_id, "/foo/bar/456?x=aaa&y=zzz: foo/bar/id teardown"
315 | calledOnce cb.setup_foo_bar_id, "/foo/bar/456?x=aaa&y=zzz: foo/bar/id setup"
316 | calledOnce cb.load_foo_bar_id, "/foo/bar/456?x=aaa&y=zzz: foo/bar/id load"
317 | cb.reset()
318 |
319 | Finch.call "/foo/bar/456?x=bbb&y=zzz"
320 |
321 | neverCalled cb.unload_foo_bar_id, "/foo/bar/456?x=bbb&y=zzz: no foo/bar/id unload"
322 | neverCalled cb.teardown_foo_bar_id, "/foo/bar/456?x=bbb&y=zzz: no foo/bar/id teardown"
323 | neverCalled cb.setup_foo_bar_id, "/foo/bar/456?x=bbb&y=zzz: no foo/bar/id setup"
324 | neverCalled cb.load_foo_bar_id, "/foo/bar/456?x=bbb&y=zzz: no foo/bar/id load"
325 | cb.reset()
326 |
327 | Finch.call "/foo/bar/456?y=zzz&x=bbb"
328 |
329 | neverCalled cb.unload_foo_bar_id, "/foo/bar/456?x=bbb&y=zzz: no foo/bar/id unload"
330 | neverCalled cb.teardown_foo_bar_id, "/foo/bar/456?x=bbb&y=zzz: no foo/bar/id teardown"
331 | neverCalled cb.setup_foo_bar_id, "/foo/bar/456?y=zzz&x=bbb: no foo/bar/id setup"
332 | neverCalled cb.load_foo_bar_id, "/foo/bar/456?y=zzz&x=bbb: no foo/bar/id load"
333 | cb.reset()
334 |
335 | Finch.call "/foo/baz/789"
336 |
337 | calledOnce cb.unload_foo_bar_id, "/foo/baz/789: foo/baz/id unload"
338 | calledOnce cb.teardown_foo_bar_id, "/foo/baz/789: foo/baz/id teardown"
339 | neverCalled cb.unload_foo_bar, "/foo/baz/789: no foo/bar unload"
340 | calledOnce cb.teardown_foo_bar, "/foo/baz/789: foo/bar teardown"
341 | neverCalled cb.unload_foo, "/foo/baz/789: no foo unload"
342 | neverCalled cb.teardown_foo, "/foo/baz/789: no foo teardown"
343 | neverCalled cb.setup_foo, "/foo/baz/789: no foo setup"
344 | neverCalled cb.load_foo, "/foo/baz/789: no foo load"
345 | calledOnce cb.setup_foo_baz, "/foo/baz/789: foo/baz setup"
346 | neverCalled cb.load_foo_baz, "/foo/baz/789: foo/baz load"
347 | calledOnce cb.setup_foo_baz_id, "/foo/baz/789: foo/baz/id setup"
348 | calledOnce cb.load_foo_baz_id, "/foo/baz/789: foo/baz/id load"
349 | cb.reset()
350 |
351 | Finch.call "/foo/baz/abc?term=Hello"
352 |
353 | calledOnce cb.unload_foo_baz_id, "/foo/baz/abc?term=Hello: foo/baz/id unload"
354 | calledOnce cb.teardown_foo_baz_id, "/foo/baz/abc?term=Hello: foo/baz/id teardown"
355 | calledOnce cb.setup_foo_baz_id, "/foo/baz/abc?term=Hello: foo/baz/id setup"
356 | calledOnce cb.load_foo_baz_id, "/foo/baz/abc?term=Hello: foo/baz/id load"
357 | cb.reset()
358 |
359 | Finch.call "/foo/baz/abc?term=World"
360 |
361 | neverCalled cb.unload_foo_baz_id, "/foo/baz/abc?term=World: no foo/baz/id unload"
362 | neverCalled cb.teardown_foo_baz_id, "/foo/baz/abc?term=World: no foo/baz/id teardown"
363 | neverCalled cb.setup_foo_baz_id, "/foo/baz/abc?term=World: no foo/baz/id setup"
364 | neverCalled cb.load_foo_baz_id, "/foo/baz/abc?term=World: no foo/baz/id load"
365 |
366 | test "Skipped levels hierarchical routing with setup, load, and teardown", sinon.test ->
367 |
368 | cb = callbackGroup()
369 |
370 | Finch.route "/foo",
371 | setup: cb.setup_foo = @stub()
372 | load: cb.load_foo = @stub()
373 | unload: cb.unload_foo = @stub()
374 | teardown: cb.teardown_foo = @stub()
375 | Finch.route "[/foo]/bar",
376 | setup: cb.setup_foo_bar = @stub()
377 | load: cb.load_foo_bar = @stub()
378 | unload: cb.unload_foo_bar = @stub()
379 | teardown: cb.teardown_foo_bar = @stub()
380 | Finch.route "[foo/bar]/:id/baz",
381 | setup: cb.setup_foo_bar_id_baz = @stub()
382 | load: cb.load_foo_bar_id_baz = @stub()
383 | unload: cb.unload_foo_bar_id_baz = @stub()
384 | teardown: cb.teardown_foo_bar_id_baz = @stub()
385 |
386 | Finch.call "/foo/bar"
387 |
388 | calledOnce cb.setup_foo, "/foo setup"
389 | neverCalled cb.load_foo, "/foo load (skipped)"
390 | calledOnce cb.setup_foo_bar, "/foo/bar setup"
391 | calledOnce cb.load_foo_bar, "/foo/bar load"
392 | cb.reset()
393 |
394 | Finch.call "/foo/bar/12345/baz"
395 |
396 | neverCalled cb.setup_foo, "/foo setup (should be cached)"
397 | neverCalled cb.load_foo, "/foo load (skipped)"
398 | neverCalled cb.setup_foo_bar, "/foo/bar setup (should be cached)"
399 | neverCalled cb.load_foo_bar, "/foo/bar load (skipped)"
400 | calledOnce cb.setup_foo_bar_id_baz, "/foo/bar/:id/baz setup"
401 | calledOnce cb.load_foo_bar_id_baz, "/foo/bar/:id/baz load"
402 |
403 | test "Calling with context", sinon.test ->
404 |
405 | Finch.route "foo",
406 | setup: setup_foo = @stub()
407 | load: load_foo = @stub()
408 | unload: unload_foo = @stub()
409 | teardown: teardown_foo = @stub()
410 | Finch.route "bar", @stub()
411 |
412 | # Test routes
413 |
414 | Finch.call "/foo"
415 |
416 | calledOnce setup_foo, 'foo setup called once'
417 | context = setup_foo.getCall(0).thisValue
418 | ok load_foo.calledOn(context), 'foo load called on same context as setup'
419 |
420 | Finch.call "/bar"
421 | ok unload_foo.calledOn(context), 'foo load called on same context as setup'
422 | ok teardown_foo.calledOn(context), 'foo teardown called on same context as setup'
423 |
424 | test "Checking Parent Context", ->
425 | Finch.route "/", ->
426 | equal @parent, null, "Parent is null"
427 |
428 | @someData = "Free Bird"
429 |
430 | Finch.route "[/]home", ->
431 | ok @parent isnt null, "Parent is defined in simple version"
432 | equal @parent.someData, "Free Bird", "Correct parent passed in"
433 |
434 | @moreData = "Hello World"
435 |
436 | Finch.route "[/home]/news",
437 | setup: ->
438 | ok @parent isnt null, "Parent is defined in setup"
439 | equal @parent.moreData, "Hello World", "Correct parent passed in"
440 | equal @parent.parent.someData, "Free Bird", "Correct parent's parent passed in"
441 |
442 | load: ->
443 | ok @parent isnt null, "Parent is defined in load"
444 | equal @parent.moreData, "Hello World", "Correct parent passed in"
445 | equal @parent.parent.someData, "Free Bird", "Correct parent's parent passed in"
446 |
447 | unload: ->
448 | ok @parent isnt null, "Parent is defined in unload"
449 | equal @parent.moreData, "Hello World", "Correct parent passed in"
450 | equal @parent.parent.someData, "Free Bird", "Correct parent's parent passed in"
451 |
452 | teardown: ->
453 | ok @parent isnt null, "Parent is defined in teardown"
454 | equal @parent.moreData, "Hello World", "Correct parent passed in"
455 | equal @parent.parent.someData, "Free Bird", "Correct parent's parent passed in"
456 |
457 | Finch.route "/foo",
458 | setup: ->
459 | equal @parent, null, "Parent is null in setup"
460 |
461 | load: ->
462 | equal @parent, null, "Parent is null in load"
463 |
464 | unload: ->
465 | equal @parent, null, "Parent is null in unload"
466 |
467 | teardown: ->
468 | equal @parent, null, "Parent is null in teardown"
469 |
470 | Finch.route "[/]bar",
471 | setup: ->
472 | ok @parent isnt null, "Parent is defined in simple version"
473 | equal @parent.someData, "Free Bird", "Correct parent passed in"
474 |
475 | load: ->
476 | ok @parent isnt null, "Parent is defined in simple version"
477 | equal @parent.someData, "Free Bird", "Correct parent passed in"
478 |
479 | unload: ->
480 | ok @parent isnt null, "Parent is defined in simple version"
481 | equal @parent.someData, "Free Bird", "Correct parent passed in"
482 |
483 | teardown: ->
484 | ok @parent isnt null, "Parent is defined in simple version"
485 | equal @parent.someData, "Free Bird", "Correct parent passed in"
486 | #End Finch.route /bar
487 |
488 | Finch.call("/bar")
489 | Finch.call("/home/news")
490 | Finch.call("/foo")
491 | Finch.call("/home/news")
492 | Finch.call("/bar")
493 | Finch.call("/foo")
494 |
495 | test "Hierarchical calling with context", sinon.test ->
496 |
497 | Finch.route "foo",
498 | setup: setup_foo = @stub()
499 | load: load_foo = @stub()
500 | teardown: teardown_foo = @stub()
501 | Finch.route "[foo]/bar",
502 | setup: setup_foo_bar = @stub()
503 | load: load_foo_bar = @stub()
504 | teardown: teardown_foo_bar = @stub()
505 | Finch.route "baz", @stub()
506 |
507 | # Test routes
508 | Finch.call "/foo"
509 |
510 | calledOnce setup_foo, 'foo setup called once'
511 | foo_context = setup_foo.getCall(0).thisValue
512 | ok load_foo.calledOn(foo_context), 'foo load called on same context as setup'
513 |
514 | Finch.call "/foo/bar"
515 |
516 | calledOnce setup_foo_bar, 'foo/bar setup called once'
517 | foo_bar_context = setup_foo_bar.getCall(0).thisValue
518 | ok load_foo_bar.calledOn(foo_bar_context), 'foo/bar load called on same context as setup'
519 |
520 | notEqual foo_context, foo_bar_context, 'foo/bar should be called on a different context than foo'
521 |
522 | Finch.call "/baz"
523 |
524 | calledOnce teardown_foo_bar, 'foo/bar teardown called once'
525 | calledOnce teardown_foo, 'foo teardown called once'
526 | ok teardown_foo_bar.calledBefore(teardown_foo), 'foo/bar teardown called before foo teardown'
527 |
528 | ok teardown_foo_bar.calledOn(foo_bar_context), 'foo/bar teardown called on same context as setup'
529 | ok teardown_foo.calledOn(foo_context), 'foo teardown called on same context as'
530 |
531 | test 'Testing synchronous and asynchronous unload method and context', sinon.test ->
532 |
533 | cb = callbackGroup()
534 | cb.home_setup = @stub()
535 | cb.home_load = @stub()
536 | cb.home_unload = @stub()
537 | cb.home_teardown = @stub()
538 |
539 | Finch.route "/home",
540 | setup: (bindings, next) ->
541 | cb.home_setup()
542 | next()
543 | load: (bindings, next) ->
544 | cb.home_load()
545 | next()
546 | unload: (bindings, next) ->
547 | cb.home_unload()
548 | next()
549 | teardown: (bindings, next) ->
550 | cb.home_teardown()
551 | next()
552 |
553 | cb.home_news_setup = @stub()
554 | cb.home_news_load = @stub()
555 | cb.home_news_unload = @stub()
556 | cb.home_news_teardown = @stub()
557 |
558 | Finch.route "[/home]/news",
559 | setup: (bindings, next) ->
560 | @did_setup = true
561 | cb.home_news_setup()
562 | next()
563 | load: (bindings, next) ->
564 | @did_load = true
565 | cb.home_news_load()
566 | next()
567 | unload: (bindings, next) ->
568 | @did_unload = true
569 | cb.home_news_unload(this, next)
570 | teardown: (bindings, next) ->
571 | @did_teardown = true
572 | cb.home_news_teardown()
573 | next()
574 |
575 | Finch.route "/foo", cb.foo = @stub()
576 |
577 | Finch.call("/home")
578 | calledOnce cb.home_setup, "Called Home Setup"
579 | calledOnce cb.home_load, "Called Home Load"
580 | neverCalled cb.home_unload, "Never Called Home Unload"
581 | neverCalled cb.home_teardown, "Never Called Home Teardown"
582 | neverCalled cb.home_news_setup, "Never Called Home News Setup"
583 | neverCalled cb.home_news_load, "Never Called Home News Load"
584 | neverCalled cb.home_news_unload, "Never Called Home News Unload"
585 | neverCalled cb.home_news_teardown, "Never Called Home News Teardown"
586 | neverCalled cb.foo, "Never Called Foo"
587 |
588 | ok cb.home_setup.calledBefore(cb.home_load), "Called Home setup before load"
589 |
590 | cb.reset()
591 |
592 | Finch.call("/home/news")
593 |
594 | neverCalled cb.home_setup, "Never Called Home Setup"
595 | neverCalled cb.home_load, "Never Called Home Load"
596 | calledOnce cb.home_unload, "Called Home Unload"
597 | neverCalled cb.home_teardown, "Never Called Home Teardown"
598 | calledOnce cb.home_news_setup, "Called Home News Setup"
599 | calledOnce cb.home_news_load, "Called Home News Load"
600 | neverCalled cb.home_news_unload, "Never Called Home News Unload"
601 | neverCalled cb.home_news_teardown, "Never Called Home News Teardown"
602 | neverCalled cb.foo, "Never Called Foo"
603 |
604 | ok cb.home_unload.calledBefore(cb.home_news_setup), "Home unload called before Home/News setup"
605 | ok cb.home_news_setup.calledBefore(cb.home_news_load), "Home/News setup called before Home/News load"
606 |
607 | cb.reset()
608 |
609 | Finch.call("/foo")
610 |
611 | neverCalled cb.home_setup, "Never Called Home Setup"
612 | neverCalled cb.home_load, "Never Called Home Load"
613 | neverCalled cb.home_unload, "Never Called Home Unload"
614 | neverCalled cb.home_teardown, "Never Called Home Teardown"
615 | neverCalled cb.home_news_setup, "Never Called Home News Setup"
616 | neverCalled cb.home_news_load, "Never Called Home News Load"
617 | calledOnce cb.home_news_unload, "Never Called Home News Unload"
618 | neverCalled cb.home_news_teardown, "Never Called Home News Teardown"
619 | neverCalled cb.foo, "Never Called Foo"
620 |
621 | call = cb.home_news_unload.getCall(0)
622 | call_context = call.args[0]
623 | call_next = call.args[1]
624 |
625 | ok call_context.did_setup?, "Setup was passed in context"
626 | ok call_context.did_load?, "Load was passed in context"
627 | ok call_context.did_unload?, "Unload was passed in context"
628 | ok not call_context.did_teardown?, "Teardown was not passed in context"
629 |
630 | call_next()
631 |
632 | neverCalled cb.home_setup, "Never Called Home Setup"
633 | neverCalled cb.home_load, "Never Called Home Load"
634 | neverCalled cb.home_unload, "Never Called Home Unload"
635 | calledOnce cb.home_teardown, "Called Home Teardown"
636 | neverCalled cb.home_news_setup, "Never Called Home News Setup"
637 | neverCalled cb.home_news_load, "Never Called Home News Load"
638 | calledOnce cb.home_news_unload, "Called Home News Unload"
639 | calledOnce cb.home_news_teardown, "Called Home News Teardown"
640 | calledOnce cb.foo, "Called Foo"
641 |
642 | cb.reset()
643 |
644 | test "Reload", sinon.test ->
645 |
646 | cb = callbackGroup()
647 | cb.home_setup = @stub()
648 | cb.home_load = @stub()
649 | cb.home_unload = @stub()
650 | cb.home_teardown = @stub()
651 |
652 | Finch.route "/home",
653 | setup: (bindings, next) ->
654 | cb.home_setup()
655 | next()
656 | load: (bindings, next) ->
657 | cb.home_load()
658 | next()
659 | unload: (bindings, next) ->
660 | cb.home_unload()
661 | next()
662 | teardown: (bindings, next) ->
663 | cb.home_teardown()
664 | next()
665 |
666 | cb.home_news_setup = @stub()
667 | cb.home_news_load = @stub()
668 | cb.home_news_unload = @stub()
669 | cb.home_news_teardown = @stub()
670 |
671 | Finch.route "[/home]/news",
672 | setup: (bindings, next) ->
673 | @did_setup = true
674 | cb.home_news_setup(this, next)
675 | load: (bindings, next) ->
676 | @did_load = true
677 | cb.home_news_load(this, next)
678 | unload: (bindings, next) ->
679 | @did_unload = true
680 | cb.home_news_unload(this, next)
681 | teardown: (bindings, next) ->
682 | @did_teardown = true
683 | cb.home_news_teardown()
684 | next()
685 |
686 | Finch.call("/home")
687 |
688 | calledOnce cb.home_setup, "Called Home Setup"
689 | calledOnce cb.home_load, "Called Home Load"
690 | neverCalled cb.home_unload, "Never Called Home Unload"
691 | neverCalled cb.home_teardown, "Never Called Home Teardown"
692 | neverCalled cb.home_news_setup, "Never Called Home News Setup"
693 | neverCalled cb.home_news_load, "Never Called Home News Load"
694 | neverCalled cb.home_news_unload, "Never Called Home News Unload"
695 | neverCalled cb.home_news_teardown, "Never Called Home News Teardown"
696 |
697 | cb.reset()
698 | Finch.reload()
699 |
700 | neverCalled cb.home_setup, "Never Called Home Setup"
701 | calledOnce cb.home_load, "Called Home Load"
702 | calledOnce cb.home_unload, "Called Home Unload"
703 | neverCalled cb.home_teardown, "Never Called Home Teardown"
704 | neverCalled cb.home_news_setup, "Never Called Home News Setup"
705 | neverCalled cb.home_news_load, "Never Called Home News Load"
706 | neverCalled cb.home_news_unload, "Never Called Home News Unload"
707 | neverCalled cb.home_news_teardown, "Never Called Home News Teardown"
708 |
709 | cb.reset()
710 | Finch.call("/home/news")
711 |
712 | neverCalled cb.home_setup, "Never Called Home Setup"
713 | neverCalled cb.home_load, "Never Called Home Load"
714 | calledOnce cb.home_unload, "Called Home Unload"
715 | neverCalled cb.home_teardown, "Never Called Home Teardown"
716 | calledOnce cb.home_news_setup, "Called Home News Setup"
717 | neverCalled cb.home_news_load, "Never Called Home News Load"
718 | neverCalled cb.home_news_unload, "Never Called Home News Unload"
719 | neverCalled cb.home_news_teardown, "Never Called Home News Teardown"
720 |
721 | call = cb.home_news_setup.getCall(0)
722 | call_context = call.args[0]
723 | call_next = call.args[1]
724 |
725 | ok call_context.did_setup?, "Setup was passed in context"
726 | ok not call_context.did_load?, "Load was not passed in context"
727 | ok not call_context.did_unload?, "Unload was not passed in context"
728 | ok not call_context.did_teardown?, "Teardown was not passed in context"
729 |
730 | cb.reset()
731 | Finch.reload()
732 |
733 | neverCalled cb.home_setup, "Never Called Home Setup"
734 | neverCalled cb.home_load, "Never Called Home Load"
735 | neverCalled cb.home_unload, "Never Called Home Unload"
736 | neverCalled cb.home_teardown, "Never Called Home Teardown"
737 | neverCalled cb.home_news_setup, "Never Called Home News Setup"
738 | neverCalled cb.home_news_load, "Never Called Home News Load"
739 | neverCalled cb.home_news_unload, "Never Called Home News Unload"
740 | neverCalled cb.home_news_teardown, "Never Called Home News Teardown"
741 |
742 | cb.reset()
743 | call_next()
744 |
745 | neverCalled cb.home_setup, "Never Called Home Setup"
746 | neverCalled cb.home_load, "Never Called Home Load"
747 | neverCalled cb.home_unload, "Never Called Home Unload"
748 | neverCalled cb.home_teardown, "Never Called Home Teardown"
749 | neverCalled cb.home_news_setup, "Never Called Home News Setup"
750 | calledOnce cb.home_news_load, "Called Home News Load"
751 | neverCalled cb.home_news_unload, "Never Called Home News Unload"
752 | neverCalled cb.home_news_teardown, "Never Called Home News Teardown"
753 |
754 | call = cb.home_news_load.getCall(0)
755 | call_context = call.args[0]
756 | call_next = call.args[1]
757 |
758 | ok call_context.did_setup?, "Setup was passed in context"
759 | ok call_context.did_load?, "Load was passed in context"
760 | ok not call_context.did_unload?, "Unload was not passed in context"
761 | ok not call_context.did_teardown?, "Teardown was not passed in context"
762 |
763 | cb.reset()
764 | Finch.reload()
765 |
766 | neverCalled cb.home_setup, "Never Called Home Setup"
767 | neverCalled cb.home_load, "Never Called Home Load"
768 | neverCalled cb.home_unload, "Never Called Home Unload"
769 | neverCalled cb.home_teardown, "Never Called Home Teardown"
770 | neverCalled cb.home_news_setup, "Never Called Home News Setup"
771 | neverCalled cb.home_news_load, "Never Called Home News Load"
772 | neverCalled cb.home_news_unload, "Never Called Home News Unload"
773 | neverCalled cb.home_news_teardown, "Never Called Home News Teardown"
774 |
775 | cb.reset()
776 | call_next()
777 | Finch.reload()
778 |
779 | neverCalled cb.home_setup, "Never Called Home Setup"
780 | neverCalled cb.home_load, "Never Called Home Load"
781 | neverCalled cb.home_unload, "Never Called Home Unload"
782 | neverCalled cb.home_teardown, "Never Called Home Teardown"
783 | neverCalled cb.home_news_setup, "Never Called Home News Setup"
784 | neverCalled cb.home_news_load, "Never Called Home News Load"
785 | calledOnce cb.home_news_unload, "Called Home News Unload"
786 | neverCalled cb.home_news_teardown, "Never Called Home News Teardown"
787 |
788 | call = cb.home_news_unload.getCall(0)
789 | call_context = call.args[0]
790 | call_next = call.args[1]
791 |
792 | ok call_context.did_setup?, "Setup was passed in context"
793 | ok call_context.did_load?, "Load was passed in context"
794 | ok call_context.did_unload?, "Unload was passed in context"
795 | ok not call_context.did_teardown?, "Teardown was not passed in context"
796 |
797 | cb.reset()
798 | Finch.reload()
799 |
800 | neverCalled cb.home_setup, "Never Called Home Setup"
801 | neverCalled cb.home_load, "Never Called Home Load"
802 | neverCalled cb.home_unload, "Never Called Home Unload"
803 | neverCalled cb.home_teardown, "Never Called Home Teardown"
804 | neverCalled cb.home_news_setup, "Never Called Home News Setup"
805 | neverCalled cb.home_news_load, "Never Called Home News Load"
806 | neverCalled cb.home_news_unload, "Never Called Home News Unload"
807 | neverCalled cb.home_news_teardown, "Never Called Home News Teardown"
808 |
809 | cb.reset()
810 | call_next()
811 |
812 | neverCalled cb.home_setup, "Never Called Home Setup"
813 | neverCalled cb.home_load, "Never Called Home Load"
814 | neverCalled cb.home_unload, "Never Called Home Unload"
815 | neverCalled cb.home_teardown, "Never Called Home Teardown"
816 | neverCalled cb.home_news_setup, "Never Called Home News Setup"
817 | calledOnce cb.home_news_load, "Called Home News Load"
818 | neverCalled cb.home_news_unload, "Never Called Home News Unload"
819 | neverCalled cb.home_news_teardown, "Never Called Home News Teardown"
820 |
821 | call = cb.home_news_load.getCall(0)
822 | call_context = call.args[0]
823 | call_next = call.args[1]
824 |
825 | ok call_context.did_setup?, "Setup was passed in context"
826 | ok call_context.did_load?, "Load was passed in context"
827 | ok call_context.did_unload?, "Unload was passed in context"
828 | ok not call_context.did_teardown?, "Teardown was not passed in context"
829 |
830 | test "Route sanitation", sinon.test ->
831 |
832 | Finch.route "/", slash = @stub()
833 | Finch.route "/foo", foo = @stub()
834 | Finch.route "/foo/bar", foo_bar = @stub()
835 |
836 | Finch.call ""
837 | calledOnce slash, "/ called once"
838 | slash.reset()
839 |
840 | Finch.call "/"
841 | neverCalled slash, "/ not called again"
842 | slash.reset()
843 |
844 | Finch.call ""
845 | neverCalled slash, "/ not called again"
846 | slash.reset()
847 |
848 | Finch.call "//"
849 | neverCalled slash, "/ not called again"
850 | slash.reset()
851 |
852 | Finch.call "foo"
853 | neverCalled slash, "/ not called again"
854 | calledOnce foo, "foo called once"
855 | slash.reset()
856 | foo.reset()
857 |
858 | Finch.call "/foo"
859 | neverCalled slash, "/ not called again"
860 | neverCalled foo, "foo not called again"
861 | slash.reset()
862 | foo.reset()
863 |
864 | Finch.call "/foo/"
865 | neverCalled slash, "/ not called again"
866 | neverCalled foo, "foo not called again"
867 | slash.reset()
868 | foo.reset()
869 |
870 | Finch.call "foo/"
871 | neverCalled slash, "/ not called again"
872 | neverCalled foo, "foo not called again"
873 | slash.reset()
874 | foo.reset()
875 |
876 | Finch.call "foo/bar"
877 | neverCalled slash, "/ not called again"
878 | neverCalled foo, "foo not called again"
879 | calledOnce foo_bar, "foo/bar called once"
880 | slash.reset()
881 | foo.reset()
882 | foo_bar.reset()
883 |
884 | Finch.call "/foo/bar"
885 | neverCalled slash, "/ not called again"
886 | neverCalled foo, "foo not called again"
887 | neverCalled foo_bar, "foo/bar not called again"
888 | slash.reset()
889 | foo.reset()
890 | foo_bar.reset()
891 |
892 | Finch.call "/foo/bar/"
893 | neverCalled slash, "/ not called again"
894 | neverCalled foo, "foo not called again"
895 | neverCalled foo_bar, "foo/bar not called again"
896 | slash.reset()
897 | foo.reset()
898 | foo_bar.reset()
899 |
900 | Finch.call "foo/bar/"
901 | neverCalled slash, "/ not called again"
902 | neverCalled foo, "foo not called again"
903 | neverCalled foo_bar, "foo/bar not called again"
904 | slash.reset()
905 | foo.reset()
906 | foo_bar.reset()
907 |
908 | test "Asynchronous setup, load, and teardown", sinon.test ->
909 | cb = callbackGroup()
910 | cb.setup_foo = @stub()
911 | cb.load_foo = @stub()
912 | cb.teardown_foo = @stub()
913 | cb.setup_foo_bar = @stub()
914 | cb.load_foo_bar = @stub()
915 |
916 | Finch.route "foo",
917 | setup: (bindings, callback) -> cb.setup_foo bindings, callback
918 | load: (bindings, callback) -> cb.load_foo bindings, callback
919 | teardown: (bindings, callback) -> cb.teardown_foo bindings, callback
920 | Finch.route "foo/bar",
921 | setup: (bindings, callback) -> cb.setup_foo_bar bindings, callback
922 | load: (bindings, callback) -> cb.load_foo_bar bindings, callback
923 | teardown: cb.teardown_foo_bar = @stub()
924 | Finch.route "[foo/bar]/baz",
925 | setup: cb.setup_foo_bar_baz = @stub()
926 | teardown: cb.teardown_foo_bar_baz = @stub()
927 | Finch.route "quux",
928 | setup: cb.setup_quux = @stub()
929 |
930 | # Call /foo to start
931 | Finch.call "/foo"
932 |
933 | calledOnce cb.setup_foo, "/foo (before /foo callback): foo setup called once"
934 | neverCalled cb.load_foo, "/foo (after /foo callback): foo load not called"
935 | neverCalled cb.teardown_foo, "/foo (after /foo callback): foo teardown not called"
936 |
937 | cb.setup_foo.callArg 1
938 | calledOnce cb.setup_foo, "/foo (after /foo callback): foo setup not called again"
939 | calledOnce cb.load_foo, "/foo (before /foo callback): foo load called once"
940 | neverCalled cb.teardown_foo, "/foo (after /foo callback): foo teardown not called"
941 |
942 | cb.load_foo.callArg 1
943 | calledOnce cb.setup_foo, "/foo (after /foo callback): foo setup not called again"
944 | calledOnce cb.load_foo, "/foo (after /foo callback): foo load not called again"
945 | neverCalled cb.teardown_foo, "/foo (after /foo callback): foo teardown not called"
946 |
947 | cb.reset()
948 |
949 | # Call /foo/bar/baz next
950 | Finch.call "/foo/bar/baz"
951 |
952 | calledOnce cb.teardown_foo, "/foo/bar/baz (before /foo teardown): foo teardown called once"
953 | neverCalled cb.setup_foo_bar, "/foo/bar/baz (before /foo teardown): foo/bar setup not called yet"
954 | neverCalled cb.load_foo_bar, "/foo/bar/baz (before /foo teardown): foo/bar load not called yet"
955 |
956 | cb.teardown_foo.callArg 1
957 |
958 | calledOnce cb.setup_foo_bar, "/foo/bar/baz (before /foo/bar callback): foo/bar setup called once"
959 | neverCalled cb.load_foo_bar, "/foo/bar/baz (before /foo/bar callback): foo/bar load not called"
960 | neverCalled cb.setup_foo_bar_baz, "/foo/bar/baz (before /foo/bar callback): foo/bar/baz setup not called yet"
961 |
962 | # Call /quux before the call to /foo/bar/baz completes
963 | Finch.call "/quux"
964 |
965 | calledOnce cb.setup_foo_bar, "/quux (before /foo/bar callback): foo/bar setup not called again"
966 | neverCalled cb.setup_foo_bar_baz, "/quux (before /foo/bar callback): foo/bar/baz setup not called"
967 | neverCalled cb.setup_quux, "/quux (before /foo/bar callback): quux setup not called yet"
968 |
969 | cb.setup_foo_bar.callArg 1
970 |
971 | equal cb.setup_foo_bar.callCount, 1, "/quux (after /foo/bar callback): foo/bar setup not called again"
972 | equal cb.teardown_foo_bar.callCount, 1, "/quux (after /foo/bar callback): foo/bar teardown called"
973 | equal cb.setup_foo_bar_baz.callCount, 0, "/quux (after /foo/bar callback): foo/bar/baz setup not called"
974 | equal cb.teardown_foo_bar_baz.callCount, 0, "/quux (after /foo/bar callback): foo/bar/baz teardown not called"
975 | equal cb.setup_quux.callCount, 1, "/quux (after /foo/bar callback): quux setup called"
976 | calledOnce cb.setup_foo_bar, "/quux (after /foo/bar callback): foo/bar setup not called again"
977 | calledOnce cb.teardown_foo_bar, "/quux (after /foo/bar callback): foo/bar teardown called"
978 | neverCalled cb.setup_foo_bar_baz, "/quux (after /foo/bar callback): foo/bar/baz setup not called"
979 | neverCalled cb.teardown_foo_bar_baz, "/quux (after /foo/bar callback): foo/bar/baz teardown not called"
980 | calledOnce cb.setup_quux, "/quux (after /foo/bar callback): quux setup called"
981 |
982 | do ->
983 | trivialObservableTest = (fn) ->
984 |
985 | Finch.call "/foo"
986 | calledOnce fn, "observable callback called once"
987 | lastCalledWithExactly fn, [undefined, undefined], "called with given args"
988 | fn.reset()
989 |
990 | Finch.call "/foo?sort=asc"
991 | calledOnce fn, "observable callback called once"
992 | lastCalledWithExactly fn, ["asc", undefined], "called with given args"
993 | fn.reset()
994 |
995 | Finch.call "/foo"
996 | calledOnce fn, "observable callback called once"
997 | lastCalledWithExactly fn, [undefined, undefined], "called with given args"
998 | fn.reset()
999 |
1000 | Finch.call "/foo?query=unicorn"
1001 | calledOnce fn, "observable callback called once"
1002 | lastCalledWithExactly fn, [undefined, "unicorn"], "called with given args"
1003 | fn.reset()
1004 |
1005 | Finch.call "/foo?query=unicorn&sort=desc"
1006 | calledOnce fn, "observable callback called once"
1007 | lastCalledWithExactly fn, ["desc", "unicorn"], "called with given args"
1008 | fn.reset()
1009 |
1010 | Finch.call "/foo?sort=desc&query=unicorn"
1011 | neverCalled fn, "observable callback not called"
1012 | fn.reset()
1013 |
1014 | Finch.call "/foo"
1015 | calledOnce fn, "observable callback called once"
1016 | lastCalledWithExactly fn, [undefined, undefined], "called with given args"
1017 | fn.reset()
1018 |
1019 | Finch.call "/foo?Unrelated=Parameter"
1020 | neverCalled fn, "observable callback not called"
1021 |
1022 | test "Trivial observable test (accessor form)", sinon.test ->
1023 |
1024 | fn = @stub()
1025 |
1026 | Finch.route "foo", (bindings) ->
1027 | Finch.observe (params) ->
1028 | fn(params("sort"), params("query"))
1029 |
1030 | trivialObservableTest(fn)
1031 |
1032 | test "Trivial observable test (binding array form)", sinon.test ->
1033 |
1034 | fn = @stub()
1035 |
1036 | Finch.route "foo", (bindings) ->
1037 | Finch.observe ["sort", "query"], (sort, query) ->
1038 | fn(sort, query)
1039 |
1040 | trivialObservableTest(fn)
1041 |
1042 | test "Trivial observable test (binding list form)", sinon.test ->
1043 |
1044 | fn = @stub()
1045 |
1046 | Finch.route "foo", (bindings) ->
1047 | Finch.observe "sort", "query", (sort, query) ->
1048 | fn(sort, query)
1049 |
1050 | trivialObservableTest(fn)
1051 |
1052 | # END trivial observable test
1053 |
1054 | test "Observable dependency tracking", sinon.test ->
1055 |
1056 | bar_on = @stub()
1057 | bar_off = @stub()
1058 |
1059 | Finch.route "bar", (bindings) ->
1060 | Finch.observe (params) ->
1061 | if params("flag") then bar_on params("on") else bar_off params("off")
1062 |
1063 | Finch.call("/bar")
1064 |
1065 | calledOnce bar_off, "off callback called once"
1066 | neverCalled bar_on, "on callback not called"
1067 | lastCalledWithExactly bar_off, [undefined], "called with given args"
1068 | bar_off.reset()
1069 |
1070 | Finch.call("/bar?off=Grue")
1071 |
1072 | calledOnce bar_off, "off callback called once"
1073 | neverCalled bar_on, "on callback not called"
1074 | lastCalledWithExactly bar_off, ["Grue"], "called with given args"
1075 | bar_off.reset()
1076 |
1077 | Finch.call("/bar?off=Grue&on=Lantern")
1078 |
1079 | neverCalled bar_off, "off callback not called"
1080 | neverCalled bar_on, "on callback not called"
1081 |
1082 | Finch.call("/bar?flag=true&off=Grue&on=Lantern")
1083 |
1084 | neverCalled bar_off, "off callback not called"
1085 | calledOnce bar_on, "on callback called once"
1086 | lastCalledWithExactly bar_on, ["Lantern"], "called with given args"
1087 | bar_on.reset()
1088 |
1089 | Finch.call("/bar?flag=true&on=Lantern")
1090 |
1091 | neverCalled bar_off, "off callback not called"
1092 | neverCalled bar_on, "on callback not called"
1093 |
1094 | test "Observable hierarchy 1", sinon.test ->
1095 |
1096 | foo = @stub()
1097 | bar = @stub()
1098 | id = @stub()
1099 |
1100 | Finch.route "foo", (bindings) ->
1101 | Finch.observe ["a"], (a) -> foo(a)
1102 | Finch.route "[foo]/bar", (bindings) ->
1103 | Finch.observe ["b"], (b) -> bar(b)
1104 | Finch.route "[foo/bar]/:id", (bindings) ->
1105 | Finch.observe ["c"], (c) -> id(c)
1106 |
1107 | Finch.call "/foo/bar?&a=1&b=2&c=3"
1108 |
1109 | calledOnce foo, "foo callback called once"
1110 | lastCalledWithExactly foo, ["1"], "foo callback args"
1111 | calledOnce bar, "bar callback called once"
1112 | lastCalledWithExactly bar, ["2"], "bar callback args"
1113 | neverCalled id, "id callback not called"
1114 |
1115 | foo.reset()
1116 | bar.reset()
1117 | id.reset()
1118 |
1119 | Finch.call "/foo/bar?a=1&b=2&c=11"
1120 |
1121 | neverCalled foo, "foo callback not called"
1122 | neverCalled bar, "bar callback not called"
1123 | neverCalled id, "id callback not called"
1124 |
1125 | foo.reset()
1126 | bar.reset()
1127 | id.reset()
1128 |
1129 | Finch.call "/foo?a=21&b=2&c=23"
1130 |
1131 | calledOnce foo, "foo callback called once"
1132 | lastCalledWithExactly foo, ["21"], "foo callback args"
1133 | neverCalled bar, "bar callback not called"
1134 | neverCalled id, "id callback not called"
1135 |
1136 | foo.reset()
1137 | bar.reset()
1138 | id.reset()
1139 |
1140 | Finch.call "/foo?a=31&b=32&c=23"
1141 |
1142 | calledOnce foo, "foo callback called once"
1143 | lastCalledWithExactly foo, ["31"], "foo callback args"
1144 | neverCalled bar, "bar callback not called"
1145 | neverCalled id, "id callback not called"
1146 |
1147 | test "Observable hierarchy 2", sinon.test ->
1148 |
1149 | slash = @stub()
1150 | foo = @stub()
1151 | bar = @stub()
1152 | id = @stub()
1153 |
1154 | Finch.route "/", (bindings) ->
1155 | Finch.observe ["x"], (x) -> slash(x)
1156 | Finch.route "[/]foo", (bindings) ->
1157 | Finch.observe ["a"], (a) -> foo(a)
1158 | Finch.route "[/foo]/bar", (bindings) ->
1159 | Finch.observe ["b"], (b) -> bar(b)
1160 | Finch.route "[/foo/bar]/:id", (bindings) ->
1161 | Finch.observe ["c"], (c) -> id(c)
1162 |
1163 | Finch.call "/foo/bar?x=0&a=1&b=2&c=3"
1164 |
1165 | calledOnce slash, "/ callback called once"
1166 | lastCalledWithExactly slash, ["0"], "/ callback args"
1167 | calledOnce foo, "foo callback called once"
1168 | lastCalledWithExactly foo, ["1"], "foo callback args"
1169 | calledOnce bar, "bar callback called once"
1170 | lastCalledWithExactly bar, ["2"], "bar callback args"
1171 | neverCalled id, "id callback not called"
1172 |
1173 | slash.reset()
1174 | foo.reset()
1175 | bar.reset()
1176 | id.reset()
1177 |
1178 | Finch.call "/foo/bar?x=0&a=1&b=10&c=11"
1179 |
1180 | neverCalled slash, "/ callback not called"
1181 | neverCalled foo, "foo callback not called"
1182 | calledOnce bar, "bar callback called once"
1183 | lastCalledWithExactly bar, ["10"], "bar callback args"
1184 | neverCalled id, "id callback not called"
1185 |
1186 | test "Observable value types", sinon.test ->
1187 |
1188 | stub = @stub()
1189 |
1190 | Finch.route "/", (bindings) ->
1191 | Finch.observe ["x"], (x) -> stub(x)
1192 |
1193 | Finch.call "/?x=123"
1194 | calledOnce stub, "/ callback called once"
1195 | lastCalledWithExactly stub, ["123"], "/ called with correct 123"
1196 | stub.reset()
1197 |
1198 | Finch.call "/?x=123.456"
1199 | calledOnce stub, "/ callback called once"
1200 | lastCalledWithExactly stub, ["123.456"], "/ called with correct 123.456"
1201 | stub.reset()
1202 |
1203 | Finch.call "/?x=true"
1204 | calledOnce stub, "/ callback called once"
1205 | lastCalledWithExactly stub, ["true"], "/ called with correct true"
1206 | stub.reset()
1207 |
1208 | Finch.call "/?x=false"
1209 | calledOnce stub, "/ callback called once"
1210 | lastCalledWithExactly stub, ["false"], "/ called with correct false"
1211 | stub.reset()
1212 |
1213 | Finch.call "/?x=stuff"
1214 | calledOnce stub, "/ callback called once"
1215 | lastCalledWithExactly stub, ["stuff"], "/ called with correct stuff"
1216 | stub.reset()
1217 |
1218 | Finch.options(CoerceParameterTypes: true)
1219 |
1220 | Finch.call "/?x=123"
1221 | calledOnce stub, "/ callback called once"
1222 | lastCalledWithExactly stub, [123], "/ called with correct 123"
1223 | stub.reset()
1224 |
1225 | Finch.call "/?x=123.456"
1226 | calledOnce stub, "/ callback called once"
1227 | lastCalledWithExactly stub, [123.456], "/ called with correct 123.456"
1228 | stub.reset()
1229 |
1230 | Finch.call "/?x=true"
1231 | calledOnce stub, "/ callback called once"
1232 | lastCalledWithExactly stub, [true], "/ called with correct true"
1233 | stub.reset()
1234 |
1235 | Finch.call "/?x=false"
1236 | calledOnce stub, "/ callback called once"
1237 | lastCalledWithExactly stub, [false], "/ called with correct false"
1238 | stub.reset()
1239 |
1240 | Finch.call "/?x=stuff"
1241 | calledOnce stub, "/ callback called once"
1242 | lastCalledWithExactly stub, ["stuff"], "/ called with correct stuff"
1243 | stub.reset()
1244 |
1245 | test "Binding value types", sinon.test ->
1246 |
1247 | stub = @stub()
1248 |
1249 | Finch.route "/:x", ({x}) -> stub(x)
1250 |
1251 | Finch.call "/123"
1252 | calledOnce stub, "/ callback called once"
1253 | lastCalledWithExactly stub, ['123'], "/ called with correct 123"
1254 | stub.reset()
1255 |
1256 | Finch.call "/123.456"
1257 | calledOnce stub, "/ callback called once"
1258 | lastCalledWithExactly stub, ['123.456'], "/ called with correct 123.456"
1259 | stub.reset()
1260 |
1261 | Finch.call "/true"
1262 | calledOnce stub, "/ callback called once"
1263 | lastCalledWithExactly stub, ['true'], "/ called with correct true"
1264 | stub.reset()
1265 |
1266 | Finch.call "/false"
1267 | calledOnce stub, "/ callback called once"
1268 | lastCalledWithExactly stub, ['false'], "/ called with correct false"
1269 | stub.reset()
1270 |
1271 | Finch.call "/stuff"
1272 | calledOnce stub, "/ callback called once"
1273 | lastCalledWithExactly stub, ["stuff"], "/ called with correct stuff"
1274 | stub.reset()
1275 |
1276 | Finch.options(CoerceParameterTypes: true)
1277 |
1278 | Finch.call "/123"
1279 | calledOnce stub, "/ callback called once"
1280 | lastCalledWithExactly stub, [123], "/ called with correct 123"
1281 | stub.reset()
1282 |
1283 | Finch.call "/123.456"
1284 | calledOnce stub, "/ callback called once"
1285 | lastCalledWithExactly stub, [123.456], "/ called with correct 123.456"
1286 | stub.reset()
1287 |
1288 | Finch.call "/true"
1289 | calledOnce stub, "/ callback called once"
1290 | lastCalledWithExactly stub, [true], "/ called with correct true"
1291 | stub.reset()
1292 |
1293 | Finch.call "/false"
1294 | calledOnce stub, "/ callback called once"
1295 | lastCalledWithExactly stub, [false], "/ called with correct false"
1296 | stub.reset()
1297 |
1298 | Finch.call "/stuff"
1299 | calledOnce stub, "/ callback called once"
1300 | lastCalledWithExactly stub, ["stuff"], "/ called with correct stuff"
1301 | stub.reset()
1302 |
1303 | test "Finch.navigate", sinon.test ->
1304 |
1305 | window.location.hash = ""
1306 |
1307 | hash = ->
1308 | return "#" + ( window.location.href.split("#", 2)[1] ? "" )
1309 |
1310 | homeRegex = /^#?\/home/
1311 | homeNewsRegex = /^#?\/home\/news/
1312 | homeAccountRegex = /^#?\/home\/account/
1313 | homeNewsArticleRegex = /^#?\/home\/news\/article/
1314 | helloWorldRegex = /^#?\/hello%20world/
1315 |
1316 | #Navigate to just a single route
1317 | Finch.navigate("/home")
1318 | ok homeRegex.test(hash()), "Navigate called and changed hash to /home"
1319 |
1320 | Finch.navigate("/home/news")
1321 | ok homeNewsRegex.test(hash()), "Navigate called and changed hash to /home/news"
1322 |
1323 | Finch.navigate("/home")
1324 | ok homeRegex.test(hash()), "fNavigate called and changed hash to /home"
1325 |
1326 | #navigate to a route and query parameters
1327 | Finch.navigate("/home", foo:"bar")
1328 | ok homeRegex.test(hash()), "Navigate remained on the /home route"
1329 | ok hash().indexOf("foo=bar") > -1, "Had correct query parameter set"
1330 |
1331 | #navigate to a route and query parameters
1332 | Finch.navigate("/home", hello:"world")
1333 | ok homeRegex.test(hash()), "Navigate remained on the /home route"
1334 | ok hash().indexOf("foo=bar") is -1, "Removed foo=bar"
1335 | ok hash().indexOf("hello=world") > -1, "Added hello=world"
1336 |
1337 | #Navigate to only a new hash
1338 | Finch.navigate(foos:"bars")
1339 | ok homeRegex.test(hash()), "Navigate remained on the /home route"
1340 | ok hash().indexOf("hello=world") is -1, "Removed hello=world"
1341 | ok hash().indexOf("foos=bars") > -1, "Added foos=bars"
1342 |
1343 | #Only update the hash
1344 | Finch.navigate(foos:"baz")
1345 | ok homeRegex.test(hash()), "Navigate remained on the /home route"
1346 | ok hash().indexOf("foos=baz") > -1, "Changed to foos=baz"
1347 |
1348 | Finch.navigate(hello:"world", true)
1349 | ok homeRegex.test(hash()), "Navigate remained on the /home route"
1350 | ok hash().indexOf("foos=baz") > -1, "Kept foos=baz"
1351 | ok hash().indexOf("hello=world") > -1, "Added hello=world"
1352 |
1353 | #Remove a paremeter
1354 | Finch.navigate(foos:null, true)
1355 | ok homeRegex.test(hash()), "Navigate remained on the /home route"
1356 | ok hash().indexOf("foos=baz") is -1, "Removed foos=baz"
1357 | ok hash().indexOf("hello=world") > -1, "Kept hello=world"
1358 |
1359 | #Make siure the doUpdate navigate keeps the query string
1360 | Finch.navigate("/home/news", true)
1361 | ok homeNewsRegex.test(hash()), "Navigate called and changed hash to /home/news"
1362 | ok hash().indexOf("hello=world") > -1, "Kept hello=world"
1363 |
1364 | #Make sure we add proper escaping
1365 | Finch.navigate("/hello world", {})
1366 | ok helloWorldRegex.test(hash()), "Navigated to /hello%20world"
1367 | ok hash().indexOf("hello=world") is -1, "Removed hello=world"
1368 |
1369 | Finch.navigate("/hello world", foo:"bar bar")
1370 | ok helloWorldRegex.test(hash()), "Navigate remained at /hello%20world"
1371 | ok hash().indexOf("foo=bar%20bar") > -1, "Added and escaped foo=bar bar"
1372 |
1373 | Finch.navigate(foo:"baz baz")
1374 | ok helloWorldRegex.test(hash()), "Navigate remained at /hello%20world"
1375 | ok hash().indexOf("foo=bar%20bar") is -1, "Removed foo=bar bar"
1376 | ok hash().indexOf("foo=baz%20baz") > -1, "Added and escaped foo=baz baz"
1377 |
1378 | Finch.navigate(hello:'world world', true)
1379 | ok helloWorldRegex.test(hash()), "Navigate remained at /hello%20world"
1380 | ok hash().indexOf("foo=baz%20baz") > -1, "Kept and escaped foo=baz baz"
1381 | ok hash().indexOf("hello=world%20world") > -1, "Added and escaped hello=world world"
1382 |
1383 | #Make sure we don't add multiple '?'
1384 | Finch.navigate("/home?foo=bar",hello:"world")
1385 | ok homeRegex.test(hash()), "Navigate called and changed hash to /home"
1386 | ok hash().indexOf("foo=bar") > -1, "Had correct query parameter set foo=bar"
1387 | ok hash().indexOf("hello=world") > -1, "Had correct query parameter set hello=world"
1388 | equal hash().split("?").length-1, 1, "Correct number of '?'"
1389 | equal hash().split("&").length-1, 1, "Correct number of '&'"
1390 |
1391 | Finch.navigate("/home?foo=bar",{hello:"world",foo:"baz"})
1392 | ok homeRegex.test(hash()), "Navigate called and changed hash to /home"
1393 | ok hash().indexOf("foo=bar") is -1, "foo=bar not set"
1394 | ok hash().indexOf("foo=baz") > -1, "Had correct query parameter set foo=baz"
1395 | ok hash().indexOf("hello=world") > -1, "Had correct query parameter set hello=world"
1396 | equal hash().split("?").length-1, 1, "Correct number of '?'"
1397 | equal hash().split("&").length-1, 1, "Correct number of '&'"
1398 |
1399 | Finch.navigate("/home?foo=bar",{hello:"world",free:"bird"})
1400 | ok homeRegex.test(hash()), "Navigate called and changed hash to /home"
1401 | ok hash().indexOf("foo=bar") > -1, "Had correct query parameter set foo=bar"
1402 | ok hash().indexOf("free=bird") > -1, "Had correct query parameter set free=bird"
1403 | ok hash().indexOf("hello=world") > -1, "Had correct query parameter set hello=world"
1404 | equal hash().split("?").length-1, 1, "Correct number of '?'"
1405 | equal hash().split("&").length-1, 2, "Correct number of '&'"
1406 |
1407 | #Account for the hash character
1408 | Finch.navigate("#/home", true)
1409 | ok homeRegex.test(hash()), "Navigate called and changed hash to /home"
1410 | ok hash().indexOf("free=bird") > -1, "Had correct query parameter set free=bird"
1411 | ok hash().indexOf("hello=world") > -1, "Had correct query parameter set hello=world"
1412 |
1413 | Finch.navigate("#/home")
1414 | ok homeRegex.test(hash()), "Navigate called and changed hash to /home"
1415 | ok hash().indexOf("free=bird") is -1, "Had correct query parameter set free=bird"
1416 | ok hash().indexOf("hello=world") is -1, "Had correct query parameter set hello=world"
1417 |
1418 | Finch.navigate("#/home/news",{free:"birds",hello:"worlds"})
1419 | ok homeNewsRegex.test(hash()), "Navigate called and changed hash to /home"
1420 | ok hash().indexOf("free=birds") > -1, "Had correct query parameter set free=birds"
1421 | ok hash().indexOf("hello=worlds") > -1, "Had correct query parameter set hello=worlds"
1422 |
1423 | Finch.navigate("#/home/news", {foo:"bar"}, true)
1424 | ok homeNewsRegex.test(hash()), "Navigate called and changed hash to /home"
1425 | ok hash().indexOf("free=birds") > -1, "Had correct query parameter set free=birds"
1426 | ok hash().indexOf("hello=worlds") > -1, "Had correct query parameter set hello=worlds"
1427 | ok hash().indexOf("foo=bar") > -1, "Had correct query parameter set hello=worlds"
1428 |
1429 | #Test relative navigation
1430 | Finch.navigate("/home/news")
1431 | ok homeNewsRegex.test(hash()), "Navigate called and changed hash to /home/news"
1432 |
1433 | Finch.navigate("../")
1434 | ok homeRegex.test(hash()), "Navigate called and changed hash to /home"
1435 |
1436 | Finch.navigate("./")
1437 | ok homeRegex.test(hash()), "Navigate called and changed hash to /home"
1438 |
1439 | Finch.navigate("./news")
1440 | ok homeNewsRegex.test(hash()), "Navigate called and changed hash to /home/news"
1441 |
1442 | Finch.navigate("/home/news/article")
1443 | ok homeNewsArticleRegex.test(hash()), "Navigate called and changed hash to /home/news/article"
1444 |
1445 | Finch.navigate("../../account")
1446 | ok homeAccountRegex.test(hash()), "Navigate called and changed hash to /home/account"
1447 |
1448 | test "Finch.listen and Finch.ignore", sinon.test ->
1449 |
1450 | #Default the necessary window methods, if they don't exist
1451 | window.hasOwnProperty ?= (prop) -> (prop of @)
1452 |
1453 | cb = callbackGroup()
1454 | cb.attachEvent = @stub()
1455 | cb.detachEvent = @stub()
1456 | cb.addEventListener = @stub()
1457 | cb.removeEventListener = @stub()
1458 | cb.setInterval = @stub()
1459 | cb.clearInterval = @stub()
1460 |
1461 | clearWindowMethods = ->
1462 | window.attachEvent = null if "attachEvent" of window
1463 | window.detachEvent = null if "detachEvent" of window
1464 | window.addEventListener = null if "addEventListener" of window
1465 | window.removeEventListener = null if "removeEventListener" of window
1466 | window.setInterval = null if "setInterval" of window
1467 | window.clearInterval = null if "clearInterval" of window
1468 |
1469 | #Test the fallback set interval
1470 | clearWindowMethods()
1471 | window.setInterval = cb.setInterval
1472 | window.clearInterval = cb.clearInterval
1473 | cb.reset()
1474 |
1475 | ok Finch.listen(), "Finch successfully listening"
1476 | equal cb.addEventListener.callCount, 0,"addEventListener not called"
1477 | equal cb.attachEvent.callCount, 0,"attachEvent not called"
1478 | equal cb.setInterval.callCount, 1,"setInterval called once"
1479 |
1480 | ok Finch.ignore(), "Finch successfuly ignoring"
1481 | equal cb.removeEventListener.callCount, 0, "removeEventListener not called"
1482 | equal cb.detachEvent.callCount, 0, "detachEvent not called"
1483 | equal cb.clearInterval.callCount, 1, "clearInterval called once"
1484 |
1485 | # Test the add/remove EventListener methods
1486 | clearWindowMethods()
1487 | window.onhashchange = "defined"
1488 | window.addEventListener = cb.addEventListener
1489 | window.removeEventListener = cb.removeEventListener
1490 | cb.reset()
1491 |
1492 | ok Finch.listen(), "Finch successfully listening"
1493 | equal cb.addEventListener.callCount, 1,"addEventListener Called once"
1494 | equal cb.attachEvent.callCount, 0,"attachEvent not called"
1495 | equal cb.setInterval.callCount, 0,"setInterval not called"
1496 |
1497 | ok Finch.ignore(), "Finch successfuly ignoring"
1498 | equal cb.removeEventListener.callCount, 1, "removeEventListener Called once"
1499 | equal cb.detachEvent.callCount, 0, "detachEvent not called"
1500 | equal cb.clearInterval.callCount, 0, "clearInterval not called"
1501 |
1502 | #Test the attach/detach Event methods
1503 | clearWindowMethods()
1504 | window.onhashchange = "defined"
1505 | window.attachEvent = cb.attachEvent
1506 | window.detachEvent = cb.detachEvent
1507 | cb.reset()
1508 |
1509 | ok Finch.listen(), "Finch successfully listening"
1510 | equal cb.addEventListener.callCount, 0,"addEventListener not called"
1511 | equal cb.attachEvent.callCount, 1,"attachEvent called once"
1512 | equal cb.setInterval.callCount, 0,"setInterval not called"
1513 |
1514 | ok Finch.ignore(), "Finch successfuly ignoring"
1515 | equal cb.removeEventListener.callCount, 0, "removeEventListener not called"
1516 | equal cb.detachEvent.callCount, 1, "detachEvent called once"
1517 | equal cb.clearInterval.callCount, 0, "clearInterval not called"
1518 |
1519 | test "Finch.abort", sinon.test ->
1520 |
1521 | homeStub = @stub()
1522 | fooStub = @stub()
1523 |
1524 | Finch.route "/home", (bindings, continuation) -> homeStub()
1525 | Finch.route "/foo", (bindings, continuation) -> fooStub()
1526 |
1527 | #make a call to home
1528 | Finch.call("home")
1529 | equal homeStub.callCount, 1, "Home called correctly"
1530 | equal fooStub.callCount, 0, "Foo not called"
1531 |
1532 | homeStub.reset()
1533 | fooStub.reset()
1534 |
1535 | #Call foo
1536 | Finch.call("foo")
1537 | equal homeStub.callCount, 0, "Home not called"
1538 | equal fooStub.callCount, 0, "Foo not called"
1539 |
1540 | homeStub.reset()
1541 | fooStub.reset()
1542 |
1543 | #abort first, then call foo
1544 | Finch.abort()
1545 | Finch.call("foo")
1546 | equal homeStub.callCount, 0, "Home not called"
1547 | equal fooStub.callCount, 1, "Foo called correctly"
1548 |
1549 | test "Route finding backtracking 1", sinon.test ->
1550 |
1551 | Finch.route "/foo", foo = @stub()
1552 | Finch.route "[/foo]/bar", bar = @stub()
1553 | Finch.route "[/foo/bar]/baz", baz = @stub()
1554 |
1555 | Finch.route "/:var1", var1 = @stub()
1556 | Finch.route "[/:var1/]:var2", var2 = @stub()
1557 | Finch.route "[/:var1/:var2]/:var3", var3 = @stub()
1558 |
1559 | # Test routes
1560 |
1561 | Finch.call "/foo/nope"
1562 |
1563 | calledOnce var1, "var1 called once"
1564 | lastCalledWithExactly var1, [{var1: "foo"}], "var1 called with binding for var1"
1565 | calledOnce var2, "var2 called once"
1566 | lastCalledWithExactly var2, [{var1: "foo", var2: "nope"}], "var2 called with bindings for var1 and var2"
1567 | neverCalled foo, "foo never called"
1568 |
1569 | test "Route finding backtracking 2", sinon.test ->
1570 |
1571 | Finch.route "/foo", foo = @stub()
1572 | Finch.route "[/foo]/bar", bar = @stub()
1573 | Finch.route "[/foo/bar]/baz", baz = @stub()
1574 |
1575 | Finch.route "/:var1", var1 = @stub()
1576 | Finch.route "[/:var1/]:var2", var2 = @stub()
1577 | Finch.route "[/:var1/:var2]/:var3", var3 = @stub()
1578 |
1579 | # Test routes
1580 |
1581 | Finch.call "/foo/bar/nope"
1582 |
1583 | calledOnce var1, "var1 called once"
1584 | lastCalledWithExactly var1, [{var1: "foo"}], "var1 called with binding for var1"
1585 | calledOnce var2, "var2 called once"
1586 | lastCalledWithExactly var2, [{var1: "foo", var2: "bar"}], "var2 called with bindings for var1 and var2"
1587 | calledOnce var3, "var3 called once"
1588 | lastCalledWithExactly var3, [{var1: "foo", var2: "bar", var3: "nope"}], "var3 called with bindings for var1, var2 and var3"
1589 | neverCalled foo, "foo never called"
1590 | neverCalled bar, "bar never called"
1591 |
1592 | test "Optional parameter parsing", sinon.test ->
1593 |
1594 | Finch.route "/"
1595 | Finch.route "/home/news/:id", foo = @stub()
1596 | Finch.call "/home/news/1234"
1597 |
1598 | calledOnce foo, "foo called once"
1599 | lastCalledWithExactly foo, [{id: "1234"}], "foo called with int parameter"
1600 |
1601 | foo.reset()
1602 |
1603 | Finch.options { CoerceParameterTypes: true }
1604 |
1605 | Finch.call "/"
1606 | Finch.call "/home/news/1234"
1607 |
1608 | calledOnce foo, "foo called once"
1609 | lastCalledWithExactly foo, [{id: 1234}], "foo called with string parameter"
1610 |
1611 | test "Variable parent routes called if no children found", sinon.test ->
1612 | cb = callbackGroup()
1613 |
1614 | Finch.route "/",
1615 | 'setup': cb.slash_setup = @stub()
1616 | 'load': cb.slash_load = @stub()
1617 | 'unload': cb.slash_unload = @stub()
1618 | 'teardown': cb.slash_teardown = @stub()
1619 |
1620 | Finch.route "[/]users/profile",
1621 | 'setup': cb.profile_setup = @stub()
1622 | 'load': cb.profile_load = @stub()
1623 | 'unload': cb.profile_unload = @stub()
1624 | 'teardown': cb.profile_teardown = @stub()
1625 |
1626 | Finch.route "[/]:page",
1627 | 'setup': cb.page_setup = @stub()
1628 | 'load': cb.page_load = @stub()
1629 | 'unload': cb.page_unload = @stub()
1630 | 'teardown': cb.page_teardown = @stub()
1631 |
1632 | Finch.call "/users"
1633 |
1634 | calledOnce cb.slash_setup, "slash setup called once"
1635 | neverCalled cb.slash_load, "slash load never called"
1636 | neverCalled cb.slash_unload, "slash unload never called"
1637 | neverCalled cb.slash_teardown, "slash teardown never called"
1638 |
1639 | calledOnce cb.page_setup, "page setup called once"
1640 | calledOnce cb.page_load, "page load called once"
1641 | neverCalled cb.page_unload, "page unload never called"
1642 | neverCalled cb.page_teardown, "page unload never called"
1643 |
1644 | neverCalled cb.profile_setup, "profile setup never called"
1645 | neverCalled cb.profile_load, "profile load never called"
1646 | neverCalled cb.profile_unload, "profile unload never called"
1647 | neverCalled cb.profile_teardown, "profile teardown never called"
1648 |
1649 | lastCalledWithExactly cb.page_setup, [{page: "users"}], "page setup called with correct parameters"
1650 | lastCalledWithExactly cb.page_load, [{page: "users"}], "page setup called with correct parameters"
1651 |
1652 | test "Test double deep variable basic routes up and down", sinon.test ->
1653 | cb = callbackGroup()
1654 | Finch.route "/project/:project_id", cb.project_id_load = @stub()
1655 | Finch.route "[/project/:project_id]/milestone", cb.milestone_load = @stub()
1656 | Finch.route "[/project/:project_id/milestone]/:milestone_id", cb.milestone_id_load = @stub()
1657 |
1658 | Finch.call "/project/1234"
1659 | calledOnce cb.project_id_load
1660 | neverCalled cb.milestone_load
1661 | neverCalled cb.milestone_id_load
1662 | lastCalledWithExactly cb.project_id_load, [{project_id: "1234"}]
1663 | cb.reset()
1664 |
1665 | Finch.call "/project/1234/milestone"
1666 | neverCalled cb.project_id_load
1667 | calledOnce cb.milestone_load
1668 | neverCalled cb.milestone_id_load
1669 | lastCalledWithExactly cb.milestone_load, [{project_id: "1234"}]
1670 | cb.reset()
1671 |
1672 | Finch.call "/project/1234/milestone/5678"
1673 | neverCalled cb.project_id_load
1674 | neverCalled cb.milestone_load
1675 | calledOnce cb.milestone_id_load
1676 | lastCalledWithExactly cb.milestone_id_load, [{project_id: "1234", milestone_id: "5678"}]
1677 | cb.reset()
1678 |
1679 | Finch.call "/project/1234/milestone"
1680 | neverCalled cb.project_id_load
1681 | calledOnce cb.milestone_load
1682 | neverCalled cb.milestone_id_load
1683 | lastCalledWithExactly cb.milestone_load, [{project_id: "1234"}]
1684 | cb.reset()
1685 |
1686 | Finch.call "/project/1234"
1687 | calledOnce cb.project_id_load
1688 | neverCalled cb.milestone_load
1689 | neverCalled cb.milestone_id_load
1690 | lastCalledWithExactly cb.project_id_load, [{project_id: "1234"}]
1691 | cb.reset()
1692 |
1693 | test "Test double deep variable basic routes up and down", sinon.test ->
1694 | cb = callbackGroup()
1695 | Finch.route "/project/:project_id/milestone",
1696 | setup: cb.milestone_setup = @stub()
1697 | load: cb.milestone_load = @stub()
1698 | unload: cb.milestone_unload = @stub()
1699 | teardown: cb.milestone_teardown = @stub()
1700 | #END rout
1701 |
1702 | Finch.route "[/project/:project_id/milestone]/:milestone_id",
1703 | setup: cb.milestone_id_setup = @stub()
1704 | load: cb.milestone_id_load = @stub()
1705 | unload: cb.milestone_id_unload = @stub()
1706 | teardown: cb.milestone_id_teardown = @stub()
1707 | #END rout
1708 |
1709 | Finch.call "/project/1234/milestone"
1710 | calledOnce cb.milestone_setup
1711 | calledOnce cb.milestone_load
1712 | neverCalled cb.milestone_unload
1713 | neverCalled cb.milestone_teardown
1714 |
1715 | neverCalled cb.milestone_id_setup
1716 | neverCalled cb.milestone_id_load
1717 | neverCalled cb.milestone_id_unload
1718 | neverCalled cb.milestone_id_teardown
1719 |
1720 | lastCalledWithExactly cb.milestone_setup, [{project_id: "1234"}]
1721 | lastCalledWithExactly cb.milestone_load, [{project_id: "1234"}]
1722 |
1723 | cb.reset()
1724 |
1725 | Finch.call "/project/1234/milestone/5678"
1726 | neverCalled cb.milestone_setup
1727 | neverCalled cb.milestone_load
1728 | calledOnce cb.milestone_unload
1729 | neverCalled cb.milestone_teardown
1730 |
1731 | calledOnce cb.milestone_id_setup
1732 | calledOnce cb.milestone_id_load
1733 | neverCalled cb.milestone_id_unload
1734 | neverCalled cb.milestone_id_teardown
1735 |
1736 | lastCalledWithExactly cb.milestone_unload, [{project_id: "1234"}]
1737 |
1738 | lastCalledWithExactly cb.milestone_id_setup, [{project_id: "1234", milestone_id: "5678"}]
1739 | lastCalledWithExactly cb.milestone_id_load, [{project_id: "1234", milestone_id: "5678"}]
1740 | cb.reset()
1741 |
1742 |
1743 | Finch.call "/project/1234/milestone"
1744 | neverCalled cb.milestone_setup
1745 | calledOnce cb.milestone_load
1746 | neverCalled cb.milestone_unload
1747 | neverCalled cb.milestone_teardown
1748 |
1749 | neverCalled cb.milestone_id_setup
1750 | neverCalled cb.milestone_id_load
1751 | calledOnce cb.milestone_id_unload
1752 | calledOnce cb.milestone_id_teardown
1753 |
1754 | lastCalledWithExactly cb.milestone_load, [{project_id: "1234"}]
1755 |
1756 | lastCalledWithExactly cb.milestone_id_unload, [{project_id: "1234", milestone_id: "5678"}]
1757 | lastCalledWithExactly cb.milestone_id_teardown, [{project_id: "1234", milestone_id: "5678"}]
1758 | cb.reset()
1759 | #END test
1760 |
1761 | test "Test Finch.route chaining", sinon.test ->
1762 | newFinch = Finch.route("foo", -> true)
1763 | result = QUnit.equiv(Finch, newFinch)
1764 | QUnit.push(result, newFinch, Finch, 'Finch.route returned this for chaining')
1765 | #END test
1766 |
--------------------------------------------------------------------------------