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