├── .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 |

Tests for Finch.js

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 + ''; 651 | if (actual != expected) { 652 | output += ''; 653 | output += ''; 654 | } 655 | if (!result) { 656 | var source = sourceFromStacktrace(); 657 | if (source) { 658 | details.source = source; 659 | output += ''; 660 | } 661 | } 662 | output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + escapeInnerText(source) + '
    "; 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 | --------------------------------------------------------------------------------