1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 // ======================================================================== 8 // SC.routes Base Tests 9 // ======================================================================== 10 /*globals module test ok isObj equals expects */ 11 12 var router; 13 14 SC.routes.wantsHistory = YES; 15 16 module('SC.routes setup'); 17 18 test('Setup', function() { 19 equals(SC.routes._didSetup, NO, 'SC.routes should not have been setup yet'); 20 }); 21 22 module('SC.routes setup', { 23 24 setup: function() { 25 router = SC.Object.create({ 26 route: function() { 27 return; 28 } 29 }); 30 SC.run(function() { 31 SC.routes.add('foo', router, router.route); 32 }); 33 } 34 35 }); 36 37 test('Setup', function() { 38 SC.run(function() { 39 equals(SC.routes._didSetup, YES, 'SC.routes should have been setup'); 40 }); 41 }); 42 43 test('Initial route', function() { 44 equals(SC.routes.get('location'), '', 'Initial route is an empty string'); 45 }); 46 47 module('SC.routes._Route', { 48 49 setup: function() { 50 router = SC.Object.create({ 51 route: function() { 52 return; 53 } 54 }); 55 } 56 57 }); 58 59 test('Route tree', function() { 60 var r = SC.routes._Route.create(), 61 abc = ['a', 'b', 'c'], 62 abd = ['a', 'b', 'd'], 63 abe = ['a', 'b', ':e'], 64 as = ['a', '*foo'], 65 a, b, c, d, e, s, p; 66 67 r.add(abc, router, router.route); 68 r.add(abd, router, router.route); 69 r.add(abe, router, router.route); 70 r.add(as, router, router.route); 71 72 a = r.staticRoutes['a']; 73 ok(a, 'There should be a staticRoutes tree for a'); 74 ok(!a.target, 'A node should not have a target'); 75 ok(!a.method, 'A node should not have a method'); 76 77 b = a.staticRoutes['b']; 78 ok(b, 'There should be a staticRoutes tree for b'); 79 ok(!b.target, 'A node should not have a target'); 80 ok(!b.method, 'A node should not have a method'); 81 82 c = b.staticRoutes['c']; 83 ok(c, 'There should be a staticRoutes tree for c'); 84 equals(c.target, router, 'A leaf should have a target'); 85 equals(c.method, router.route, 'A leaf should have a method'); 86 87 d = b.staticRoutes['d']; 88 ok(d, 'There should be a staticRoutes tree for d'); 89 equals(d.target, router, 'A leaf should have a target'); 90 equals(d.method, router.route, 'A leaf should have a method'); 91 92 e = b.dynamicRoutes['e']; 93 ok(e, 'There should be a dynamicRoutes tree for e'); 94 equals(d.target, router, 'A leaf should have a target'); 95 equals(d.method, router.route, 'A leaf should have a method'); 96 97 s = a.wildcardRoutes['foo']; 98 ok(s, 'There should be a wildcardRoutes tree for a'); 99 100 equals(r.routeForParts(['a'], {}), null, 'routeForParts should return null for non existent routes'); 101 equals(r.routeForParts(['a', 'b'], {}), null, 'routeForParts should return null for non existent routes'); 102 equals(r.routeForParts(abc, {}), c, 'routeForParts should return the correct route for a/b/c'); 103 104 equals(r.routeForParts(abd, {}), d, 'routeForParts should return the correct route for a/b/d'); 105 106 abe[2] = 'foo'; 107 p = {}; 108 equals(r.routeForParts(abe, p), e, 'routeForParts should return the correct route for a/b/:e'); 109 equals(p['e'], 'foo', 'routeForParts should return the params for a/b/:e'); 110 111 p = {}; 112 equals(r.routeForParts(['a', 'double', 'double', 'toil', 'and', 'trouble'], p), s, 'routeForParts should return the correct route for a/*foo'); 113 equals(p.foo, 'double/double/toil/and/trouble', 'routeForParts should return the params for a/*foo'); 114 }); 115 116 module('SC.routes location', { 117 118 teardown: function() { 119 SC.routes.set('location', null); 120 } 121 122 }); 123 124 var routeWorks = function(route, name) { 125 SC.routes.set('location', route); 126 equals(SC.routes.get('location'), route, name + ' route has been set'); 127 128 setTimeout(function() { 129 equals(SC.routes.get('location'), route, name + ' route is still the same'); 130 start(); 131 }, 300); 132 133 stop(); 134 }; 135 136 test('Null route', function() { 137 SC.routes.set('location', null); 138 equals(SC.routes.get('location'), '', 'Null route is the empty string'); 139 }); 140 141 test('Simple route', function() { 142 routeWorks('sixty-six', 'simple'); 143 }); 144 145 test('UTF-8 route', function() { 146 routeWorks('éàçù߀', 'UTF-8'); 147 }); 148 149 test('Already escaped route', function() { 150 routeWorks('%C3%A9%C3%A0%20%C3%A7%C3%B9%20%C3%9F%E2%82%AC', 'already escaped'); 151 }); 152 153 module('SC.routes informLocation', { 154 155 teardown: function() { 156 SC.routes.set('informLocation', null); 157 } 158 159 }); 160 161 test('informLocation updates location', function() { 162 SC.routes.set('informLocation', 'simple'); 163 stop(); 164 165 setTimeout(function() { 166 equals(SC.routes.get('location'), 'simple'); 167 start(); 168 }, 300); 169 }); 170 171 test('informLocation and location invalidate each others caches', function() { 172 SC.routes.set('location', ''); 173 stop(); 174 175 setTimeout(function() { 176 equals(SC.routes.get('location'), ''); 177 SC.routes.set('informLocation', 'simple'); 178 179 setTimeout(function() { 180 equals(SC.routes.get('location'), 'simple'); 181 SC.routes.set('location', ''); 182 183 setTimeout(function() { 184 equals(SC.routes.get('location'), ''); 185 SC.routes.set('informLocation', 'simple'); 186 187 setTimeout(function() { 188 equals(SC.routes.get('location'), 'simple'); 189 start(); 190 }, 300); 191 }, 300); 192 }, 300); 193 }, 300); 194 }); 195 196 module('SC.routes defined routes', { 197 198 setup: function() { 199 router = SC.Object.create({ 200 params: null, 201 triggered: NO, 202 route: function(params) { 203 this.set('params', params); 204 }, 205 triggerRoute: function() { 206 this.triggered = YES; 207 } 208 }); 209 }, 210 211 teardown: function() { 212 SC.routes.set('location', null); 213 } 214 215 }); 216 217 test('setting location triggers function when only passed function', function() { 218 var barred = false; 219 220 SC.routes.add('bar', function(params) { 221 barred = true; 222 }); 223 SC.routes.set('location', 'bar'); 224 225 ok(barred, 'Function was called'); 226 }); 227 228 test('setting location simply triggers route', function() { 229 SC.routes.add("foo", router, "triggerRoute"); 230 SC.routes.set('location', 'bar'); 231 ok(!router.triggered, "Router not triggered with nonexistent route."); 232 233 SC.routes.set('location', 'foo'); 234 ok(router.triggered, "Router triggered."); 235 }); 236 237 test('calling trigger() triggers current location (again)', function() { 238 SC.routes.add("foo", router, "triggerRoute"); 239 SC.routes.set('location', 'foo'); 240 ok(router.triggered, "Router triggered first time."); 241 router.triggered = NO; 242 243 SC.routes.trigger(); 244 ok(router.triggered, "Router triggered (again)."); 245 }); 246 247 test('A mix of static, dynamic and wildcard route', function() { 248 var didObserve = false, 249 timer; 250 251 timer = setTimeout(function() { 252 ok(false, 'Route change was not notified within 2 seconds'); 253 window.start(); 254 }, 2000); 255 256 router.addObserver('params', function() { 257 if (!didObserve) { 258 didObserve = true; 259 same(router.get('params'), { controller: 'users', action: 'éàçù߀', id: '5', witches: 'double/double/toil/and/trouble' }); 260 clearTimeout(timer); 261 window.start(); 262 } 263 }); 264 265 SC.routes.add('foo/:controller/:action/bar/:id/*witches', router, router.route); 266 SC.routes.set('location', 'foo/users/éàçù߀/bar/5/double/double/toil/and/trouble'); 267 268 stop(); 269 }); 270 271 test('Route with parameters defined in a string', function() { 272 var didObserve = false, 273 timer; 274 275 timer = setTimeout(function() { 276 ok(false, 'Route change was not notified within 2 seconds'); 277 window.start(); 278 }, 2000); 279 280 router.addObserver('params', function() { 281 if (!didObserve) { 282 didObserve = true; 283 same(router.get('params'), { cuisine: 'french', party: '4', url: '' }); 284 clearTimeout(timer); 285 window.start(); 286 } 287 }); 288 289 SC.routes.add('*url', router, router.route); 290 SC.routes.set('location', '?cuisine=french&party=4'); 291 292 stop(); 293 }); 294 295 test('Route with parameters defined in a hash', function() { 296 var didObserve = false, 297 timer; 298 299 timer = setTimeout(function() { 300 ok(false, 'Route change was not notified within 2 seconds'); 301 window.start(); 302 }, 2000); 303 304 router.addObserver('params', function() { 305 if (!didObserve) { 306 didObserve = true; 307 same(router.get('params'), { cuisine: 'french', party: '4', url: '' }); 308 clearTimeout(timer); 309 window.start(); 310 } 311 }); 312 313 SC.routes.add('*url', router, router.route); 314 SC.routes.set('location', { cuisine: 'french', party: '4' }); 315 316 stop(); 317 }); 318 319 test('A mix of everything', function() { 320 var didObserve = false, 321 timer; 322 323 timer = setTimeout(function() { 324 ok(false, 'Route change was not notified within 2 seconds'); 325 window.start(); 326 }, 2000); 327 328 router.addObserver('params', function() { 329 if (!didObserve) { 330 didObserve = true; 331 same(router.get('params'), { controller: 'users', action: 'éàçù߀', id: '5', witches: 'double/double/toil/and/trouble', cuisine: 'french', party: '4' }); 332 clearTimeout(timer); 333 window.start(); 334 } 335 }); 336 337 SC.routes.add('foo/:controller/:action/bar/:id/*witches', router, router.route); 338 SC.routes.set('location', 'foo/users/éàçù߀/bar/5/double/double/toil/and/trouble?cuisine=french&party=4'); 339 340 stop(); 341 }); 342 343 module('SC.routes location observing', { 344 345 setup: function() { 346 router = SC.Object.create({ 347 hasBeenNotified: NO, 348 route: function(params) { 349 this.set('hasBeenNotified', YES); 350 } 351 }); 352 }, 353 354 teardown: function() { 355 SC.routes.set('location', null); 356 } 357 358 }); 359 360 test('Location change', function() { 361 var timer; 362 363 if (!SC.routes.get('usesHistory')) { 364 timer = setTimeout(function() { 365 ok(false, 'Route change was not notified within 2 seconds'); 366 window.start(); 367 }, 2000); 368 369 router.addObserver('hasBeenNotified', function() { 370 equals(router.get('hasBeenNotified'), YES, 'router should have been notified'); 371 clearTimeout(timer); 372 window.start(); 373 }); 374 375 SC.routes.add('foo', router, router.route); 376 window.location.hash = 'foo'; 377 378 stop(); 379 } 380 }); 381 382 module('_extractParametersAndRoute'); 383 384 test('_extractParametersAndRoute with ? syntax', function() { 385 same(SC.routes._extractParametersAndRoute({ route: 'videos/5?format=h264' }), 386 { route: 'videos/5', params:'?format=h264', format: 'h264' }, 387 'route parameters should be correctly extracted'); 388 389 same(SC.routes._extractParametersAndRoute({ route: 'videos/5?format=h264&size=small' }), 390 { route: 'videos/5', params:'?format=h264&size=small', format: 'h264', size: 'small' }, 391 'route parameters should be correctly extracted'); 392 393 same(SC.routes._extractParametersAndRoute({ route: 'videos/5?format=h264&size=small', format: 'ogg' }), 394 { route: 'videos/5', params:'?format=ogg&size=small', format: 'ogg', size: 'small' }, 395 'route parameters should be extracted and overwritten'); 396 397 same(SC.routes._extractParametersAndRoute({ route: 'videos/5', format: 'h264', size: 'small' }), 398 { route: 'videos/5', params:'?format=h264&size=small', format: 'h264', size: 'small' }, 399 'route should be well formatted with the given parameters'); 400 401 same(SC.routes._extractParametersAndRoute({ format: 'h264', size: 'small' }), 402 { route: '', params:'?format=h264&size=small', format: 'h264', size: 'small' }, 403 'route should be well formatted with the given parameters even if there is no initial route'); 404 405 same(SC.routes._extractParametersAndRoute({ route: 'videos/5?format=h264&size=small&url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DG0k3kHtyoqc%26feature%3Dg-logo%26context%3DG21d2678FOAAAAAAABAA' }), 406 { route: 'videos/5', params:'?format=h264&size=small&url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DG0k3kHtyoqc%26feature%3Dg-logo%26context%3DG21d2678FOAAAAAAABAA', format: 'h264', size: 'small', url: 'http://www.youtube.com/watch?v=G0k3kHtyoqc&feature=g-logo&context=G21d2678FOAAAAAAABAA' }, 407 'route paramters should be extracted and urldecoded'); 408 409 same(SC.routes._extractParametersAndRoute({ route: 'videos/5?format=h264&size=small&url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DG0k3kHtyoqc%26feature%3Dg-logo%26context%3DG21d2678FOAAAAAAABAA&videoLength=1120&macVersion=true' }, true), 410 { route: 'videos/5', params:'?format=h264&size=small&url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DG0k3kHtyoqc%26feature%3Dg-logo%26context%3DG21d2678FOAAAAAAABAA&videoLength=1120&macVersion=true', format: 'h264', size: 'small', url: 'http://www.youtube.com/watch?v=G0k3kHtyoqc&feature=g-logo&context=G21d2678FOAAAAAAABAA', videoLength: 1120, macVersion: true }, 411 'route paramters should be extracted, urldecoded and coerced'); 412 }); 413 414 test('_extractParametersAndRoute with & syntax', function() { 415 same(SC.routes._extractParametersAndRoute({ route: 'videos/5&format=h264' }), 416 { route: 'videos/5', params:'&format=h264', format: 'h264' }, 417 'route parameters should be correctly extracted'); 418 419 same(SC.routes._extractParametersAndRoute({ route: 'videos/5&format=h264&size=small' }), 420 { route: 'videos/5', params:'&format=h264&size=small', format: 'h264', size: 'small' }, 421 'route parameters should be correctly extracted'); 422 423 same(SC.routes._extractParametersAndRoute({ route: 'videos/5&format=h264&size=small', format: 'ogg' }), 424 { route: 'videos/5', params:'&format=ogg&size=small', format: 'ogg', size: 'small' }, 425 'route parameters should be extracted and overwritten'); 426 }); 427 428 module('deparam'); 429 430 test('deparam outputs object from string', function() { 431 same(SC.routes.deparam('http://test.fakeurl.com/dir/foo.html?query=test&numItems=5#home', true), 432 { query: 'test', numItems: 5 }, 433 'deparam with properly query string first url-like string, coerce true'); 434 same(SC.routes.deparam('http://test.fakeurl.com/dir/foo.html#home?query=test&numItems=5', true), 435 { query: 'test', numItems: 5 }, 436 'deparam with hash location first url-like string, coerce true'); 437 same(SC.routes.deparam('foo.html?query=test&numItems=5#home', true), 438 { query: 'test', numItems: 5 }, 439 'deparam with relative location url-like string, coerce true'); 440 same(SC.routes.deparam('query=test&numItems=5&size=small', true), 441 { query: 'test', numItems: 5, size: 'small' }, 442 'deparam works with params only string'); 443 }); 444 445 446 // For this module, we're going to replace the route's inner plumbing with a test 447 // version. Fragile. Apologies. 448 var _extractLocation = SC.routes._extractLocation; 449 module("Browser events", { 450 teardown: function() { 451 // Reset the innards. 452 SC.routes._extractLocation = _extractLocation; 453 } 454 }); 455 456 test("Internal flag is properly set for browser events.", function() { 457 var runCount = 0; 458 SC.routes._extractLocation = function() { 459 runCount++; 460 ok(this._exogenous, "Internal flag reflects that location is being updated by the browser."); 461 } 462 463 SC.routes.hashChange(); 464 if (runCount === 0) ok(false, "Internal plumbing method '_extractLocation' failed to fire as expected."); 465 466 runCount = 0; 467 SC.routes.popState(); 468 if (runCount === 0) ok(false, "Internal plumbing method '_extractLocation' failed to fire as expected."); 469 }); 470