// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== // ======================================================================== // SC.routes Base Tests // ======================================================================== /*globals module test ok isObj equals expects */

var router;

SC.routes.wantsHistory = YES;

module('SC.routes setup');

test('Setup', function() {

equals(SC.routes._didSetup, NO, 'SC.routes should not have been setup yet');

});

module('SC.routes setup', {

setup: function() {
  router = SC.Object.create({
    route: function() {
      return;
    }
  });
  SC.run(function() {
    SC.routes.add('foo', router, router.route);
  });
}

});

test('Setup', function() {

SC.run(function() {
  equals(SC.routes._didSetup, YES, 'SC.routes should have been setup');
});

});

test('Initial route', function() {

equals(SC.routes.get('location'), '', 'Initial route is an empty string');

});

module('SC.routes._Route', {

setup: function() {
  router = SC.Object.create({
    route: function() {
      return;
    }
  });
}

});

test('Route tree', function() {

var r = SC.routes._Route.create(),
    abc = ['a', 'b', 'c'],
    abd = ['a', 'b', 'd'],
    abe = ['a', 'b', ':e'],
    as = ['a', '*foo'],
    a, b, c, d, e, s, p;

r.add(abc, router, router.route);
r.add(abd, router, router.route);
r.add(abe, router, router.route);
r.add(as, router, router.route);

a = r.staticRoutes['a'];
ok(a, 'There should be a staticRoutes tree for a');
ok(!a.target, 'A node should not have a target');
ok(!a.method, 'A node should not have a method');

b = a.staticRoutes['b'];
ok(b, 'There should be a staticRoutes tree for b');
ok(!b.target, 'A node should not have a target');
ok(!b.method, 'A node should not have a method');

c = b.staticRoutes['c'];
ok(c, 'There should be a staticRoutes tree for c');
equals(c.target, router, 'A leaf should have a target');
equals(c.method, router.route, 'A leaf should have a method');

d = b.staticRoutes['d'];
ok(d, 'There should be a staticRoutes tree for d');
equals(d.target, router, 'A leaf should have a target');
equals(d.method, router.route, 'A leaf should have a method');

e = b.dynamicRoutes['e'];
ok(e, 'There should be a dynamicRoutes tree for e');
equals(d.target, router, 'A leaf should have a target');
equals(d.method, router.route, 'A leaf should have a method');

s = a.wildcardRoutes['foo'];
ok(s, 'There should be a wildcardRoutes tree for a');

equals(r.routeForParts(['a'], {}), null, 'routeForParts should return null for non existent routes');
equals(r.routeForParts(['a', 'b'], {}), null, 'routeForParts should return null for non existent routes');
equals(r.routeForParts(abc, {}), c, 'routeForParts should return the correct route for a/b/c');

equals(r.routeForParts(abd, {}), d, 'routeForParts should return the correct route for a/b/d');

abe[2] = 'foo';
p = {};
equals(r.routeForParts(abe, p), e, 'routeForParts should return the correct route for a/b/:e');
equals(p['e'], 'foo', 'routeForParts should return the params for a/b/:e');

p = {};
equals(r.routeForParts(['a', 'double', 'double', 'toil', 'and', 'trouble'], p), s, 'routeForParts should return the correct route for a/*foo');
equals(p.foo, 'double/double/toil/and/trouble', 'routeForParts should return the params for a/*foo');

});

module('SC.routes location', {

teardown: function() {
  SC.routes.set('location', null);
}

});

var routeWorks = function(route, name) {

SC.routes.set('location', route);
equals(SC.routes.get('location'), route, name + ' route has been set');

setTimeout(function() {
  equals(SC.routes.get('location'), route, name + ' route is still the same');
  start();
}, 300);

stop();

};

test('Null route', function() {

SC.routes.set('location', null);
equals(SC.routes.get('location'), '', 'Null route is the empty string');

});

test('Simple route', function() {

routeWorks('sixty-six', 'simple');

});

test('UTF-8 route', function() {

routeWorks('éàçù߀', 'UTF-8');

});

test('Already escaped route', function() {

routeWorks('%C3%A9%C3%A0%20%C3%A7%C3%B9%20%C3%9F%E2%82%AC', 'already escaped');

});

module('SC.routes informLocation', {

teardown: function() {
  SC.routes.set('informLocation', null);
}

});

test('informLocation updates location', function() {

SC.routes.set('informLocation', 'simple');
stop();

setTimeout(function() {
  equals(SC.routes.get('location'), 'simple');
  start();
}, 300);

});

