can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
967 lines (776 loc) • 21.7 kB
JavaScript
/* jshint asi:true,scripturl:true */
steal('can/route/pushstate', "can/test", "steal-qunit", function () {
function eventFire(el, etype) {
var doc = el.ownerDocument,
win = doc.defaultView || doc.parentWindow;
win.can.trigger(el, etype, [], true);
/*if (el.fireEvent) {
(el.fireEvent('on' + etype));
} else {
var evObj = el.ownerDocument.createEvent('MouseEvents');
evObj.initEvent("click", true, true, el.ownerDocument.defaultView,
0, 0, 0, 0, 0, false, false, false, false, 0, null);
el.dispatchEvent(evObj);
}*/
}
if (window.history && history.pushState) {
QUnit.module("can/route/pushstate", {
setup: function () {
can.route._teardown();
can.route.defaultBinding = "pushstate";
},
teardown: function () {
}
});
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"
});
});
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"
};
// adding the / should not be necessary. can.route.deparam removes / if the root starts with /
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")
// you really should deparam with root ..
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("\/"))
});
// Start steal-only
if (typeof steal !== 'undefined') {
var makeTestingIframe = function (callback) {
window.routeTestReady = function (iCanRoute, loc, history, win) {
callback({
route: iCanRoute,
location: loc,
history: history,
window: win,
iframe: iframe
}, function () {
iframe.onload = null;
can.remove(can.$(iframe));
delete window.routeTestReady;
});
};
var iframe = document.createElement('iframe');
iframe.src = can.test.path("route/pushstate/testing.html")+"?" + Math.random();
can.$("#qunit-fixture")[0].appendChild(iframe);
};
test("updating the url", function () {
stop();
makeTestingIframe(function (info, done) {
info.route.ready()
info.route("/:type/:id");
info.route.attr({
type: "bar",
id: "5"
});
setTimeout(function () {
var after = info.location.pathname;
equal(after, "/bar/5", "path is " + after);
start();
done();
}, 100);
});
});
test("sticky enough routes", function () {
stop();
makeTestingIframe(function (info, done) {
info.route("/active");
info.route("");
info.history.pushState(null, null, "/active");
setTimeout(function () {
var after = info.location.pathname;
equal(after, "/active");
start();
done();
}, 30);
});
});
test("unsticky routes", function () {
stop();
window.routeTestReady = function (iCanRoute, loc, iframeHistory) {
// check if we can even test this
iframeHistory.pushState(null, null, "/bar/" + encodeURIComponent("\/"));
setTimeout(function timer() {
if ("/bar/" + encodeURIComponent("\/") === loc.pathname) {
runTest();
} else if (loc.pathname.indexOf("/bar/") >= 0) {
// encoding doesn't actually work
ok(true, "can't test!");
can.remove(can.$(iframe))
start()
} else {
setTimeout(timer, 30)
}
}, 30);
var runTest = function () {
iCanRoute.ready();
iCanRoute("/:type");
iCanRoute("/:type/:id");
iCanRoute.attr({
type: "bar"
});
setTimeout(function () {
var after = loc.pathname;
equal(after, "/bar", "only type is set");
iCanRoute.attr({
type: "bar",
id: "\/"
});
// check for 1 second
var time = new Date()
setTimeout(function innerTimer() {
var after = loc.pathname;
if (after === "/bar/" + encodeURIComponent("\/")) {
equal(after, "/bar/" + encodeURIComponent("\/"), "should go to type/id");
can.remove(can.$(iframe))
start();
} else if (new Date() - time > 2000) {
ok(false, "hash is " + after);
can.remove(can.$(iframe))
} else {
setTimeout(innerTimer, 30)
}
}, 30);
}, 30);
};
};
var iframe = document.createElement('iframe');
iframe.src = can.test.path("route/pushstate/testing.html?1");
can.$("#qunit-fixture")[0].appendChild(iframe);
});
test("clicked hashes work (#259)", function () {
stop();
window.routeTestReady = function (iCanRoute, loc, hist, win) {
iCanRoute(win.location.pathname, {
page: "index"
});
iCanRoute(":type/:id");
iCanRoute.ready();
window.win = win;
var link = win.document.createElement("a");
link.href = "/articles/17#references";
link.innerHTML = "Click Me"
win.document.body.appendChild(link);
win.can.trigger(win.can.$(link), "click")
//link.click()
setTimeout(function () {
deepEqual(can.extend({}, iCanRoute.attr()), {
type: "articles",
id: "17",
route: ":type/:id"
}, "articles are right")
equal(win.location.hash, "#references", "includes hash");
start();
can.remove(can.$(iframe))
}, 100);
};
var iframe = document.createElement('iframe');
iframe.src = can.test.path("route/pushstate/testing.html");
can.$("#qunit-fixture")[0].appendChild(iframe);
});
test("javascript:// links do not get pushstated", function(){
stop();
makeTestingIframe(function (info, done) {
info.route(":type", { type: "yay" });
info.route.ready();
var window = info.window;
var link = window.document.createElement("a");
link.href = "javascript://";
link.innerHTML = "Click Me";
window.document.body.appendChild(link);
try {
window.can.trigger(window.can.$(link), "click");
ok(true, "Clicking javascript:// anchor did not cause a security exception");
} catch(err) {
ok(false, "Clicking javascript:// anchor caused a security exception");
}
start();
done();
});
});
if(window.parent === window) {
// we can't call back if running in multiple frames
test("no doubled history states (#656)", function () {
stop();
window.routeTestReady = function (iCanRoute, loc, hist, win) {
var root = loc.pathname.substr(0, loc.pathname.lastIndexOf("/") + 1);
var stateTest = -1,
message;
function nextStateTest() {
stateTest++;
win.can.route.attr("page", "start");
setTimeout(function () {
if (stateTest === 0) {
message = "can.route.attr";
win.can.route.attr("page", "test");
} else if (stateTest === 1) {
message = "history.pushState";
win.history.pushState(null, null, root + "test/");
} else if (stateTest === 2) {
message = "link click";
var link = win.document.createElement("a");
link.href = root + "test/";
link.innerText = "asdf";
win.document.body.appendChild(link);
win.can.trigger(win.can.$(link), "click");
} else {
start();
can.remove(can.$(iframe));
return;
}
setTimeout(function () {
win.history.back();
setTimeout(function () {
var path = win.location.pathname;
// strip root for deparam
if (path.indexOf(root) === 0) {
path = path.substr(root.length);
}
equal(win.can.route.deparam(path)
.page, "start", message + " passed");
nextStateTest();
}, 200);
}, 200);
}, 200);
}
win.can.route.bindings.pushstate.root = root;
win.can.route(":page/");
win.can.route.ready();
nextStateTest();
};
var iframe = document.createElement("iframe");
iframe.src = can.test.path("route/pushstate/testing.html");
can.$("#qunit-fixture")[0].appendChild(iframe);
});
test("root can include the domain", function () {
// Allows bindings.pushstate.root to handle the full domain instead of just the pathname
stop();
makeTestingIframe(function(info, done){
info.route.bindings.pushstate.root = can.test.path("route/pushstate/testing.html", true).replace("route/pushstate/testing.html", "");
info.route(":module/:plugin/:page\\.html");
info.route.ready();
setTimeout(function(){
equal(info.route.attr('module'), 'route', 'works');
start();
done();
}, 100);
});
});
test("URL's don't greedily match", function () {
stop();
makeTestingIframe(function(info, done){
info.route.bindings.pushstate.root = can.test.path("route/pushstate/testing.html", true).replace("route/pushstate/testing.html", "");
info.route(":module\\.html");
info.route.ready();
setTimeout(function(){
ok(!info.route.attr('module'), 'there is no route match');
start();
done();
}, 100);
});
});
}
test("routed links must descend from pushstate root (#652)", 1, function () {
stop();
var setupRoutesAndRoot = function (iCanRoute, root) {
iCanRoute(":section/");
iCanRoute(":section/:sub/");
iCanRoute.bindings.pushstate.root = root;
iCanRoute.ready();
};
var createLink = function (win, url) {
var link = win.document.createElement("a");
link.href = link.innerHTML = url;
win.document.body.appendChild(link);
return link;
};
// The following makes sure a link that is not "rooted" will
// behave normally and not call pushState
makeTestingIframe(function (info, done) {
setupRoutesAndRoot(info.route, "/app/");
var link = createLink(info.window, "/route/pushstate/empty.html"); // a link to somewhere outside app
var clickKiller = function(ev) {
if(ev.preventDefault) {
ev.preventDefault();
}
return false;
};
// kill the click b/c phantom doesn't like it.
can.bind.call(info.window.document,"click",clickKiller);
info.history.pushState = function () {
ok(false, "pushState should not have been called");
};
// click a link and make sure the iframe url changes
eventFire(link, "click")
done();
setTimeout(next, 10);
});
var next = function () {
makeTestingIframe(function (info, done) {
var timer;
info.route.bind("change", function () {
clearTimeout(timer);
timer = setTimeout(function () {
// deepEqual doesn't like to compare objects from different contexts
// so we copy it
var obj = can.simpleExtend({}, info.route.attr());
deepEqual(obj, {
section: "something",
sub: "test",
route: ":section/:sub/"
}, "route's data is correct");
done();
start();
}, 10);
});
setupRoutesAndRoot(info.route, "/app/");
var link = createLink(info.window, "/app/something/test/");
eventFire(link, "click")
// click a link and make sure the iframe url changes
});
};
});
test("replaceStateOn makes changes to an attribute use replaceSate (#1137)", function() {
stop();
makeTestingIframe(function(info, done){
info.history.pushState = function () {
ok(false, "pushState should not have been called");
};
info.history.replaceState = function () {
ok(true, "replaceState called");
};
info.route.replaceStateOn("ignoreme");
info.route.ready();
info.route.attr('ignoreme', 'yes');
setTimeout(function(){
start();
done();
}, 30);
});
});
test("replaceStateOn makes changes to multiple attributes use replaceState (#1137)", function() {
stop();
makeTestingIframe(function(info, done){
info.history.pushState = function () {
ok(false, "pushState should not have been called");
};
info.history.replaceState = function () {
ok(true, "replaceState called");
};
info.route.replaceStateOn("ignoreme", "metoo");
info.route.ready();
info.route.attr('ignoreme', 'yes');
setTimeout(function(){
info.route.attr('metoo', 'yes');
setTimeout(function(){
start();
done();
}, 30);
}, 30);
});
});
test("replaceStateOnce makes changes to an attribute use replaceState only once (#1137)", function() {
stop();
var replaceCalls = 0,
pushCalls = 0;
makeTestingIframe(function(info, done){
info.history.pushState = function () {
pushCalls++;
};
info.history.replaceState = function () {
replaceCalls++;
};
info.route.replaceStateOnce("ignoreme", "metoo");
info.route.ready();
info.route.attr('ignoreme', 'yes');
setTimeout(function(){
info.route.attr('ignoreme', 'no');
setTimeout(function(){
equal(replaceCalls, 1);
equal(pushCalls, 1);
start();
done();
}, 30);
}, 30);
});
});
test("replaceStateOff makes changes to an attribute use pushState again (#1137)", function(){
stop();
makeTestingIframe(function(info, done){
info.history.pushState = function () {
ok(true, "pushState called");
};
info.history.replaceState = function () {
ok(false, "replaceState should not be called called");
};
info.route.replaceStateOn("ignoreme");
info.route.replaceStateOff("ignoreme");
info.route.ready();
info.route.attr('ignoreme', 'yes');
setTimeout(function(){
start();
done();
}, 30);
});
});
} // end steal-only
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"
});
});
}
});