diff --git a/backbone.js b/backbone.js index bbc63fdab..3484f8484 100644 --- a/backbone.js +++ b/backbone.js @@ -1455,6 +1455,16 @@ // Set up all inheritable **Backbone.Router** properties and methods. _.extend(Router.prototype, Events, { + // If set to true, route handlers will be called with routeData object + // + // this.route('search/:query/p:num', 'search', function(params, routeData) { + // // for route 'search/backbone/p5?a=b' + // // params.query === 'backbone' + // // params.num === '5' + // // routeData.queryString === 'a=b' + // }); + // + useParamsObject: false, // Initialize is an empty function by default. Override it with your own // initialization logic. @@ -1467,12 +1477,26 @@ // }); // route: function(route, name, callback) { - if (!_.isRegExp(route)) route = this._routeToRegExp(route); + // Pass paramNames into _routeToRegExp and let that function populate it + var paramNames = []; + // Need to know this so we don't try to create params object when the route is matched + var isRegExpRoute = _.isRegExp(route); + + if (!isRegExpRoute) route = this._routeToRegExp(route, paramNames); + if (_.isFunction(name)) { callback = name; name = ''; } if (!callback) callback = this[name]; + + this._routeInfos = this._routeInfos || {}; + this._routeInfos[route] = { + name: name, + isRegExpRoute: isRegExpRoute, + paramNames: paramNames + }; + var router = this; Backbone.history.route(route, function(fragment) { var args = router._extractParameters(route, fragment); @@ -1511,13 +1535,18 @@ // Convert a route string into a regular expression, suitable for matching // against the current location hash. - _routeToRegExp: function(route) { + _routeToRegExp: function(route, paramNames) { route = route.replace(escapeRegExp, '\\$&') .replace(optionalParam, '(?:$1)?') .replace(namedParam, function(match, optional) { - return optional ? match : '([^/?]+)'; + if (optional) return match; + paramNames.push(match.slice(1)); + return '([^/?]+)'; }) - .replace(splatParam, '([^?]*?)'); + .replace(splatParam, function(match) { + paramNames.push(match.slice(1)); + return '([^?]*?)'; + }); return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); }, @@ -1526,11 +1555,37 @@ // treated as `null` to normalize cross-browser behavior. _extractParameters: function(route, fragment) { var params = route.exec(fragment).slice(1); - return _.map(params, function(param, i) { + var decodedParams = _.map(params, function(param, i) { // Don't decode the search params. if (i === params.length - 1) return param || null; return param ? decodeURIComponent(param) : null; }); + + if (this.useParamsObject) { + // get routeInfo, which has array of param names + var routeInfo = this._routeInfos[route]; + + var routeData = { + name: routeInfo.name + }; + var routeParams; + + if (routeInfo.isRegExpRoute) { + // if original route is RegExp, everything goes into .params + routeParams = decodedParams; + } + else { + routeParams = _.object(routeInfo.paramNames, decodedParams); + var queryString = decodedParams[decodedParams.length - 1]; + if (queryString) { + routeData.queryString = queryString; + } + } + + return [routeParams, routeData]; + } + + return decodedParams; } }); diff --git a/test/index.html b/test/index.html index 3dad8c679..c632e4171 100644 --- a/test/index.html +++ b/test/index.html @@ -18,6 +18,7 @@ + diff --git a/test/router-with-params-object.js b/test/router-with-params-object.js new file mode 100644 index 000000000..77dc8bb38 --- /dev/null +++ b/test/router-with-params-object.js @@ -0,0 +1,1106 @@ +(function() { + + var router = null; + var location = null; + var lastRoute = null; + var lastArgs = []; + + var onRoute = function(router, route, args) { + lastRoute = route; + lastArgs = args; + }; + + var Location = function(href) { + this.replace(href); + }; + + _.extend(Location.prototype, { + + parser: document.createElement('a'), + + replace: function(href) { + this.parser.href = href; + _.extend(this, _.pick(this.parser, + 'href', + 'hash', + 'host', + 'search', + 'fragment', + 'pathname', + 'protocol' + )); + // In IE, anchor.pathname does not contain a leading slash though + // window.location.pathname does. + if (!/^\//.test(this.pathname)) this.pathname = '/' + this.pathname; + }, + + toString: function() { + return this.href; + } + + }); + + module("Backbone.RouterWithParamsObject", { + + setup: function() { + location = new Location('http://example.com'); + Backbone.history = _.extend(new Backbone.History, {location: location}); + router = new Router({testing: 101}); + Backbone.history.interval = 9; + Backbone.history.start({pushState: false}); + lastRoute = null; + lastArgs = []; + Backbone.history.on('route', onRoute); + }, + + teardown: function() { + Backbone.history.stop(); + Backbone.history.off('route', onRoute); + } + + }); + + var ExternalObject = { + params: 'unset', + + routingFunction: function(params, routeData) { + this.params = params; + } + }; + ExternalObject.routingFunction = _.bind(ExternalObject.routingFunction, ExternalObject); + + var Router = Backbone.Router.extend({ + useParamsObject: true, + + count: 0, + + routes: { + "noCallback": "noCallback", + "counter": "counter", + "search/:query": "search", + "search/:query/p:page": "search", + "charñ": "charUTF", + "char%C3%B1": "charEscaped", + "contacts": "contacts", + "contacts/new": "newContact", + "contacts/:id": "loadContact", + "route-event/:arg": "routeEvent", + "optional(/:item)": "optionalItem", + "named/optional/(y:z)": "namedOptional", + "splat/*args/end": "splat", + ":repo/compare/*from...*to": "github", + "decode/:named/*splat": "decode", + "*first/complex-*part/*rest": "complex", + "query/:entity": "query", + "function/:value": ExternalObject.routingFunction, + "*anything": "anything" + }, + + initialize : function(options) { + this.testing = options.testing; + this.route('implicit', 'implicit'); + }, + + counter: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + this.count++; + }, + + implicit: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + this.count++; + }, + + search: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + }, + + charUTF: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + this.charType = 'UTF'; + }, + + charEscaped: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + this.charType = 'escaped'; + }, + + contacts: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + this.contact = 'index'; + }, + + newContact: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + this.contact = 'new'; + }, + + loadContact: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + this.contact = 'load'; + }, + + optionalItem: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + }, + + splat: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + }, + + github: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + }, + + complex: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + }, + + query: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + }, + + anything: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + }, + + namedOptional: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + }, + + decode: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + }, + + routeEvent: function(params, routeData) { + this.routeParams = params; + this.routeData = routeData; + } + + }); + + test("initialize", 1, function() { + equal(router.testing, 101); + }); + + test("routes (simple)", 2, function() { + location.replace('http://example.com#search/news'); + Backbone.history.checkUrl(); + deepEqual(router.routeParams, { + query: "news" + }); + deepEqual(router.routeData, { + name: "search" + }); + }); + + test("routes (simple, but unicode)", 2, function() { + location.replace('http://example.com#search/тест'); + Backbone.history.checkUrl(); + deepEqual(router.routeParams, { + query: "тест" + }); + deepEqual(router.routeData, { + name: "search" + }); + }); + + test("routes (two part)", 2, function() { + location.replace('http://example.com#search/nyc/p10'); + Backbone.history.checkUrl(); + deepEqual(router.routeParams, { + query: "nyc", + page: "10" + }); + deepEqual(router.routeData, { + name: "search" + }); + }); + + test("routes via navigate", 2, function() { + Backbone.history.navigate('search/manhattan/p20', {trigger: true}); + deepEqual(router.routeParams, { + query: "manhattan", + page: "20" + }); + deepEqual(router.routeData, { + name: "search" + }); + }); + + test("routes via navigate with params", 2, function() { + Backbone.history.navigate('query/test?a=b', {trigger: true}); + deepEqual(router.routeParams, { + entity: "test" + }); + deepEqual(router.routeData, { + name: "query", + queryString: "a=b" + }); + }); + + test("routes via navigate for backwards-compatibility", function() { + Backbone.history.navigate('search/manhattan/p20', true); + deepEqual(router.routeParams, { + query: "manhattan", + page: "20" + }); + deepEqual(router.routeData, { + name: "search" + }); + }); + + test("reports matched route via nagivate", 1, function() { + ok(Backbone.history.navigate('search/manhattan/p20', true)); + }); + + test("route precedence via navigate", 6, function(){ + // check both 0.9.x and backwards-compatibility options + _.each([ { trigger: true }, true ], function( options ){ + Backbone.history.navigate('contacts', options); + equal(router.contact, 'index'); + Backbone.history.navigate('contacts/new', options); + equal(router.contact, 'new'); + Backbone.history.navigate('contacts/foo', options); + equal(router.contact, 'load'); + }); + }); + + test("loadUrl is not called for identical routes.", 0, function() { + Backbone.history.loadUrl = function(){ ok(false); }; + location.replace('http://example.com#route'); + Backbone.history.navigate('route'); + Backbone.history.navigate('/route'); + Backbone.history.navigate('/route'); + }); + + test("use implicit callback if none provided", 1, function() { + router.count = 0; + router.navigate('implicit', {trigger: true}); + equal(router.count, 1); + }); + + test("routes via navigate with {replace: true}", 1, function() { + location.replace('http://example.com#start_here'); + Backbone.history.checkUrl(); + location.replace = function(href) { + strictEqual(href, new Location('http://example.com#end_here').href); + }; + Backbone.history.navigate('end_here', {replace: true}); + }); + + test("routes (splats)", 2, function() { + location.replace('http://example.com#splat/long-list/of/splatted_99args/end'); + Backbone.history.checkUrl(); + deepEqual(router.routeParams, { + args: "long-list/of/splatted_99args" + }); + deepEqual(router.routeData, { + name: "splat" + }); + }); + + test("routes (github)", 2, function() { + location.replace('http://example.com#backbone/compare/1.0...braddunbar:with/slash'); + Backbone.history.checkUrl(); + deepEqual(router.routeParams, { + repo: "backbone", + from: "1.0", + to: "braddunbar:with/slash" + }); + deepEqual(router.routeData, { + name: "github" + }); + }); + + test("routes (optional)", 4, function() { + location.replace('http://example.com#optional'); + Backbone.history.checkUrl(); + deepEqual(router.routeParams, { + item: null + }); + deepEqual(router.routeData, { + name: "optionalItem" + }); + location.replace('http://example.com#optional/thing'); + Backbone.history.checkUrl(); + deepEqual(router.routeParams, { + item: "thing" + }); + deepEqual(router.routeData, { + name: "optionalItem" + }); + }); + + test("routes (complex)", 2, function() { + location.replace('http://example.com#one/two/three/complex-part/four/five/six/seven'); + Backbone.history.checkUrl(); + deepEqual(router.routeParams, { + first: "one/two/three", + part: "part", + rest: "four/five/six/seven" + }); + deepEqual(router.routeData, { + name: 'complex' + }); + + }); + + test("routes (query)", 2, function() { + location.replace('http://example.com#query/mandel?a=b&c=d'); + Backbone.history.checkUrl(); + deepEqual(router.routeParams, { + entity: "mandel" + }); + deepEqual(router.routeData, { + name: "query", + queryString: "a=b&c=d" + }); + }); + + test("routes (anything)", 2, function() { + location.replace('http://example.com#doesnt-match-a-route'); + Backbone.history.checkUrl(); + deepEqual(router.routeParams, { + anything: "doesnt-match-a-route" + }); + deepEqual(router.routeData, { + name: "anything" + }); + }); + + test("routes (function)", 3, function() { + router.on('route', function(name) { + ok(name === ''); + }); + equal(ExternalObject.params, 'unset'); + location.replace('http://example.com#function/set'); + Backbone.history.checkUrl(); + equal(ExternalObject.params.value, 'set'); + }); + + test("routes (regexp)", 2, function() { + router.route(/^(.*?)\/open$/, function(params, routeData) { + deepEqual(params, ["117-a/b/c"]); + deepEqual(routeData, { + name: "" + }); + }); + location.replace('http://example.com#117-a/b/c/open'); + Backbone.history.checkUrl(); + }); + + test("Decode named parameters, not splats.", 2, function() { + location.replace('http://example.com#decode/a%2Fb/c%2Fd/e'); + Backbone.history.checkUrl(); + deepEqual(router.routeParams, { + named: 'a/b', + splat: 'c/d/e' + }); + deepEqual(router.routeData, { + name: "decode" + }); + }); + + test("fires event when router doesn't have callback on it", 1, function() { + router.on("route:noCallback", function(){ ok(true); }); + location.replace('http://example.com#noCallback'); + Backbone.history.checkUrl(); + }); + + test("No events are triggered if #execute returns false.", 1, function() { + var Router = Backbone.Router.extend({ + + routes: { + foo: function() { + ok(true); + } + }, + + execute: function(callback, args) { + callback.apply(this, args); + return false; + } + + }); + + var router = new Router; + + router.on('route route:foo', function() { + ok(false); + }); + + Backbone.history.on('route', function() { + ok(false); + }); + + location.replace('http://example.com#foo'); + Backbone.history.checkUrl(); + }); + + test("#933, #908 - leading slash", 2, function() { + location.replace('http://example.com/root/foo'); + + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.start({root: '/root', hashChange: false, silent: true}); + strictEqual(Backbone.history.getFragment(), 'foo'); + + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.start({root: '/root/', hashChange: false, silent: true}); + strictEqual(Backbone.history.getFragment(), 'foo'); + }); + + test("#967 - Route callback gets passed encoded values.", 2, function() { + var route = 'has%2Fslash/complex-has%23hash/has%20space'; + Backbone.history.navigate(route, {trigger: true}); + deepEqual(router.routeParams, { + first: "has/slash", + part: "has#hash", + rest: "has space" + }); + deepEqual(router.routeData, { + name: "complex" + }); + }); + + test("correctly handles URLs with % (#868)", 2, function() { + location.replace('http://example.com#search/fat%3A1.5%25'); + Backbone.history.checkUrl(); + location.replace('http://example.com#search/fat'); + Backbone.history.checkUrl(); + deepEqual(router.routeParams, { + query: "fat" + }); + deepEqual(router.routeData, { + name: "search" + }); + }); + + test("#2666 - Hashes with UTF8 in them.", 2, function() { + Backbone.history.navigate('charñ', {trigger: true}); + equal(router.charType, 'UTF'); + Backbone.history.navigate('char%C3%B1', {trigger: true}); + equal(router.charType, 'UTF'); + }); + + test("#1185 - Use pathname when hashChange is not wanted.", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/path/name#hash'); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.start({hashChange: false}); + var fragment = Backbone.history.getFragment(); + strictEqual(fragment, location.pathname.replace(/^\//, '')); + }); + + test("#1206 - Strip leading slash before location.assign.", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/root/'); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.start({hashChange: false, root: '/root/'}); + location.assign = function(pathname) { + strictEqual(pathname, '/root/fragment'); + }; + Backbone.history.navigate('/fragment'); + }); + + test("#1387 - Root fragment without trailing slash.", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/root'); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.start({hashChange: false, root: '/root/', silent: true}); + strictEqual(Backbone.history.getFragment(), ''); + }); + + test("#1366 - History does not prepend root to fragment.", 2, function() { + Backbone.history.stop(); + location.replace('http://example.com/root/'); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(state, title, url) { + strictEqual(url, '/root/x'); + } + } + }); + Backbone.history.start({ + root: '/root/', + pushState: true, + hashChange: false + }); + Backbone.history.navigate('x'); + strictEqual(Backbone.history.fragment, 'x'); + }); + + test("Normalize root.", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/root'); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(state, title, url) { + strictEqual(url, '/root/fragment'); + } + } + }); + Backbone.history.start({ + pushState: true, + root: '/root', + hashChange: false + }); + Backbone.history.navigate('fragment'); + }); + + test("Normalize root.", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/root#fragment'); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(state, title, url) {}, + replaceState: function(state, title, url) { + strictEqual(url, '/root/fragment'); + } + } + }); + Backbone.history.start({ + pushState: true, + root: '/root' + }); + }); + + test("Normalize root.", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/root'); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.loadUrl = function() { ok(true); }; + Backbone.history.start({ + pushState: true, + root: '/root' + }); + }); + + test("Normalize root - leading slash.", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/root'); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(){}, + replaceState: function(){} + } + }); + Backbone.history.start({root: 'root'}); + strictEqual(Backbone.history.root, '/root/'); + }); + + test("Transition from hashChange to pushState.", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/root#x/y'); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(){}, + replaceState: function(state, title, url){ + strictEqual(url, '/root/x/y'); + } + } + }); + Backbone.history.start({ + root: 'root', + pushState: true + }); + }); + + test("#1619: Router: Normalize empty root", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/'); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(){}, + replaceState: function(){} + } + }); + Backbone.history.start({root: ''}); + strictEqual(Backbone.history.root, '/'); + }); + + test("#1619: Router: nagivate with empty root", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/'); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(state, title, url) { + strictEqual(url, '/fragment'); + } + } + }); + Backbone.history.start({ + pushState: true, + root: '', + hashChange: false + }); + Backbone.history.navigate('fragment'); + }); + + test("Transition from pushState to hashChange.", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/root/x/y?a=b'); + location.replace = function(url) { + strictEqual(url, '/root#x/y?a=b'); + }; + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: null, + replaceState: null + } + }); + Backbone.history.start({ + root: 'root', + pushState: true + }); + }); + + test("#1695 - hashChange to pushState with search.", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/root#x/y?a=b'); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(){}, + replaceState: function(state, title, url){ + strictEqual(url, '/root/x/y?a=b'); + } + } + }); + Backbone.history.start({ + root: 'root', + pushState: true + }); + }); + + test("#1746 - Router allows empty route.", 1, function() { + var Router = Backbone.Router.extend({ + routes: {'': 'empty'}, + empty: function(){}, + route: function(route){ + strictEqual(route, ''); + } + }); + new Router; + }); + + test("#1794 - Trailing space in fragments.", 1, function() { + var history = new Backbone.History; + strictEqual(history.getFragment('fragment '), 'fragment'); + }); + + test("#1820 - Leading slash and trailing space.", 1, function() { + var history = new Backbone.History; + strictEqual(history.getFragment('/fragment '), 'fragment'); + }); + + test("#1980 - Optional parameters.", 2, function() { + location.replace('http://example.com#named/optional/y'); + Backbone.history.checkUrl(); + strictEqual(router.routeParams.z, undefined); + location.replace('http://example.com#named/optional/y123'); + Backbone.history.checkUrl(); + strictEqual(router.routeParams.z, '123'); + }); + + test("#2062 - Trigger 'route' event on router instance.", 2, function() { + router.on('route', function(name, args) { + strictEqual(name, 'routeEvent'); + deepEqual(args, [ + { + arg: 'x' + }, + { + name: 'routeEvent' + } + ]); + }); + location.replace('http://example.com#route-event/x'); + Backbone.history.checkUrl(); + }); + + test("#2255 - Extend routes by making routes a function.", 1, function() { + var RouterBase = Backbone.Router.extend({ + routes: function() { + return { + home: "root", + index: "index.html" + }; + } + }); + + var RouterExtended = RouterBase.extend({ + routes: function() { + var _super = RouterExtended.__super__.routes; + return _.extend(_super(), + { show: "show", + search: "search" }); + } + }); + + var router = new RouterExtended(); + deepEqual({home: "root", index: "index.html", show: "show", search: "search"}, router.routes); + }); + + test("#2538 - hashChange to pushState only if both requested.", 0, function() { + Backbone.history.stop(); + location.replace('http://example.com/root?a=b#x/y'); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(){}, + replaceState: function(){ ok(false); } + } + }); + Backbone.history.start({ + root: 'root', + pushState: true, + hashChange: false + }); + }); + + test('No hash fallback.', 0, function() { + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(){}, + replaceState: function(){} + } + }); + + var Router = Backbone.Router.extend({ + routes: { + hash: function() { ok(false); } + } + }); + var router = new Router; + + location.replace('http://example.com/'); + Backbone.history.start({ + pushState: true, + hashChange: false + }); + location.replace('http://example.com/nomatch#hash'); + Backbone.history.checkUrl(); + }); + + test('#2656 - No trailing slash on root.', 1, function() { + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(state, title, url){ + strictEqual(url, '/root'); + } + } + }); + location.replace('http://example.com/root/path'); + Backbone.history.start({pushState: true, hashChange: false, root: 'root'}); + Backbone.history.navigate(''); + }); + + test('#2656 - No trailing slash on root.', 1, function() { + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(state, title, url) { + strictEqual(url, '/'); + } + } + }); + location.replace('http://example.com/path'); + Backbone.history.start({pushState: true, hashChange: false}); + Backbone.history.navigate(''); + }); + + test('#2656 - No trailing slash on root.', 1, function() { + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(state, title, url){ + strictEqual(url, '/root?x=1'); + } + } + }); + location.replace('http://example.com/root/path'); + Backbone.history.start({pushState: true, hashChange: false, root: 'root'}); + Backbone.history.navigate('?x=1'); + }); + + test('#2765 - Fragment matching sans query/hash.', 2, function() { + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(state, title, url) { + strictEqual(url, '/path?query#hash'); + } + } + }); + + var Router = Backbone.Router.extend({ + routes: { + path: function() { ok(true); } + } + }); + var router = new Router; + + location.replace('http://example.com/'); + Backbone.history.start({pushState: true, hashChange: false}); + Backbone.history.navigate('path?query#hash', true); + }); + + test('Do not decode the search params.', function() { + var Router = Backbone.Router.extend({ + routes: { + path: function(params){ + strictEqual(params, 'x=y%3Fz'); + } + } + }); + var router = new Router; + Backbone.history.navigate('path?x=y%3Fz', true); + }); + + test('Navigate to a hash url.', function() { + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.start({pushState: true}); + var Router = Backbone.Router.extend({ + routes: { + path: function(params) { + strictEqual(params, 'x=y'); + } + } + }); + var router = new Router; + location.replace('http://example.com/path?x=y#hash'); + Backbone.history.checkUrl(); + }); + + test('#navigate to a hash url.', function() { + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.start({pushState: true}); + var Router = Backbone.Router.extend({ + routes: { + path: function(params) { + strictEqual(params, 'x=y'); + } + } + }); + var router = new Router; + Backbone.history.navigate('path?x=y#hash', true); + }); + + test('unicode pathname', 1, function() { + location.replace('http://example.com/myyjä'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + var Router = Backbone.Router.extend({ + routes: { + myyjä: function() { + ok(true); + } + } + }); + new Router; + Backbone.history.start({pushState: true}); + }); + + test('unicode pathname with % in a parameter', 1, function() { + location.replace('http://example.com/myyjä/foo%20%25%3F%2f%40%25%20bar'); + location.pathname = '/myyj%C3%A4/foo%20%25%3F%2f%40%25%20bar'; + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + var Router = Backbone.Router.extend({ + routes: { + 'myyjä/:query': function(query) { + strictEqual(query, 'foo %?/@% bar'); + } + } + }); + new Router; + Backbone.history.start({pushState: true}); + }); + + test('newline in route', 1, function() { + location.replace('http://example.com/stuff%0Anonsense?param=foo%0Abar'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + var Router = Backbone.Router.extend({ + routes: { + 'stuff\nnonsense': function() { + ok(true); + } + } + }); + new Router; + Backbone.history.start({pushState: true}); + }); + + test('Router#execute receives callback, args, name.', 3, function() { + location.replace('http://example.com#foo/123/bar?x=y'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + var Router = Backbone.Router.extend({ + routes: {'foo/:id/bar': 'foo'}, + foo: function(){}, + execute: function(callback, args, name) { + strictEqual(callback, this.foo); + deepEqual(args, ['123', 'x=y']); + strictEqual(name, 'foo'); + } + }); + var router = new Router; + Backbone.history.start(); + }); + + test("pushState to hashChange with only search params.", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com?a=b'); + location.replace = function(url) { + strictEqual(url, '/#?a=b'); + }; + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: null + }); + Backbone.history.start({pushState: true}); + }); + + test("#3123 - History#navigate decodes before comparison.", 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/shop/search?keyword=short%20dress'); + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: function(){ ok(false); }, + replaceState: function(){ ok(false); } + } + }); + Backbone.history.start({pushState: true}); + Backbone.history.navigate('shop/search?keyword=short%20dress', true); + strictEqual(Backbone.history.fragment, 'shop/search?keyword=short dress'); + }); + + test('#3175 - Urls in the params', 1, function() { + Backbone.history.stop(); + location.replace('http://example.com#login?a=value&backUrl=https%3A%2F%2Fwww.msn.com%2Fidp%2Fidpdemo%3Fspid%3Dspdemo%26target%3Db'); + Backbone.history = _.extend(new Backbone.History, {location: location}); + var router = new Backbone.Router; + router.route('login', function(params) { + strictEqual(params, 'a=value&backUrl=https%3A%2F%2Fwww.msn.com%2Fidp%2Fidpdemo%3Fspid%3Dspdemo%26target%3Db'); + }); + Backbone.history.start(); + }); + + test('#3358 - pushState to hashChange transition with search params', 1, function() { + Backbone.history.stop(); + location.replace('http://example.com/root?foo=bar'); + location.replace = function(url) { + strictEqual(url, '/root#?foo=bar'); + }; + Backbone.history = _.extend(new Backbone.History, { + location: location, + history: { + pushState: undefined, + replaceState: undefined + } + }); + Backbone.history.start({root: '/root', pushState: true}); + }); + + test("Paths that don't match the root should not match no root", 0, function() { + location.replace('http://example.com/foo'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + var Router = Backbone.Router.extend({ + routes: { + foo: function(){ + ok(false, 'should not match unless root matches'); + } + } + }); + var router = new Router; + Backbone.history.start({root: 'root', pushState: true}); + }); + + test("Paths that don't match the root should not match roots of the same length", 0, function() { + location.replace('http://example.com/xxxx/foo'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + var Router = Backbone.Router.extend({ + routes: { + foo: function(){ + ok(false, 'should not match unless root matches'); + } + } + }); + var router = new Router; + Backbone.history.start({root: 'root', pushState: true}); + }); + + test("roots with regex characters", 1, function() { + location.replace('http://example.com/x+y.z/foo'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + var Router = Backbone.Router.extend({ + routes: {foo: function(){ ok(true); }} + }); + var router = new Router; + Backbone.history.start({root: 'x+y.z', pushState: true}); + }); + + test("roots with unicode characters", 1, function() { + location.replace('http://example.com/®ooτ/foo'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + var Router = Backbone.Router.extend({ + routes: {foo: function(){ ok(true); }} + }); + var router = new Router; + Backbone.history.start({root: '®ooτ', pushState: true}); + }); + + test("roots without slash", 1, function() { + location.replace('http://example.com/®ooτ'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + var Router = Backbone.Router.extend({ + routes: {'': function(){ ok(true); }} + }); + var router = new Router; + Backbone.history.start({root: '®ooτ', pushState: true}); + }); + +})(); \ No newline at end of file