test('informLocation and location invalidate each others caches', function() {

SC.routes.set('location', '');
stop();

setTimeout(function() {
  equals(SC.routes.get('location'), '');
  SC.routes.set('informLocation', 'simple');

  setTimeout(function() {
    equals(SC.routes.get('location'), 'simple');
    SC.routes.set('location', '');

    setTimeout(function() {
      equals(SC.routes.get('location'), '');
      SC.routes.set('informLocation', 'simple');

      setTimeout(function() {
        equals(SC.routes.get('location'), 'simple');
        start();
      }, 300);
    }, 300);
  }, 300);
}, 300);

});

module('SC.routes defined routes', {

setup: function() {
  router = SC.Object.create({
    params: null,
    triggered: NO,
    route: function(params) {
      this.set('params', params);
    },
    triggerRoute: function() {
      this.triggered = YES;
    }
  });
},

teardown: function() {
  SC.routes.set('location', null);
}

});

test('setting location triggers function when only passed function', function() {

var barred = false;

SC.routes.add('bar', function(params) {
  barred = true;
});
SC.routes.set('location', 'bar');

ok(barred, 'Function was called');

});

test('setting location simply triggers route', function() {

SC.routes.add("foo", router, "triggerRoute");
SC.routes.set('location', 'bar');
ok(!router.triggered, "Router not triggered with nonexistent route.");

SC.routes.set('location', 'foo');
ok(router.triggered, "Router triggered.");

});

test('calling trigger() triggers current location (again)', function() {

SC.routes.add("foo", router, "triggerRoute");
SC.routes.set('location', 'foo');
ok(router.triggered, "Router triggered first time.");
router.triggered = NO;

SC.routes.trigger();
ok(router.triggered, "Router triggered (again).");

});

test('A mix of static, dynamic and wildcard route', function() {

var didObserve = false,
    timer;

timer = setTimeout(function() {
  ok(false, 'Route change was not notified within 2 seconds');
  window.start();
}, 2000);

router.addObserver('params', function() {
  if (!didObserve) {
    didObserve = true;
    same(router.get('params'), { controller: 'users', action: 'éàçù߀', id: '5', witches: 'double/double/toil/and/trouble' });
    clearTimeout(timer);
    window.start();
  }
});

SC.routes.add('foo/:controller/:action/bar/:id/*witches', router, router.route);
SC.routes.set('location', 'foo/users/éàçù߀/bar/5/double/double/toil/and/trouble');

stop();

});

test('Route with parameters defined in a string', function() {

var didObserve = false,
    timer;

timer = setTimeout(function() {
  ok(false, 'Route change was not notified within 2 seconds');
  window.start();
}, 2000);

router.addObserver('params', function() {
  if (!didObserve) {
    didObserve = true;
    same(router.get('params'), { cuisine: 'french', party: '4', url: '' });
    clearTimeout(timer);
    window.start();
  }
});

SC.routes.add('*url', router, router.route);
SC.routes.set('location', '?cuisine=french&party=4');

stop();

});

test('Route with parameters defined in a hash', function() {

var didObserve = false,
    timer;

timer = setTimeout(function() {
  ok(false, 'Route change was not notified within 2 seconds');
  window.start();
}, 2000);

router.addObserver('params', function() {
  if (!didObserve) {
    didObserve = true;
    same(router.get('params'), { cuisine: 'french', party: '4', url: '' });
    clearTimeout(timer);
    window.start();
  }
});

SC.routes.add('*url', router, router.route);
SC.routes.set('location', { cuisine: 'french', party: '4' });

stop();

});

test('A mix of everything', function() {

var didObserve = false,
    timer;

timer = setTimeout(function() {
  ok(false, 'Route change was not notified within 2 seconds');
  window.start();
}, 2000);

router.addObserver('params', function() {
  if (!didObserve) {
    didObserve = true;
    same(router.get('params'), { controller: 'users', action: 'éàçù߀', id: '5', witches: 'double/double/toil/and/trouble', cuisine: 'french', party: '4' });
    clearTimeout(timer);
    window.start();
  }
});

SC.routes.add('foo/:controller/:action/bar/:id/*witches', router, router.route);
SC.routes.set('location', 'foo/users/éàçù߀/bar/5/double/double/toil/and/trouble?cuisine=french&party=4');

stop();

});

module('SC.routes location observing', {

setup: function() {
  router = SC.Object.create({
    hasBeenNotified: NO,
    route: function(params) {
      this.set('hasBeenNotified', YES);
    }
  });
},

teardown: function() {
  SC.routes.set('location', null);
}

});

