can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
971 lines (779 loc) • 20.1 kB
JavaScript
/* jshint asi:true*/
steal("can/route", "can/test", "steal-qunit", "can/map/define", function () {
QUnit.module("can/route", {
setup: function () {
can.route._teardown();
can.route.defaultBinding = "hashchange";
}
})
if (!("onhashchange" in window)) {
return;
}
test("deparam", function () {
can.route.routes = {};
can.route(":page", {
page: "index"
});
var obj = can.route.deparam("can.Control");
deepEqual(obj, {
page: "can.Control",
route: ":page"
});
obj = can.route.deparam("");
deepEqual(obj, {
page: "index",
route: ":page"
});
obj = can.route.deparam("can.Control&where=there");
deepEqual(obj, {
page: "can.Control",
where: "there",
route: ":page"
});
can.route.routes = {};
can.route(":page/:index", {
page: "index",
index: "foo"
});
obj = can.route.deparam("can.Control/&where=there");
deepEqual(obj, {
page: "can.Control",
index: "foo",
where: "there",
route: ":page/:index"
}, "default value and queryparams");
});
test("deparam of invalid url", function () {
var obj;
can.route.routes = {};
can.route("pages/:var1/:var2/:var3", {
var1: 'default1',
var2: 'default2',
var3: 'default3'
});
// This path does not match the above route, and since the hash is not
// a &key=value list there should not be data.
obj = can.route.deparam("pages//");
deepEqual(obj, {});
// A valid path with invalid parameters should return the path data but
// ignore the parameters.
obj = can.route.deparam("pages/val1/val2/val3&invalid-parameters");
deepEqual(obj, {
var1: 'val1',
var2: 'val2',
var3: 'val3',
route: "pages/:var1/:var2/:var3"
});
});
test("deparam of url with non-generated hash (manual override)", function () {
var obj;
can.route.routes = {};
// This won't be set like this by route, but it could easily happen via a
// user manually changing the URL or when porting a prior URL structure.
obj = can.route.deparam("page=foo&bar=baz&where=there");
deepEqual(obj, {
page: 'foo',
bar: 'baz',
where: 'there'
});
});
test("param", function () {
can.route.routes = {};
can.route("pages/:page", {
page: "index"
})
var res = can.route.param({
page: "foo"
});
equal(res, "pages/foo")
res = can.route.param({
page: "foo",
index: "bar"
});
equal(res, "pages/foo&index=bar")
can.route("pages/:page/:foo", {
page: "index",
foo: "bar"
})
res = can.route.param({
page: "foo",
foo: "bar",
where: "there"
});
equal(res, "pages/foo/&where=there")
// There is no matching route so the hash should be empty.
res = can.route.param({});
equal(res, "")
can.route.routes = {};
res = can.route.param({
page: "foo",
bar: "baz",
where: "there"
});
equal(res, "&page=foo&bar=baz&where=there")
res = can.route.param({});
equal(res, "")
});
test("symmetry", function () {
can.route.routes = {};
var obj = {
page: "=&[]",
nestedArray: ["a"],
nested: {
a: "b"
}
}
var res = can.route.param(obj)
var o2 = can.route.deparam(res)
deepEqual(o2, obj)
});
test("light param", function () {
can.route.routes = {};
can.route(":page", {
page: "index"
})
var res = can.route.param({
page: "index"
});
equal(res, "")
can.route("pages/:p1/:p2/:p3", {
p1: "index",
p2: "foo",
p3: "bar"
})
res = can.route.param({
p1: "index",
p2: "foo",
p3: "bar"
});
equal(res, "pages///")
res = can.route.param({
p1: "index",
p2: "baz",
p3: "bar"
});
equal(res, "pages//baz/")
});
test('param doesnt add defaults to params', function () {
can.route.routes = {};
can.route("pages/:p1", {
p2: "foo"
})
var res = can.route.param({
p1: "index",
p2: "foo"
});
equal(res, "pages/index")
})
test("param-deparam", function () {
can.route(":page/:type", {
page: "index",
type: "foo"
})
var data = {
page: "can.Control",
type: "document",
bar: "baz",
where: "there"
};
var res = can.route.param(data);
var obj = can.route.deparam(res);
delete obj.route
deepEqual(obj, data)
data = {
page: "can.Control",
type: "foo",
bar: "baz",
where: "there"
};
res = can.route.param(data);
obj = can.route.deparam(res);
delete obj.route;
deepEqual(data, obj)
data = {
page: " a ",
type: " / "
};
res = can.route.param(data);
obj = can.route.deparam(res);
delete obj.route;
deepEqual(obj, data, "slashes and spaces")
data = {
page: "index",
type: "foo",
bar: "baz",
where: "there"
};
res = can.route.param(data);
obj = can.route.deparam(res);
delete obj.route;
deepEqual(data, obj)
can.route.routes = {};
data = {
page: "foo",
bar: "baz",
where: "there"
};
res = can.route.param(data);
obj = can.route.deparam(res);
deepEqual(data, obj)
})
test("deparam-param", function () {
can.route.routes = {};
can.route(":foo/:bar", {
foo: 1,
bar: 2
});
var res = can.route.param({
foo: 1,
bar: 2
});
equal(res, "/", "empty slash")
var deparamed = can.route.deparam("/")
deepEqual(deparamed, {
foo: 1,
bar: 2,
route: ":foo/:bar"
})
})
test("precident", function () {
can.route.routes = {};
can.route(":who", {
who: "index"
});
can.route("search/:search");
var obj = can.route.deparam("can.Control");
deepEqual(obj, {
who: "can.Control",
route: ":who"
});
obj = can.route.deparam("search/can.Control");
deepEqual(obj, {
search: "can.Control",
route: "search/:search"
}, "bad deparam");
equal(can.route.param({
search: "can.Control"
}),
"search/can.Control", "bad param");
equal(can.route.param({
who: "can.Control"
}),
"can.Control");
});
test("better matching precident", function () {
can.route.routes = {};
can.route(":type", {
who: "index"
});
can.route(":type/:id");
equal(can.route.param({
type: "foo",
id: "bar"
}),
"foo/bar");
})
test("linkTo", function () {
can.route.routes = {};
can.route(":foo");
var res = can.route.link("Hello", {
foo: "bar",
baz: 'foo'
});
equal(res, '<a href="#!bar&baz=foo">Hello</a>');
});
test("param with route defined", function () {
can.route.routes = {};
can.route("holler")
can.route("foo");
var res = can.route.param({
foo: "abc",
route: "foo"
});
equal(res, "foo&foo=abc")
});
test("route endings", function () {
can.route.routes = {};
can.route("foo", {
foo: true
});
can.route("food", {
food: true
});
var res = can.route.deparam("food")
ok(res.food, "we get food back")
});
test("strange characters", function () {
can.route.routes = {};
can.route(":type/:id");
var res = can.route.deparam("foo/" + encodeURIComponent("\/"))
equal(res.id, "\/")
res = can.route.param({
type: "bar",
id: "\/"
});
equal(res, "bar/" + encodeURIComponent("\/"))
});
test("empty default is matched even if last", function () {
can.route.routes = {};
can.route(":who");
can.route("", {
foo: "bar"
})
var obj = can.route.deparam("");
deepEqual(obj, {
foo: "bar",
route: ""
});
});
test("order matched", function () {
can.route.routes = {};
can.route(":foo");
can.route(":bar")
var obj = can.route.deparam("abc");
deepEqual(obj, {
foo: "abc",
route: ":foo"
});
});
test("param order matching", function () {
can.route.routes = {};
can.route("", {
bar: "foo"
});
can.route("something/:bar");
var res = can.route.param({
bar: "foo"
});
equal(res, "", "picks the shortest, best match");
// picks the first that matches everything ...
can.route.routes = {};
can.route(":recipe", {
recipe: "recipe1",
task: "task3"
});
can.route(":recipe/:task", {
recipe: "recipe1",
task: "task3"
});
res = can.route.param({
recipe: "recipe1",
task: "task3"
});
equal(res, "", "picks the first match of everything");
res = can.route.param({
recipe: "recipe1",
task: "task2"
});
equal(res, "/task2")
});
test("dashes in routes", function () {
can.route.routes = {};
can.route(":foo-:bar");
var obj = can.route.deparam("abc-def");
deepEqual(obj, {
foo: "abc",
bar: "def",
route: ":foo-:bar"
});
window.location.hash = "qunit-fixture";
window.location.hash = "";
});
var teardownRouteTest;
var setupRouteTest = function(callback){
var testarea = document.getElementById('qunit-fixture');
var iframe = document.createElement('iframe');
stop();
window.routeTestReady = function(){
var args = can.makeArray(arguments)
args.unshift(iframe);
callback.apply(null, args);
};
iframe.src = can.test.path("route/testing.html?"+Math.random());
testarea.appendChild(iframe);
teardownRouteTest = function(){
setTimeout(function(){
can.remove(can.$(iframe));
setTimeout(function(){
start();
},10);
},1);
};
};
if (typeof steal !== 'undefined') {
test("listening to hashchange (#216, #124)", function () {
setupRouteTest(function (iframe, iCanRoute) {
ok(!iCanRoute.attr('bla'), 'Value not set yet');
iCanRoute.bind('change', function () {
equal(iCanRoute.attr('bla'), 'blu', 'Got route change event and value is as expected');
teardownRouteTest();
});
iCanRoute.ready();
setTimeout(function () {
iframe.src = iframe.src + '#!bla=blu';
}, 10);
});
});
test("initial route fires twice", function () {
stop();
expect(1);
window.routeTestReady = function (iCanRoute, loc) {
iCanRoute("", {});
iCanRoute.bind('change', function(){
ok(true, 'change triggered once')
start();
});
iCanRoute.ready();
}
var iframe = document.createElement('iframe');
iframe.src = can.test.path("route/testing.html?5");
can.$("#qunit-fixture")[0].appendChild(iframe);
});
test("removing things from the hash", function () {
setupRouteTest(function (iframe, iCanRoute, loc) {
iCanRoute.bind('change', function () {
equal(iCanRoute.attr('foo'), 'bar', 'expected value');
iCanRoute.unbind('change');
iCanRoute.bind('change', function(){
equal(iCanRoute.attr('personId'), '3', 'personId');
equal(iCanRoute.attr('foo'), undefined, 'unexpected value');
iCanRoute.unbind('change');
teardownRouteTest();
});
setTimeout(function () {
iframe.contentWindow.location.hash = '#!personId=3';
}, 100);
});
iCanRoute.ready();
setTimeout(function () {
iframe.contentWindow.location.hash = '#!foo=bar';
}, 100);
});
});
test("can.route.map: conflicting route values, hash should win", function(){
setupRouteTest(function (iframe, iCanRoute, loc) {
iCanRoute(":type/:id");
var AppState = can.Map.extend();
var appState = new AppState({type: "dog", id: '4'});
iCanRoute.map(appState);
loc.hash = "#!cat/5";
iCanRoute.ready();
setTimeout(function () {
var after = loc.href.substr(loc.href.indexOf("#"));
equal(after, "#!cat/5", "same URL");
equal(appState.attr("type"), "cat", "conflicts should be won by the URL");
equal(appState.attr("id"), "5", "conflicts should be won by the URL");
teardownRouteTest();
}, 30);
});
});
test("can.route.map: route is initialized from URL first, then URL params are added from can.route.data", function(){
setupRouteTest(function (iframe, iCanRoute, loc) {
iCanRoute(":type/:id");
var AppState = can.Map.extend();
var appState = new AppState({section: 'home'});
iCanRoute.map(appState);
loc.hash = "#!cat/5";
iCanRoute.ready();
setTimeout(function () {
var after = loc.href.substr(loc.href.indexOf("#"));
equal(after, "#!cat/5§ion=home", "same URL");
equal(appState.attr("type"), "cat", "hash populates the appState");
equal(appState.attr("id"), "5", "hash populates the appState");
equal(appState.attr("section"), "home", "appState keeps its properties");
ok(iCanRoute.data === appState, "can.route.data is the same as appState");
teardownRouteTest();
}, 30);
});
});
test("updating the hash", function () {
setupRouteTest(function (iframe, iCanRoute, loc) {
iCanRoute.ready();
iCanRoute(":type/:id");
iCanRoute.attr({
type: "bar",
id: "\/"
});
setTimeout(function () {
var after = loc.href.substr(loc.href.indexOf("#"));
equal(after, "#!bar/" + encodeURIComponent("\/"));
teardownRouteTest();
}, 30);
});
});
test("sticky enough routes", function () {
setupRouteTest(function (iframe, iCanRoute, loc) {
iCanRoute.ready()
iCanRoute("active");
iCanRoute("");
loc.hash = "#!active";
setTimeout(function () {
var after = loc.href.substr(loc.href.indexOf("#"));
equal(after, "#!active");
teardownRouteTest();
}, 30);
});
});
test("unsticky routes", function () {
setupRouteTest(function (iframe, iCanRoute, loc) {
iCanRoute.ready();
iCanRoute(":type");
iCanRoute(":type/:id");
iCanRoute.attr({
type: "bar"
});
setTimeout(function () {
var after = loc.href.substr(loc.href.indexOf("#"));
equal(after, "#!bar");
iCanRoute.attr({
type: "bar",
id: "\/"
});
// check for 1 second
var time = new Date()
setTimeout(function innerTimer() {
var after = loc.href.substr(loc.href.indexOf("#"));
if (after === "#!bar/" + encodeURIComponent("\/")) {
equal(after, "#!bar/" + encodeURIComponent("\/"), "should go to type/id");
teardownRouteTest();
} else if (new Date() - time > 2000) {
ok(false, "hash is " + after);
can.remove(can.$(iframe))
} else {
setTimeout(innerTimer, 30)
}
}, 100);
}, 100);
});
});
test("can.route.current is live-bindable (#1156)", function () {
setupRouteTest(function (iframe, iCanRoute, loc, win) {
iCanRoute.ready();
var isOnTestPage = win.can.compute(function(){
return iCanRoute.current({page: "test"});
});
isOnTestPage.bind("change", function(ev,newVal){
teardownRouteTest();
});
equal(isOnTestPage(), false, "initially not on test page")
setTimeout(function(){
iCanRoute.attr("page","test");
},20);
});
});
test("can.compute.read should not call can.route (#1154)", function () {
setupRouteTest(function (iframe, iCanRoute, loc, win) {
iCanRoute.attr("page","test");
iCanRoute.ready();
var val = win.can.compute.read({route: iCanRoute},win.can.compute.read.reads("route")).value;
setTimeout(function(){
equal(val,iCanRoute,"read correctly");
teardownRouteTest();
},1);
});
});
test("routes should deep clean", function() {
expect(2);
setupRouteTest(function (iframe, iCanRoute, loc) {
iCanRoute.ready();
var hash1 = can.route.url({
panelA: {
name: "fruit",
id: 15,
show: true
}
});
var hash2 = can.route.url({
panelA: {
name: "fruit",
id: 20,
read: false
}
});
loc.hash = hash1;
loc.hash = hash2;
setTimeout(function() {
equal(iCanRoute.attr("panelA.id"), 20, "id should change");
equal(iCanRoute.attr("panelA.show"), undefined, "show should be removed");
teardownRouteTest();
}, 30);
});
});
test("updating bound can.Map causes single update with a coerced string value", function() {
expect(1);
setupRouteTest(function (iframe, route) {
var appVM = new can.Map();
route.map(appVM);
route.ready();
appVM.bind('action', function(ev, newVal) {
strictEqual(newVal, '10');
});
appVM.attr('action', 10);
// check after 30ms to see that we only have a single call
setTimeout(function() {
teardownRouteTest();
}, 5);
});
});
test("updating unserialized prop on bound can.Map causes single update without a coerced string value", function() {
expect(1);
setupRouteTest(function (iframe, route) {
var appVM = new (can.Map.extend({define: {
action: {serialize: false}
}}))();
route.map(appVM);
route.ready();
appVM.bind('action', function(ev, newVal) {
equal(typeof newVal, 'function');
});
appVM.attr('action', function() {});
// check after 30ms to see that we only have a single call
setTimeout(function() {
teardownRouteTest();
}, 5);
});
});
test("hash doesn't update to itself with a !", function() {
stop();
window.routeTestReady = function (iCanRoute, loc) {
iCanRoute.ready();
iCanRoute(":path");
iCanRoute.attr('path', 'foo');
setTimeout(function() {
var counter = 0;
try {
equal(loc.hash, '#!foo');
} catch(e) {
start();
throw e;
}
iCanRoute.bind("change", function() {
counter++;
});
loc.hash = "bar";
setTimeout(function() {
try {
equal(loc.hash, '#bar');
equal(counter, 1); //sanity check -- bindings only ran once before this change.
} finally {
start();
}
}, 100);
}, 100);
};
var iframe = document.createElement('iframe');
iframe.src = can.test.path("route/testing.html?1");
can.$("#qunit-fixture")[0].appendChild(iframe);
});
}
test("escaping periods", function () {
can.route.routes = {};
can.route(":page\\.html", {
page: "index"
});
var obj = can.route.deparam("can.Control.html");
deepEqual(obj, {
page: "can.Control",
route: ":page\\.html"
});
equal(can.route.param({
page: "can.Control"
}), "can.Control.html");
});
if (typeof require === 'undefined') {
test("correct stringing", function () {
setupRouteTest(function(iframe, route) {
route.routes = {};
route.attr('number', 1);
propEqual(route.attr(), {
'number': "1"
});
route.attr({
bool: true
}, true);
propEqual(route.attr(), {
'bool': "true"
});
route.attr({
string: "hello"
}, true);
propEqual(route.attr(), {
'string': "hello"
});
// test that toString is run on arbitrary objects if an impl is provided
var fakeDate = {
toString: function() {
return "fake time o clock"
}
};
route.attr({
obj: fakeDate
}, true);
propEqual(route.attr(), {
'obj': "fake time o clock"
});
route.attr({
array: [1, true, "hello"]
}, true);
propEqual(route.attr(), {
'array': ["1", "true", "hello"]
});
route.attr({
number: 1,
bool: true,
string: "hello",
array: [2, false, "world"],
obj: {
number: 3,
array: [4, true]
}
}, true);
propEqual(route.attr(), {
number: "1",
bool: "true",
string: "hello",
array: ["2", "false", "world"],
obj: {
number: "3",
array: ["4", "true"]
}
});
route.routes = {};
route(":type/:id");
route.attr({
type: 'page',
id: 10,
sort_by_name: true
}, true);
propEqual(route.attr(), {
type: "page",
id: "10",
sort_by_name: "true"
});
teardownRouteTest();
});
});
}
test("on/off binding", function () {
can.route.routes = {};
expect(1)
can.route.on('foo', function () {
ok(true, "foo called");
can.route.off('foo');
can.route.attr('foo', 'baz');
});
can.route.attr('foo', 'bar');
});
test("two way binding can.route.map with can.Map instance", function(){
expect(1);
var AppState = can.Map.extend();
var appState = new AppState();
can.route.map(appState);
can.route.on('change', function(){
equal(can.route.attr('name'), 'Brian', 'appState is bound to can.route');
can.route.off('change');
appState.removeAttr('name');
});
appState.attr('name', 'Brian');
});
});