From 7ab7ff5128c9c9037802170761e248a758016b51 Mon Sep 17 00:00:00 2001
From: Isiah Meadows
diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 1f7fc1d9a..5aed53bde 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -7,6 +7,7 @@ var throttleMocker = require("../../test-utils/throttleMock") var m = require("../../render/hyperscript") var callAsync = require("../../test-utils/callAsync") +var apiMount = require("../../api/mount") var apiRedraw = require("../../api/redraw") var apiRouter = require("../../api/router") var Promise = require("../../promise/promise") @@ -24,7 +25,7 @@ o.spec("route", function() { root = $window.document.body redrawService = apiRedraw($window, throttleMock.throttle) - route = apiRouter($window, redrawService) + route = apiRouter($window, redrawService, apiMount(redrawService)) route.prefix(prefix) }) @@ -55,6 +56,165 @@ o.spec("route", function() { o(root.firstChild.nodeName).equals("DIV") }) + o("resolves to route w/ escaped unicode", function() { + $window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6" + route(root, "/ö", { + "/ö" : { + view: function() { + return m("div") + } + } + }) + + o(root.firstChild.nodeName).equals("DIV") + }) + + o("resolves to route w/ unicode", function() { + $window.location.href = prefix + "/ö?ö=ö" + route(root, "/ö", { + "/ö" : { + view: function() { + return JSON.stringify(route.param()) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals('{"ö":"ö"} /ö?ö=ö') + }) + + o("handles parameterized route", function() { + $window.location.href = prefix + "/test/x" + route(root, "/test/:a", { + "/test/:a" : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals( + '{"a":"x"} {"a":"x"} /test/x' + ) + }) + + o("handles multi-parameterized route", function() { + $window.location.href = prefix + "/test/x/y" + route(root, "/test/:a/:b", { + "/test/:a/:b" : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals( + '{"a":"x","b":"y"} {"a":"x","b":"y"} /test/x/y' + ) + }) + + o("handles rest parameterized route", function() { + $window.location.href = prefix + "/test/x/y" + route(root, "/test/:a...", { + "/test/:a..." : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals( + '{"a":"x/y"} {"a":"x/y"} /test/x/y' + ) + }) + + o("handles route with search", function() { + $window.location.href = prefix + "/test?a=b&c=d" + route(root, "/test", { + "/test" : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals( + '{"a":"b","c":"d"} {"a":"b","c":"d"} /test?a=b&c=d' + ) + }) + + o("redirects to default route if no match", function(done) { + $window.location.href = prefix + "/test" + route(root, "/other", { + "/other": { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + callAsync(function() { + o(root.firstChild.nodeValue).equals("{} {} /other") + done() + }) + }) + + o("handles out of order routes", function() { + $window.location.href = prefix + "/z/y/x" + + route(root, "/z/y/x", { + "/z/y/x": { + view: function() { return "1" }, + }, + "/:a...": { + view: function() { return "2" }, + }, + }) + + o(root.firstChild.nodeValue).equals("1") + }) + + o("handles reverse out of order routes", function() { + $window.location.href = prefix + "/z/y/x" + + route(root, "/z/y/x", { + "/:a...": { + view: function() { return "2" }, + }, + "/z/y/x": { + view: function() { return "1" }, + }, + }) + + o(root.firstChild.nodeValue).equals("2") + }) + + o("resolves to route on fallback mode", function() { + $window.location.href = "file://" + prefix + "/test" + + route(root, "/test", { + "/test" : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals("{} {} /test") + }) + o("routed mount points only redraw asynchronously (POJO component)", function() { var view = o.spy() diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js new file mode 100644 index 000000000..57ec0c406 --- /dev/null +++ b/api/tests/test-routerGetSet.js @@ -0,0 +1,281 @@ +"use strict" + +var o = require("../../ospec/ospec") +var callAsync = require("../../test-utils/callAsync") +var browserMock = require("../../test-utils/browserMock") +var throttleMocker = require("../../test-utils/throttleMock") + +var callAsync = require("../../test-utils/callAsync") +var apiMount = require("../../api/mount") +var apiRedraw = require("../../api/redraw") +var apiRouter = require("../../api/router") + +o.spec("route.get/route.set", function() { + void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { + void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { + o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { + var $window, root, redrawService, route, throttleMock + + o.beforeEach(function() { + $window = browserMock(env) + throttleMock = throttleMocker() + + root = $window.document.body + + redrawService = apiRedraw($window, throttleMock.throttle) + route = apiRouter($window, redrawService, apiMount(redrawService)) + route.prefix(prefix) + }) + + o.afterEach(function() { + o(throttleMock.queueLength()).equals(0) + }) + + o("gets route", function() { + $window.location.href = prefix + "/test" + route(root, "/test", {"/test": {view: function() {}}}) + + o(route.get()).equals("/test") + }) + + o("gets route w/ params", function() { + $window.location.href = prefix + "/other/x/y/z?c=d#e=f" + + route(root, "/other/x/y/z?c=d#e=f", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + o(route.get()).equals("/other/x/y/z?c=d#e=f") + }) + + o("gets route w/ escaped unicode", function() { + $window.location.href = prefix + encodeURI("/ö/é/å?ö=ö#ö=ö") + + route(root, "/ö/é/å?ö=ö#ö=ö", { + "/test": {view: function() {}}, + "/ö/:a/:b...": {view: function() {}}, + }) + + o(route.get()).equals("/ö/é/å?ö=ö#ö=ö") + }) + + o("gets route w/ unicode", function() { + $window.location.href = prefix + "/ö/é/å?ö=ö#ö=ö" + + route(root, "/ö/é/å?ö=ö#ö=ö", { + "/test": {view: function() {}}, + "/ö/:a/:b...": {view: function() {}}, + }) + + o(route.get()).equals("/ö/é/å?ö=ö#ö=ö") + }) + + o("sets path asynchronously", function(done) { + $window.location.href = prefix + "/a" + var spy1 = o.spy() + var spy2 = o.spy() + + route(root, "/a", { + "/a": {view: spy1}, + "/b": {view: spy2}, + }) + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(0) + route.set("/b") + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(0) + callAsync(function() { + throttleMock.fire() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + done() + }) + }) + + o("sets fallback asynchronously", function(done) { + $window.location.href = prefix + "/b" + var spy1 = o.spy() + var spy2 = o.spy() + + route(root, "/a", { + "/a": {view: spy1}, + "/b": {view: spy2}, + }) + + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(1) + route.set("/c") + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(1) + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/b") + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/a") + throttleMock.fire() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + done() + }) + }) + }) + + o("exposes new route asynchronously", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + route.set("/other/x/y/z?c=d#e=f") + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y/z?c=d#e=f") + throttleMock.fire() + done() + }) + }) + + o("exposes new escaped unicode route asynchronously", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/ö": {view: function() {}}, + }) + + route.set(encodeURI("/ö?ö=ö#ö=ö")) + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/ö?ö=ö#ö=ö") + throttleMock.fire() + done() + }) + }) + + o("exposes new unescaped unicode route asynchronously", function(done) { + $window.location.href = "file://" + prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/ö": {view: function() {}}, + }) + + route.set("/ö?ö=ö#ö=ö") + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/ö?ö=ö#ö=ö") + throttleMock.fire() + done() + }) + }) + + o("exposes new route asynchronously on fallback mode", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + route.set("/other/x/y/z?c=d#e=f") + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y/z?c=d#e=f") + throttleMock.fire() + done() + }) + }) + + o("sets route via pushState/onpopstate", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + callAsync(function() { + $window.history.pushState(null, null, prefix + "/other/x/y/z?c=d#e=f") + $window.onpopstate() + + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y/z?c=d#e=f") + throttleMock.fire() + + done() + }) + }) + }) + + o("sets parameterized route", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + route.set("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"}) + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y%2Fz?c=d&e=f") + throttleMock.fire() + done() + }) + }) + + o("replace:true works", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other": {view: function() {}}, + }) + + route.set("/other", null, {replace: true}) + + callAsync(function() { + throttleMock.fire() + $window.history.back() + o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + "/") + done() + }) + }) + + o("replace:false works", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other": {view: function() {}}, + }) + + route.set("/other", null, {replace: false}) + + callAsync(function() { + throttleMock.fire() + $window.history.back() + var slash = prefix[0] === "/" ? "" : "/" + o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") + done() + }) + }) + + o("state works", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other": {view: function() {}}, + }) + + route.set("/other", null, {state: {a: 1}}) + callAsync(function() { + throttleMock.fire() + o($window.history.state).deepEquals({a: 1}) + done() + }) + }) + }) + }) + }) +}) diff --git a/route.js b/route.js index 4e8295276..a29cd9f67 100644 --- a/route.js +++ b/route.js @@ -1,5 +1,6 @@ "use strict" var redrawService = require("./redraw") +var mount = require("./mount") -module.exports = require("./api/router")(window, redrawService) +module.exports = require("./api/router")(window, redrawService, mount) diff --git a/router/router.js b/router/router.js deleted file mode 100644 index 37fc29b5b..000000000 --- a/router/router.js +++ /dev/null @@ -1,112 +0,0 @@ -"use strict" - -var buildPathname = require("../pathname/build") -var parsePathname = require("../pathname/parse") -var compileTemplate = require("../pathname/compileTemplate") -var assign = require("../pathname/assign") - -module.exports = function($window) { - var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout - var supportsPushState = typeof $window.history.pushState === "function" - var fireAsync - - return { - prefix: "#!", - - getPath: function() { - // Consider the pathname holistically. The prefix might even be invalid, - // but that's not our problem. - var prefix = $window.location.hash - if (this.prefix[0] !== "#") { - prefix = $window.location.search + prefix - if (this.prefix[0] !== "?") { - prefix = $window.location.pathname + prefix - if (prefix[0] !== "/") prefix = "/" + prefix - } - } - // This seemingly useless `.concat()` speeds up the tests quite a bit, - // since the representation is consistently a relatively poorly - // optimized cons string. - return prefix.concat() - .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent) - .slice(this.prefix.length) - }, - - setPath: function(path, data, options) { - path = buildPathname(path, data) - if (fireAsync != null) { - fireAsync() - var state = options ? options.state : null - var title = options ? options.title : null - if (options && options.replace) $window.history.replaceState(state, title, this.prefix + path) - else $window.history.pushState(state, title, this.prefix + path) - } - else { - $window.location.href = this.prefix + path - } - }, - - defineRoutes: function(routes, resolve, reject, defaultRoute, subscribe) { - var self = this - var compiled = Object.keys(routes).map(function(route) { - if (route[0] !== "/") throw new SyntaxError("Routes must start with a `/`") - if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) { - throw new SyntaxError("Route parameter names must be separated with either `/`, `.`, or `-`") - } - return { - route: route, - component: routes[route], - check: compileTemplate(route), - } - }) - var unsubscribe, asyncId - - fireAsync = null - - if (defaultRoute != null) { - var defaultData = parsePathname(defaultRoute) - - if (!compiled.some(function (i) { return i.check(defaultData) })) { - throw new ReferenceError("Default route doesn't match any known routes") - } - } - - function resolveRoute() { - var path = self.getPath() - var data = parsePathname(path) - - assign(data.params, $window.history.state) - - for (var i = 0; i < compiled.length; i++) { - if (compiled[i].check(data)) { - resolve(compiled[i].component, data.params, path, compiled[i].route) - return - } - } - - reject(path, data.params) - } - - if (supportsPushState) { - unsubscribe = function() { - $window.removeEventListener("popstate", fireAsync, false) - } - $window.addEventListener("popstate", fireAsync = function() { - if (asyncId) return - asyncId = callAsync(function() { - asyncId = null - resolveRoute() - }) - }, false) - } else if (this.prefix[0] === "#") { - unsubscribe = function() { - $window.removeEventListener("hashchange", resolveRoute, false) - } - $window.addEventListener("hashchange", resolveRoute, false) - } - - subscribe(unsubscribe) - resolveRoute() - }, - } -} diff --git a/router/tests/index.html b/router/tests/index.html deleted file mode 100644 index 1f186449c..000000000 --- a/router/tests/index.html +++ /dev/null @@ -1,26 +0,0 @@ - - -
- - -
-