test('Location change', function() {

var timer;

if (!SC.routes.get('usesHistory')) {
  timer = setTimeout(function() {
    ok(false, 'Route change was not notified within 2 seconds');
    window.start();
  }, 2000);

  router.addObserver('hasBeenNotified', function() {
    equals(router.get('hasBeenNotified'), YES, 'router should have been notified');
    clearTimeout(timer);
    window.start();
  });

  SC.routes.add('foo', router, router.route);
  window.location.hash = 'foo';

  stop();
}

});

module('_extractParametersAndRoute');

test('_extractParametersAndRoute with ? syntax', function() {

same(SC.routes._extractParametersAndRoute({ route: 'videos/5?format=h264' }),
     { route: 'videos/5', params:'?format=h264', format: 'h264' },
     'route parameters should be correctly extracted');

same(SC.routes._extractParametersAndRoute({ route: 'videos/5?format=h264&size=small' }),
     { route: 'videos/5', params:'?format=h264&size=small', format: 'h264', size: 'small' },
     'route parameters should be correctly extracted');

same(SC.routes._extractParametersAndRoute({ route: 'videos/5?format=h264&size=small', format: 'ogg' }),
     { route: 'videos/5', params:'?format=ogg&size=small', format: 'ogg', size: 'small' },
     'route parameters should be extracted and overwritten');

same(SC.routes._extractParametersAndRoute({ route: 'videos/5', format: 'h264', size: 'small' }),
     { route: 'videos/5', params:'?format=h264&size=small', format: 'h264', size: 'small' },
     'route should be well formatted with the given parameters');

same(SC.routes._extractParametersAndRoute({ format: 'h264', size: 'small' }),
     { route: '', params:'?format=h264&size=small', format: 'h264', size: 'small' },
     'route should be well formatted with the given parameters even if there is no initial route');

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' }),
     { 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' },
     'route paramters should be extracted and urldecoded');

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),
     { 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 },
     'route paramters should be extracted, urldecoded and coerced');

});

test('_extractParametersAndRoute with & syntax', function() {

same(SC.routes._extractParametersAndRoute({ route: 'videos/5&format=h264' }),
     { route: 'videos/5', params:'&format=h264', format: 'h264' },
     'route parameters should be correctly extracted');

same(SC.routes._extractParametersAndRoute({ route: 'videos/5&format=h264&size=small' }),
     { route: 'videos/5', params:'&format=h264&size=small', format: 'h264', size: 'small' },
     'route parameters should be correctly extracted');

same(SC.routes._extractParametersAndRoute({ route: 'videos/5&format=h264&size=small', format: 'ogg' }),
     { route: 'videos/5', params:'&format=ogg&size=small', format: 'ogg', size: 'small' },
     'route parameters should be extracted and overwritten');

});

module('deparam');

test('deparam outputs object from string', function() {

same(SC.routes.deparam('http://test.fakeurl.com/dir/foo.html?query=test&numItems=5#home', true),
  { query: 'test', numItems: 5 },
  'deparam with properly query string first url-like string, coerce true');
same(SC.routes.deparam('http://test.fakeurl.com/dir/foo.html#home?query=test&numItems=5', true),
  { query: 'test', numItems: 5 },
  'deparam with hash location first url-like string, coerce true');
same(SC.routes.deparam('foo.html?query=test&numItems=5#home', true),
  { query: 'test', numItems: 5 },
  'deparam with relative location url-like string, coerce true');
same(SC.routes.deparam('query=test&numItems=5&size=small', true),
  { query: 'test', numItems: 5, size: 'small' },
  'deparam works with params only string');

});

// For this module, we're going to replace the route's inner plumbing with a test // version. Fragile. Apologies. var _extractLocation = SC.routes._extractLocation; module(“Browser events”, {

teardown: function() {
  // Reset the innards.
  SC.routes._extractLocation = _extractLocation;
}

});

test(“Internal flag is properly set for browser events.”, function() {

var runCount = 0;
SC.routes._extractLocation = function() {
  runCount++;
  ok(this._exogenous, "Internal flag reflects that location is being updated by the browser.");
}

SC.routes.hashChange();
if (runCount === 0) ok(false, "Internal plumbing method '_extractLocation' failed to fire as expected.");

runCount = 0;
SC.routes.popState();
if (runCount === 0) ok(false, "Internal plumbing method '_extractLocation' failed to fire as expected.");

});