mithril-vndb/test/mithril.route.js
2015-12-20 09:14:28 -05:00

1240 lines
26 KiB
JavaScript

describe("m.route()", function () {
"use strict"
// Use this instead of m.route() unless you have to call m.route and do
// something else in the same frame.
function route() {
var res = m.route.apply(null, arguments)
mock.requestAnimationFrame.$resolve()
return res
}
var mode = (function () {
var types = {
search: "?",
hash: "#",
pathname: "/"
}
return function (type) {
if (!{}.hasOwnProperty.call(types, type)) {
throw new RangeError("bad mode type")
}
mock.location[type] = types[type]
m.route.mode = type
}
})()
// Little helper utility
function noop() {}
// Use this if all you need to do is render a view (i.e. a pure component).
function pure(view) {
return {
controller: noop,
view: view
}
}
// Use these instead of `it` and `xit` in this set of tests if you need a
// root element.
var dit = makeIt(it)
var xdit = makeIt(xit)
// Wraps the `it` function for dependency injection that doesn't require
// `this`
/* eslint-disable no-invalid-this */
function makeIt(it) {
return function (name, callback) {
return it(name, function () {
var args = [this.root]
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i])
}
callback.apply(null, args)
})
}
}
beforeEach(function () {
mock.requestAnimationFrame.$resolve()
this.root = mock.document.createElement("div")
})
afterEach(function () {
m.mount(this.root, null)
})
/* eslint-enable no-invalid-this */
it("exists", function () {
expect(m.route).to.be.a("function")
})
dit("routes to the right location by default", function (root) {
mode("search")
route(root, "/test1", {
"/test1": pure(function () { return "foo" })
})
expect(mock.location.search).to.equal("?/test1")
expect(root.childNodes[0].nodeValue).to.equal("foo")
})
dit("gets the right right location when routed to it", function (root) {
mode("search")
var route1, route2
route(root, "/", {
"/": {
controller: function () { route1 = m.route() },
view: noop
},
"/test13": {
controller: function () { route2 = m.route() },
view: noop
}
})
m.route("/test13")
expect(route1).to.equal("/")
expect(route2).to.equal("/test13")
})
// FIXME: this causes others to fail
xdit("skips route change if component ctrl.onunload calls preventDefault", function (root) { // eslint-disable-line
mode("search")
var spy = sinon.spy()
var sub = {
controller: function () {
this.onunload = function (e) { e.preventDefault() }
},
view: function () {
return m("div")
}
}
route(root, "/a", {
"/a": pure(function () { return sub }),
"/b": {
controller: spy,
view: noop
}
})
route("/b")
expect(spy).to.not.have.been.called
})
// FIXME: this causes others to fail
xdit("skips route change if subcomponent ctrl.onunload calls preventDefault", function (root) { // eslint-disable-line
mode("search")
var spy = sinon.spy()
var subsub = {
controller: function () {
this.onunload = function (e) { e.preventDefault() }
},
view: function () {
return m("div")
}
}
var sub = pure(function () { return subsub })
route(root, "/a", {
"/a": pure(function () { return sub }),
"/b": {
controller: spy,
view: noop
}
})
route("/b")
expect(spy).to.not.have.been.called
})
// FIXME: this causes others to fail
xdit("skips route change if non-curried component ctrl.onunload calls preventDefault", function (root) { // eslint-disable-line
mode("search")
var spy = sinon.spy()
var sub = {
controller: function () {
this.onunload = function (e) { e.preventDefault() }
},
view: function () {
return m("div")
}
}
route(root, "/a", {
"/a": pure(function () { return sub }),
"/b": {
controller: spy,
view: noop
}
})
route("/b")
expect(spy).to.not.have.been.called
})
// FIXME: this causes others to fail
xdit("skips route change if non-curried subcomponent ctrl.onunload calls preventDefault", function (root) { // eslint-disable-line
mode("search")
var spy = sinon.spy()
var subsub = {
controller: function () {
this.onunload = function (e) { e.preventDefault() }
},
view: function () {
return m("div")
}
}
var sub = pure(function () { return subsub })
route(root, "/a", {
"/a": pure(function () { return sub }),
"/b": {
controller: spy,
view: noop
}
})
route("/b")
expect(spy).to.not.have.been.called
})
dit("initializes a component's constructor on route change", function (root) { // eslint-disable-line
mode("search")
var ctrl1 = sinon.spy()
var ctrl2 = sinon.spy()
var sub1 = {
controller: ctrl1,
view: function () { return m("div") }
}
var sub2 = {
controller: ctrl2,
view: function () { return m("div") }
}
route(root, "/a", {
"/a": pure(function () {
return m(".page-a", [
m("h1"), m.component(sub1, {x: 11})
])
}),
"/b": pure(function () {
return m(".page-b", [
m("h2"), m.component(sub2, {y: 22})
])
})
})
route("/b")
route("/a")
expect(ctrl1).to.have.been.calledTwice
expect(ctrl2).to.have.been.calledOnce
})
dit("doesn't require components to have a view", function (root) {
mode("search")
var Component = pure(function () { return m(".comp") })
route(root, "/foo", {
"/foo": pure(function () { return [Component] })
})
expect(root.childNodes[0].nodeName).to.equal("DIV")
})
// https://github.com/lhorie/mithril.js/issues/555
dit("reinstantiates the controller when redraw strategy is `all`", function (root) { // eslint-disable-line
var MyComponent = {
controller: function (args) {
this.name = args.name
},
view: function (ctrl) {
return m("div", ctrl.name)
}
}
route(root, "/", {
"/": pure(function () {
return m("div", [
m("a[href=/]", {config: m.route}, "foo"),
m("a[href=/bar]", {config: m.route}, "bar"),
m.component(MyComponent, {name: "Jane"})
])
}),
"/bar": pure(function () {
return m("div", [
m("a[href=/]", {config: m.route}, "foo"),
m("a[href=/bar]", {config: m.route}, "bar"),
m.component(MyComponent, {name: "Bob"})
])
})
})
route("/bar")
expect(root.childNodes[0].childNodes[2].childNodes[0].nodeValue)
.to.equal("Bob")
})
dit("sets the correct href with config: m.route", function (root) {
mode("pathname")
route(root, "/test2", {
"/test2": pure(function () {
return [
"foo",
m("a", {href: "/test2", config: m.route}, "Test2")
]
})
})
expect(mock.location.pathname).to.equal("/test2")
expect(root.childNodes[0].nodeValue).to.equal("foo")
expect(root.childNodes[1].href).to.equal("/test2")
})
dit("can use a hash", function (root) {
mode("hash")
route(root, "/test3", {
"/test3": pure(function () { return "foo" })
})
expect(mock.location.hash).to.equal("#/test3")
expect(root.childNodes[0].nodeValue).to.equal("foo")
})
dit("can use a query", function (root) {
mode("search")
route(root, "/test4/foo", {
"/test4/:test": pure(function () { return m.route.param("test") })
})
expect(mock.location.search).to.equal("?/test4/foo")
expect(root.childNodes[0].nodeValue).to.equal("foo")
})
context("m.route.param()", function () {
it("exists", function () {
expect(m.route.param).to.be.a("function")
})
dit("can get params (1)", function (root) {
mode("search")
var component = pure(function () { return m.route.param("test") })
m.route(root, "/test5/foo", {
"/": component,
"/test5/:test": component
})
var paramValueBefore = m.route.param("test")
mock.requestAnimationFrame.$resolve()
m.route("/")
var paramValueAfter = m.route.param("test")
mock.requestAnimationFrame.$resolve()
expect(mock.location.search).to.equal("?/")
expect(paramValueBefore).to.equal("foo")
expect(paramValueAfter).to.not.exist
})
dit("can deal with params (2)", function (root) {
mode("search")
var component = pure(function () { return m.route.param("a1") })
m.route(root, "/test6/foo", {
"/": component,
"/test6/:a1": component
})
var paramValueBefore = m.route.param("a1")
mock.requestAnimationFrame.$resolve()
m.route("/")
var paramValueAfter = m.route.param("a1")
mock.requestAnimationFrame.$resolve()
expect(mock.location.search).to.equal("?/")
expect(paramValueBefore).to.equal("foo")
expect(paramValueAfter).to.not.exist
})
// https://github.com/lhorie/mithril.js/issues/61
dit("can get the route via m.route()", function (root) {
mode("search")
var component = pure(function () { return m.route.param("a1") })
m.route(root, "/test7/foo", {
"/": component,
"/test7/:a1": component
})
var routeValueBefore = m.route()
mock.requestAnimationFrame.$resolve()
m.route("/")
var routeValueAfter = m.route()
mock.requestAnimationFrame.$resolve()
expect(routeValueBefore).to.equal("/test7/foo")
expect(routeValueAfter).to.equal("/")
})
dit("can deal with rest paths at the end", function (root) {
mode("search")
route(root, "/test8/foo/SEP/bar/baz", {
"/test8/:test/SEP/:path...": pure(function () {
return m.route.param("test") + "_" + m.route.param("path")
})
})
expect(mock.location.search).to.equal("?/test8/foo/SEP/bar/baz")
expect(root.childNodes[0].nodeValue).to.equal("foo_bar/baz")
})
dit("can deal with rest paths in the middle", function (root) {
mode("search")
route(root, "/test9/foo/bar/SEP/baz", {
"/test9/:test.../SEP/:path": pure(function () {
return m.route.param("test") + "_" + m.route.param("path")
})
})
expect(mock.location.search).to.equal("?/test9/foo/bar/SEP/baz")
expect(root.childNodes[0].nodeValue).to.equal("foo/bar_baz")
})
dit("unescapes urls for m.route.param()", function (root) {
mode("search")
route(root, "/test10/foo%20bar", {
"/test10/:test": pure(function () {
return m.route.param("test")
})
})
expect(root.childNodes[0].nodeValue).to.equal("foo bar")
})
dit("renders the correct path", function (root) {
mode("search")
route(root, "/", {
"/": pure(function () { return "foo" }),
"/test11": pure(function () { return "bar" })
})
route("/test11/")
expect(mock.location.search).to.equal("?/test11/")
expect(root.childNodes[0].nodeValue).to.equal("bar")
})
dit("reads params by parsing query string", function (root) {
mode("search")
route(root, "/", {
"/": pure(noop),
"/test12": pure(noop)
})
route("/test12?a=foo&b=bar")
expect(mock.location.search).to.equal("?/test12?a=foo&b=bar")
expect(m.route.param("a")).to.equal("foo")
expect(m.route.param("b")).to.equal("bar")
})
dit("prefers local params to global params", function (root) {
mode("search")
route(root, "/", {
"/": pure(function () { return "bar" }),
"/test13/:test": pure(function () {
return m.route.param("test")
})
})
route("/test13/foo?test=bar")
expect(mock.location.search).to.equal("?/test13/foo?test=bar")
expect(root.childNodes[0].nodeValue).to.equal("foo")
})
dit("reads global params", function (root) {
mode("search")
route(root, "/", {
"/": pure(function () { return "bar" }),
"/test14": pure(function () { return "foo" })
})
route("/test14?test&test2=")
expect(mock.location.search).to.equal("?/test14?test&test2=")
expect(m.route.param("test")).to.not.exist
expect(m.route.param("test2")).to.equal("")
})
dit("parses params when using m.route(path, params)", function (root) {
mode("search")
route(root, "/", {
"/": pure(noop),
"/test12": pure(noop)
})
route("/test12", {a: "foo", b: "bar"})
expect(mock.location.search).to.equal("?/test12?a=foo&b=bar")
expect(m.route.param("a")).to.equal("foo")
expect(m.route.param("b")).to.equal("bar")
})
dit("gets params object by using m.route.param()", function (root) {
mode("search")
route(root, "/", {
"/": pure(noop),
"/test12": pure(noop)
})
route("/test12", {a: "foo", b: "bar"})
var params = m.route.param()
expect(params.a).to.equal("foo")
expect(params.b).to.equal("bar")
})
})
dit("only calls onunload once when routed away (1)", function (root) {
mode("search")
var onunload = sinon.spy()
route(root, "/", {
"/": pure(function () {
return m("div", {
config: function (el, init, ctx) {
ctx.onunload = onunload
}
})
}),
"/test14": pure(noop)
})
route("/test14")
expect(onunload).to.be.calledOnce
})
dit("only calls onunload once when routed away (2)", function (root) {
mode("search")
var onunload = sinon.spy()
route(root, "/", {
"/": pure(function () {
return [
m("div"),
m("div", {
config: function (el, init, ctx) {
ctx.onunload = onunload
}
})
]
}),
"/test15": pure(function () { return [m("div")] })
})
route("/test15")
expect(onunload).to.be.calledOnce
})
dit("only calls onunload once when routed away (3)", function (root) {
mode("search")
var onunload = sinon.spy()
route(root, "/", {
"/": pure(function () {
return m("div", {
config: function (el, init, ctx) {
ctx.onunload = onunload
}
})
}),
"/test16": pure(function () { return m("a") })
})
route("/test16")
expect(onunload).to.be.calledOnce
})
dit("only calls onunload once when routed away (4)", function (root) {
mode("search")
var onunload = sinon.spy()
route(root, "/", {
"/": pure(function () {
return [
m("div", {
config: function (el, init, ctx) {
ctx.onunload = onunload
}
})
]
}),
"/test17": pure(function () { return m("a") })
})
route("/test17")
expect(onunload).to.be.calledOnce
})
dit("only calls onunload once when routed away (5)", function (root) {
mode("search")
var onunload = sinon.spy()
route(root, "/", {
"/": pure(function () {
return m("div", {
config: function (el, init, ctx) {
ctx.onunload = onunload
}
})
}),
"/test18": pure(function () { return [m("a")] })
})
route("/test18")
expect(onunload).to.be.calledOnce
})
dit("only calls onunload once when routed away (6)", function (root) {
mode("search")
var onunload = sinon.spy()
route(root, "/", {
"/": pure(function () {
return [
m("div", {
key: 1,
config: function (el, init, ctx) {
ctx.onunload = onunload
}
})
]
}),
"/test20": pure(function () {
return [
m("div", {
key: 2,
config: function (el, init, ctx) {
ctx.onunload = onunload
}
})
]
})
})
route("/test20")
expect(onunload).to.be.calledOnce
})
dit("only calls onunload once when routed away (7)", function (root) {
mode("search")
var onunload = sinon.spy()
route(root, "/", {
"/": pure(function () {
return [
m("div", {
key: 1,
config: function (el, init, ctx) {
ctx.onunload = onunload
}
})
]
}),
"/test21": pure(function () {
return [
m("div", {
config: function (el, init, ctx) {
ctx.onunload = onunload
}
})
]
})
})
route("/test21")
expect(onunload).to.be.calledOnce
})
dit("renders the right virtual node when routed to it", function (root) {
mode("search")
route(root, "/foo", {
"/foo": pure(function () { return m("div", "foo") }),
"/bar": pure(function () { return m("div", "bar") })
})
var foo = root.childNodes[0].childNodes[0].nodeValue
route("/bar")
var bar = root.childNodes[0].childNodes[0].nodeValue
expect(foo).to.equal("foo")
expect(bar).to.equal("bar")
})
dit("keeps identity with unchanged nodes", function (root) {
mode("search")
var onunload = sinon.spy()
function config(el, init, ctx) {
ctx.onunload = onunload
}
route(root, "/foo1", {
"/foo1": pure(function () {
return m("div", m("a", {config: config}, "foo"))
}),
"/bar1": pure(function () {
return m("main", m("a", {config: config}, "foo"))
})
})
route("/bar1")
expect(onunload).to.be.calledOnce
})
dit("allows illegal URL characters in paths", function (root) {
mode("search")
var value
m.route(root, "/foo+bar", {
"/:arg": {
controller: function () { value = m.route.param("arg") },
view: function () {
return ""
}
}
})
expect(value).to.equal("foo+bar")
})
dit("allows trailing slashes in paths", function (root) {
mode("search")
route(root, "/", {
"/": pure(function () { return "foo" }),
"/test22": pure(function () { return "bar" })
})
m.route("/test22/")
expect(mock.location.search).to.equal("?/test22/")
expect(root.childNodes[0].nodeValue).to.equal("bar")
})
dit("reads non-primitive String objects in route changes", function (root) {
mode("search")
route(root, "/", {
"/": pure(function () { return "foo" }),
"/test23": pure(function () { return "bar" })
})
route(new String("/test23/")) // eslint-disable-line no-new-wrappers
expect(mock.location.search).to.equal("?/test23/")
expect(root.childNodes[0].nodeValue).to.equal("bar")
})
dit("reads primitive Strings in default routes", function (root) {
mode("search")
var value
m.route(root, "/foo+bar", {
"/:arg": {
controller: function () { value = m.route.param("arg") },
view: function () {
return ""
}
}
})
expect(value).to.equal("foo+bar")
})
dit("reads non-primitive Strings in default routes", function (root) {
mode("search")
var value
m.route(root, new String("/foo+bar"), { // eslint-disable-line
"/:arg": {
controller: function () { value = m.route.param("arg") },
view: function () {
return ""
}
}
})
expect(value).to.equal("foo+bar")
})
dit("can redirect to another route while loading default", function (root) { // eslint-disable-line
mode("search")
route(root, "/a", {
"/a": {
controller: function () { m.route("/b") },
view: function () { return "a" }
},
"/b": pure(function () { return "b" })
})
expect(root.childNodes[0].nodeValue).to.equal("b")
})
dit("can redirect to another route with params while loading default", function (root) { // eslint-disable-line
mode("search")
route(root, "/", {
"/": {
controller: function () {
m.route("/b?foo=1", {foo: 2})
},
view: function () { return "a" }
},
"/b": pure(function () { return "b" })
})
expect(mock.location.search).to.equal("?/b?foo=2")
})
dit("modifies history when changing route", function (root) {
mode("search")
mock.history.$$length = 0
route(root, "/a", {
"/a": pure(function () { return "a" }),
"/b": pure(function () { return "b" })
})
route("/b")
expect(mock.history.$$length).to.equal(1)
})
dit("doesn't modify history when redirecting to same route", function (root) { // eslint-disable-line
mode("search")
mock.history.$$length = 0
route(root, "/a", {
"/a": pure(function () { return "a" }),
"/b": pure(function () { return "b" })
})
route("/a")
expect(mock.history.$$length).to.equal(0)
})
context("m.route.strategy() === \"all\", identical views", function () {
context("parent nodes", function () {
dit("renders routes independently", function (root) {
mode("search")
var initCount = 0
var a = pure(function () {
return m("a", {
config: function (el, init) {
if (!init) initCount++
}
})
})
route(root, "/a", {
"/a": a,
"/b": pure(a.view)
})
route("/b")
expect(initCount).to.equal(2)
})
dit("renders routes independently with `context.retain === false`", function (root) { // eslint-disable-line
mode("search")
var initCount = 0
var a = pure(function () {
return m("a", {
config: function (el, init, ctx) {
ctx.retain = false
if (!init) initCount++
}
})
})
route(root, "/a", {
"/a": a,
"/b": pure(a.view)
})
route("/b")
expect(initCount).to.equal(2)
})
dit("renders routes independently with `context.retain === true`", function (root) { // eslint-disable-line
mode("search")
var initCount = 0
var a = pure(function () {
return m("a", {
config: function (el, init, ctx) {
ctx.retain = true
if (!init) initCount++
}
})
})
route(root, "/a", {
"/a": a,
"/b": pure(a.view)
})
route("/b")
expect(initCount).to.equal(1)
})
})
context("child nodes", function () {
dit("reinitialize unchanged child nodes without `context.retain`", function (root) { // eslint-disable-line
mode("search")
var initCount = 0
function config(el, init) {
if (!init) initCount++
}
route(root, "/a", {
"/a": pure(function () {
return m("div", m("a", {config: config}))
}),
"/b": pure(function () {
return m("section", m("a", {config: config}))
})
})
route("/b")
expect(initCount).to.equal(2)
})
dit("doesn't reinitialize unchanged child nodes with `context.retain === true`", function (root) { // eslint-disable-line
mode("search")
var initCount = 0
function config(el, init, ctx) {
ctx.retain = true
if (!init) initCount++
}
route(root, "/a", {
"/a": pure(function () {
return m("div", m("a", {config: config}))
}),
"/b": pure(function () {
return m("section", m("a", {config: config}))
})
})
route("/b")
expect(initCount).to.equal(1)
})
dit("reinitializes unchanged child nodes with `context.retain === false`", function (root) { // eslint-disable-line
mode("search")
var initCount = 0
function config(el, init, ctx) {
ctx.retain = false
if (!init) initCount++
}
route(root, "/a", {
"/a": pure(function () {
return m("div", m("a", {config: config}))
}),
"/b": pure(function () {
return m("section", m("a", {config: config}))
})
})
route("/b")
expect(initCount).to.equal(2)
})
})
})
context("m.route.strategy() === \"diff\"", function () {
function diff(view) {
return {
controller: function () { m.redraw.strategy("diff") },
view: view
}
}
context("parent nodes", function () {
dit("renders routes independently", function (root) {
mode("search")
var initCount = 0
var a = diff(function () {
return m("a", {
config: function (el, init) {
if (!init) initCount++
}
})
})
route(root, "/a", {
"/a": a,
"/b": diff(a.view)
})
route("/b")
expect(initCount).to.equal(1)
})
dit("renders routes independently with `context.retain === false`", function (root) { // eslint-disable-line
mode("search")
var initCount = 0
var a = diff(function () {
return m("a", {
config: function (el, init, ctx) {
ctx.retain = true
if (!init) initCount++
}
})
})
route(root, "/a", {
"/a": a,
"/b": diff(a.view)
})
route("/b")
expect(initCount).to.equal(1)
})
dit("renders routes independently with `context.retain === true`", function (root) { // eslint-disable-line
mode("search")
var initCount = 0
var a = diff(function () {
return m("a", {
config: function (el, init, ctx) {
ctx.retain = false
if (!init) initCount++
}
})
})
route(root, "/a", {
"/a": a,
"/b": diff(a.view)
})
route("/b")
expect(initCount).to.equal(2)
})
})
context("child nodes", function () {
dit("reinitialize unchanged child nodes without `context.retain`", function (root) { // eslint-disable-line
mode("search")
var initCount = 0
function config(el, init) {
if (!init) initCount++
}
route(root, "/a", {
"/a": diff(function () {
return m("div", m("a", {config: config}))
}),
"/b": diff(function () {
return m("section", m("a", {config: config}))
})
})
route("/b")
expect(initCount).to.equal(1)
})
dit("doesn't reinitialize unchanged child nodes with `context.retain === true`", function (root) { // eslint-disable-line
mode("search")
var initCount = 0
function config(el, init, ctx) {
ctx.retain = true
if (!init) initCount++
}
route(root, "/a", {
"/a": diff(function () {
return m("div", m("a", {config: config}))
}),
"/b": diff(function () {
return m("section", m("a", {config: config}))
})
})
route("/b")
expect(initCount).to.equal(1)
})
dit("reinitializes unchanged child nodes with `context.retain === false`", function (root) { // eslint-disable-line
mode("search")
var initCount = 0
function config(el, init, ctx) {
ctx.retain = false
if (!init) initCount++
}
route(root, "/a", {
"/a": diff(function () {
return m("div", m("a", {config: config}))
}),
"/b": diff(function () {
return m("section", m("a", {config: config}))
})
})
m.route("/b")
expect(initCount).to.equal(2)
})
})
})
dit("honors retain flag inside child components during route change", function (root) { // eslint-disable-line
mode("search")
var initCount = 0
function config(el, init, ctx) {
ctx.retain = true
if (!init) initCount++
}
var a = pure(function () {
return m("div", m("a", {config: config}))
})
var b = {
controller: function () { m.redraw.strategy("diff") },
view: function () {
return m("section", m("a", {config: config}))
}
}
route(root, "/a", {
"/a": pure(function () { return m("div", a) }),
"/b": pure(function () { return m("div", b) })
})
route("/b")
expect(initCount).to.equal(1)
})
// https://github.com/lhorie/mithril.js/pull/571
dit("clears nodes with config on route change", function (root) {
mode("search")
route(root, "/a", {
"/a": pure(function () {
return m("div", {
config: function (el) {
el.childNodes[0].modified = true
}
}, m("div"))
}),
"/b": pure(function () { return m("div", m("div")) })
})
route("/b")
expect(root.childNodes[0].childNodes[0])
.to.not.have.property("modified")
})
})