Deservicify core (#2458)

* De-servicify router (mostly)

Still uses the redraw service, but it no longer has an intermediate
service of its own.

Also, did a *lot* of test deduplication in this. About 30-40% of the
router service tests were already tested on the main router API instance
itself.

Bundle size decreased from 9560 to 9548 bytes min+gzip.

* Merge `m.mount` + `m.redraw`, update router

Simplifies the router and redraw mechanism, and makes it much easier to
keep predictable.

Bundle size down to 9433 bytes min+gzip, docs updated accordingly.

* Make `mithril/render` just return the `m.render` function directly.

* Deservicify `m.render`, revise `m.route`

- You now have to use `mithril/render/render` directly if you want an
  implicit redraw function. (This will likely be going away in v3.)
- Revise `m.route` to only `key` components

* Add `redraw` to `m.render`, deservicify requests

* Test error logging

* Update docs + changelog [skip ci]
This commit is contained in:
Isiah Meadows 2019-07-07 18:28:43 -04:00 committed by GitHub
parent db277217f8
commit 1f4b2cf49a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1212 additions and 1393 deletions

View file

@ -24,13 +24,11 @@
<script src="../../querystring/build.js"></script>
<script src="../../querystring/parse.js"></script>
<script src="../../request/request.js"></script>
<script src="../../router/router.js"></script>
<script src="../../api/redraw.js"></script>
<script src="../../api/mount.js"></script>
<script src="../../api/mount-redraw.js"></script>
<script src="../../api/router.js"></script>
<script src="./test-redraw.js"></script>
<script src="./test-mount.js"></script>
<script src="./test-mountRedraw.js"></script>
<script src="./test-router.js"></script>
<script src="./test-routerGetSet.js"></script>
<script>require("../../ospec/ospec").run()</script>
</body>

View file

@ -1,274 +0,0 @@
"use strict"
var o = require("../../ospec/ospec")
var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
var throttleMocker = require("../../test-utils/throttleMock")
var m = require("../../render/hyperscript")
var apiRedraw = require("../../api/redraw")
var apiMounter = require("../../api/mount")
o.spec("mount", function() {
var $window, root, redrawService, mount, render, throttleMock
o.beforeEach(function() {
$window = domMock()
throttleMock = throttleMocker()
root = $window.document.body
redrawService = apiRedraw($window, throttleMock.throttle)
mount = apiMounter(redrawService)
render = redrawService.render
})
o.afterEach(function() {
o(throttleMock.queueLength()).equals(0)
})
o("throws on invalid component", function() {
var threw = false
try {
mount(root, {})
} catch (e) {
threw = true
}
o(threw).equals(true)
})
components.forEach(function(cmp){
o.spec(cmp.kind, function(){
var createComponent = cmp.create
o("throws on invalid `root` DOM node", function() {
var threw = false
try {
mount(null, createComponent({view: function() {}}))
} catch (e) {
threw = true
}
o(threw).equals(true)
})
o("renders into `root` synchronoulsy", function() {
mount(root, createComponent({
view : function() {
return m("div")
}
}))
o(root.firstChild.nodeName).equals("DIV")
})
o("mounting null unmounts", function() {
mount(root, createComponent({
view : function() {
return m("div")
}
}))
mount(root, null)
o(root.childNodes.length).equals(0)
})
o("Mounting a second root doesn't cause the first one to redraw", function() {
var view = o.spy(function() {
return m("div")
})
render(root, [
m("#child0"),
m("#child1")
])
mount(root.childNodes[0], createComponent({
view : view
}))
o(root.firstChild.nodeName).equals("DIV")
o(view.callCount).equals(1)
mount(root.childNodes[1], createComponent({
view : function() {
return m("div")
}
}))
o(view.callCount).equals(1)
throttleMock.fire()
o(view.callCount).equals(1)
})
o("redraws on events", function() {
var onupdate = o.spy()
var oninit = o.spy()
var onclick = o.spy()
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
mount(root, createComponent({
view : function() {
return m("div", {
oninit : oninit,
onupdate : onupdate,
onclick : onclick,
})
}
}))
root.firstChild.dispatchEvent(e)
o(oninit.callCount).equals(1)
o(onupdate.callCount).equals(0)
o(onclick.callCount).equals(1)
o(onclick.this).equals(root.firstChild)
o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild)
throttleMock.fire()
o(onupdate.callCount).equals(1)
})
o("redraws several mount points on events", function() {
var onupdate0 = o.spy()
var oninit0 = o.spy()
var onclick0 = o.spy()
var onupdate1 = o.spy()
var oninit1 = o.spy()
var onclick1 = o.spy()
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
render(root, [
m("#child0"),
m("#child1")
])
mount(root.childNodes[0], createComponent({
view : function() {
return m("div", {
oninit : oninit0,
onupdate : onupdate0,
onclick : onclick0,
})
}
}))
o(oninit0.callCount).equals(1)
o(onupdate0.callCount).equals(0)
mount(root.childNodes[1], createComponent({
view : function() {
return m("div", {
oninit : oninit1,
onupdate : onupdate1,
onclick : onclick1,
})
}
}))
o(oninit1.callCount).equals(1)
o(onupdate1.callCount).equals(0)
root.childNodes[0].firstChild.dispatchEvent(e)
o(onclick0.callCount).equals(1)
o(onclick0.this).equals(root.childNodes[0].firstChild)
throttleMock.fire()
o(onupdate0.callCount).equals(1)
o(onupdate1.callCount).equals(1)
root.childNodes[1].firstChild.dispatchEvent(e)
o(onclick1.callCount).equals(1)
o(onclick1.this).equals(root.childNodes[1].firstChild)
throttleMock.fire()
o(onupdate0.callCount).equals(2)
o(onupdate1.callCount).equals(2)
})
o("event handlers can skip redraw", function() {
var onupdate = o.spy(function(){
throw new Error("This shouldn't have been called")
})
var oninit = o.spy()
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
mount(root, createComponent({
view: function() {
return m("div", {
oninit: oninit,
onupdate: onupdate,
onclick: function(e) {
e.redraw = false
}
})
}
}))
root.firstChild.dispatchEvent(e)
o(oninit.callCount).equals(1)
throttleMock.fire()
o(onupdate.callCount).equals(0)
})
o("redraws when the render function is run", function() {
var onupdate = o.spy()
var oninit = o.spy()
mount(root, createComponent({
view : function() {
return m("div", {
oninit: oninit,
onupdate: onupdate
})
}
}))
o(oninit.callCount).equals(1)
o(onupdate.callCount).equals(0)
redrawService.redraw()
throttleMock.fire()
o(onupdate.callCount).equals(1)
})
o("throttles", function() {
var i = 0
mount(root, createComponent({view: function() {i++}}))
var before = i
redrawService.redraw()
redrawService.redraw()
redrawService.redraw()
redrawService.redraw()
var after = i
throttleMock.fire()
o(before).equals(1) // mounts synchronously
o(after).equals(1) // throttles rest
o(i).equals(2)
})
})
})
})

View file

@ -0,0 +1,424 @@
"use strict"
// Low-priority TODO: remove the dependency on the renderer here.
var o = require("../../ospec/ospec")
var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
var throttleMocker = require("../../test-utils/throttleMock")
var mountRedraw = require("../../api/mount-redraw")
var coreRenderer = require("../../render/render")
var h = require("../../render/hyperscript")
o.spec("mount/redraw", function() {
var root, m, throttleMock, consoleMock, $document, errors
o.beforeEach(function() {
var $window = domMock()
consoleMock = {error: o.spy()}
throttleMock = throttleMocker()
root = $window.document.body
m = mountRedraw(coreRenderer($window), throttleMock.schedule, consoleMock)
$document = $window.document
errors = []
})
o.afterEach(function() {
o(consoleMock.error.calls.map(function(c) {
return c.args[0]
})).deepEquals(errors)
o(throttleMock.queueLength()).equals(0)
})
o("shouldn't error if there are no renderers", function() {
m.redraw()
throttleMock.fire()
})
o("schedules correctly", function() {
var spy = o.spy()
m.mount(root, {view: spy})
o(spy.callCount).equals(1)
m.redraw()
o(spy.callCount).equals(1)
throttleMock.fire()
o(spy.callCount).equals(2)
})
o("should run a single renderer entry", function() {
var spy = o.spy()
m.mount(root, {view: spy})
o(spy.callCount).equals(1)
m.redraw()
m.redraw()
m.redraw()
o(spy.callCount).equals(1)
throttleMock.fire()
o(spy.callCount).equals(2)
})
o("should run all renderer entries", function() {
var el1 = $document.createElement("div")
var el2 = $document.createElement("div")
var el3 = $document.createElement("div")
var spy1 = o.spy()
var spy2 = o.spy()
var spy3 = o.spy()
m.mount(el1, {view: spy1})
m.mount(el2, {view: spy2})
m.mount(el3, {view: spy3})
m.redraw()
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
m.redraw()
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
throttleMock.fire()
o(spy1.callCount).equals(2)
o(spy2.callCount).equals(2)
o(spy3.callCount).equals(2)
})
o("should stop running after mount null", function() {
var spy = o.spy()
m.mount(root, {view: spy})
o(spy.callCount).equals(1)
m.mount(root, null)
m.redraw()
o(spy.callCount).equals(1)
throttleMock.fire()
o(spy.callCount).equals(1)
})
o("should stop running after mount undefined", function() {
var spy = o.spy()
m.mount(root, {view: spy})
o(spy.callCount).equals(1)
m.mount(root, undefined)
m.redraw()
o(spy.callCount).equals(1)
throttleMock.fire()
o(spy.callCount).equals(1)
})
o("should stop running after mount no arg", function() {
var spy = o.spy()
m.mount(root, {view: spy})
o(spy.callCount).equals(1)
m.mount(root)
m.redraw()
o(spy.callCount).equals(1)
throttleMock.fire()
o(spy.callCount).equals(1)
})
o("should invoke remove callback on unmount", function() {
var spy = o.spy()
var onremove = o.spy()
m.mount(root, {view: spy, onremove: onremove})
o(spy.callCount).equals(1)
m.mount(root)
o(spy.callCount).equals(1)
o(onremove.callCount).equals(1)
})
o("should stop running after unsubscribe, even if it occurs after redraw is requested", function() {
var spy = o.spy()
m.mount(root, {view: spy})
o(spy.callCount).equals(1)
m.redraw()
m.mount(root)
o(spy.callCount).equals(1)
throttleMock.fire()
o(spy.callCount).equals(1)
})
o("does nothing on invalid unmount", function() {
var spy = o.spy()
m.mount(root, {view: spy})
o(spy.callCount).equals(1)
m.mount(null)
m.redraw()
throttleMock.fire()
o(spy.callCount).equals(2)
})
o("redraw.sync() redraws all roots synchronously", function() {
var el1 = $document.createElement("div")
var el2 = $document.createElement("div")
var el3 = $document.createElement("div")
var spy1 = o.spy()
var spy2 = o.spy()
var spy3 = o.spy()
m.mount(el1, {view: spy1})
m.mount(el2, {view: spy2})
m.mount(el3, {view: spy3})
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
m.redraw.sync()
o(spy1.callCount).equals(2)
o(spy2.callCount).equals(2)
o(spy3.callCount).equals(2)
m.redraw.sync()
o(spy1.callCount).equals(3)
o(spy2.callCount).equals(3)
o(spy3.callCount).equals(3)
})
o("throws on invalid component", function() {
o(function() { m.mount(root, {}) }).throws(TypeError)
})
components.forEach(function(cmp){
o.spec(cmp.kind, function(){
var createComponent = cmp.create
o("throws on invalid `root` DOM node", function() {
o(function() {
m.mount(null, createComponent({view: function() {}}))
}).throws(TypeError)
})
o("renders into `root` synchronously", function() {
m.mount(root, createComponent({
view: function() {
return h("div")
}
}))
o(root.firstChild.nodeName).equals("DIV")
})
o("mounting null unmounts", function() {
m.mount(root, createComponent({
view: function() {
return h("div")
}
}))
m.mount(root, null)
o(root.childNodes.length).equals(0)
})
o("Mounting a second root doesn't cause the first one to redraw", function() {
var root1 = $document.createElement("div")
var root2 = $document.createElement("div")
var view = o.spy()
m.mount(root1, createComponent({view: view}))
o(view.callCount).equals(1)
m.mount(root2, createComponent({view: function() {}}))
o(view.callCount).equals(1)
throttleMock.fire()
o(view.callCount).equals(1)
})
o("redraws on events", function() {
var onupdate = o.spy()
var oninit = o.spy()
var onclick = o.spy()
var e = $document.createEvent("MouseEvents")
e.initEvent("click", true, true)
m.mount(root, createComponent({
view: function() {
return h("div", {
oninit: oninit,
onupdate: onupdate,
onclick: onclick,
})
}
}))
root.firstChild.dispatchEvent(e)
o(oninit.callCount).equals(1)
o(onupdate.callCount).equals(0)
o(onclick.callCount).equals(1)
o(onclick.this).equals(root.firstChild)
o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild)
throttleMock.fire()
o(onupdate.callCount).equals(1)
})
o("redraws several mount points on events", function() {
var onupdate0 = o.spy()
var oninit0 = o.spy()
var onclick0 = o.spy()
var onupdate1 = o.spy()
var oninit1 = o.spy()
var onclick1 = o.spy()
var root1 = $document.createElement("div")
var root2 = $document.createElement("div")
var e = $document.createEvent("MouseEvents")
e.initEvent("click", true, true)
m.mount(root1, createComponent({
view: function() {
return h("div", {
oninit: oninit0,
onupdate: onupdate0,
onclick: onclick0,
})
}
}))
o(oninit0.callCount).equals(1)
o(onupdate0.callCount).equals(0)
m.mount(root2, createComponent({
view: function() {
return h("div", {
oninit: oninit1,
onupdate: onupdate1,
onclick: onclick1,
})
}
}))
o(oninit1.callCount).equals(1)
o(onupdate1.callCount).equals(0)
root1.firstChild.dispatchEvent(e)
o(onclick0.callCount).equals(1)
o(onclick0.this).equals(root1.firstChild)
throttleMock.fire()
o(onupdate0.callCount).equals(1)
o(onupdate1.callCount).equals(1)
root2.firstChild.dispatchEvent(e)
o(onclick1.callCount).equals(1)
o(onclick1.this).equals(root2.firstChild)
throttleMock.fire()
o(onupdate0.callCount).equals(2)
o(onupdate1.callCount).equals(2)
})
o("event handlers can skip redraw", function() {
var onupdate = o.spy(function(){
throw new Error("This shouldn't have been called")
})
var oninit = o.spy()
var e = $document.createEvent("MouseEvents")
e.initEvent("click", true, true)
m.mount(root, createComponent({
view: function() {
return h("div", {
oninit: oninit,
onupdate: onupdate,
onclick: function(e) {
e.redraw = false
}
})
}
}))
root.firstChild.dispatchEvent(e)
o(oninit.callCount).equals(1)
o(e.redraw).equals(false)
throttleMock.fire()
o(onupdate.callCount).equals(0)
o(e.redraw).equals(false)
})
o("redraws when the render function is run", function() {
var onupdate = o.spy()
var oninit = o.spy()
m.mount(root, createComponent({
view: function() {
return h("div", {
oninit: oninit,
onupdate: onupdate
})
}
}))
o(oninit.callCount).equals(1)
o(onupdate.callCount).equals(0)
m.redraw()
throttleMock.fire()
o(onupdate.callCount).equals(1)
})
o("emits errors correctly", function() {
errors = ["foo", "bar", "baz"]
var counter = -1
m.mount(root, createComponent({
view: function() {
var value = errors[counter++]
if (value != null) throw value
return null
}
}))
m.redraw()
throttleMock.fire()
m.redraw()
throttleMock.fire()
m.redraw()
throttleMock.fire()
})
})
})
})

View file

@ -1,195 +0,0 @@
"use strict"
var o = require("../../ospec/ospec")
var domMock = require("../../test-utils/domMock")
var throttleMocker = require("../../test-utils/throttleMock")
var apiRedraw = require("../../api/redraw")
// Because Node doesn't have this.
if (typeof requestAnimationFrame !== "function") {
global.requestAnimationFrame = (function (delay, last) {
return function(callback) {
var elapsed = Date.now() - last
return setTimeout(function() {
callback()
last = Date.now()
}, delay - elapsed)
}
})(16, 0)
}
o.spec("redrawService", function() {
var root, redrawService, $document
o.beforeEach(function() {
var $window = domMock()
root = $window.document.body
redrawService = apiRedraw($window)
$document = $window.document
})
o("shouldn't error if there are no renderers", function() {
redrawService.redraw()
})
o("honours throttleMock", function() {
var throttleMock = throttleMocker()
redrawService = apiRedraw(domMock(), throttleMock.throttle)
var spy = o.spy()
redrawService.subscribe(root, spy)
o(spy.callCount).equals(1)
redrawService.redraw()
o(spy.callCount).equals(1)
throttleMock.fire()
o(spy.callCount).equals(2)
})
o("should run a single renderer entry", function(done) {
var spy = o.spy()
redrawService.subscribe(root, spy)
o(spy.callCount).equals(1)
redrawService.redraw()
redrawService.redraw()
redrawService.redraw()
o(spy.callCount).equals(1)
setTimeout(function() {
o(spy.callCount).equals(2)
done()
}, 20)
})
o("should run all renderer entries", function(done) {
var el1 = $document.createElement("div")
var el2 = $document.createElement("div")
var el3 = $document.createElement("div")
var spy1 = o.spy()
var spy2 = o.spy()
var spy3 = o.spy()
redrawService.subscribe(el1, spy1)
redrawService.subscribe(el2, spy2)
redrawService.subscribe(el3, spy3)
redrawService.redraw()
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
redrawService.redraw()
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
setTimeout(function() {
o(spy1.callCount).equals(2)
o(spy2.callCount).equals(2)
o(spy3.callCount).equals(2)
done()
}, 20)
})
o("should stop running after unsubscribe", function(done) {
var spy = o.spy()
redrawService.subscribe(root, spy)
o(spy.callCount).equals(1)
redrawService.unsubscribe(root)
redrawService.redraw()
o(spy.callCount).equals(1)
setTimeout(function() {
o(spy.callCount).equals(1)
done()
}, 20)
})
o("should invoke remove callback on unsubscribe", function() {
var spy = o.spy()
var onremove = o.spy()
redrawService.subscribe(root, spy, onremove)
o(spy.callCount).equals(1)
redrawService.unsubscribe(root)
o(spy.callCount).equals(1)
o(onremove.callCount).equals(1)
})
o("should stop running after unsubscribe, even if it occurs after redraw is requested", function(done) {
var spy = o.spy()
redrawService.subscribe(root, spy)
o(spy.callCount).equals(1)
redrawService.redraw()
redrawService.unsubscribe(root)
o(spy.callCount).equals(1)
setTimeout(function() {
o(spy.callCount).equals(1)
done()
}, 20)
})
o("does nothing on invalid unsubscribe", function(done) {
var spy = o.spy()
redrawService.subscribe(root, spy)
o(spy.callCount).equals(1)
redrawService.unsubscribe(null)
redrawService.redraw()
setTimeout(function() {
o(spy.callCount).equals(2)
done()
}, 20)
})
o("redraw.sync() redraws all roots synchronously", function() {
var el1 = $document.createElement("div")
var el2 = $document.createElement("div")
var el3 = $document.createElement("div")
var spy1 = o.spy()
var spy2 = o.spy()
var spy3 = o.spy()
redrawService.subscribe(el1, spy1)
redrawService.subscribe(el2, spy2)
redrawService.subscribe(el3, spy3)
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
redrawService.redraw.sync()
o(spy1.callCount).equals(2)
o(spy2.callCount).equals(2)
o(spy3.callCount).equals(2)
redrawService.redraw.sync()
o(spy1.callCount).equals(3)
o(spy2.callCount).equals(3)
o(spy3.callCount).equals(3)
})
})

View file

@ -1,13 +1,15 @@
"use strict"
// Low-priority TODO: remove the dependency on the renderer here.
var o = require("../../ospec/ospec")
var callAsync = require("../../test-utils/callAsync")
var browserMock = require("../../test-utils/browserMock")
var throttleMocker = require("../../test-utils/throttleMock")
var m = require("../../render/hyperscript")
var coreRenderer = require("../../render/render")
var callAsync = require("../../test-utils/callAsync")
var apiRedraw = require("../../api/redraw")
var apiMountRedraw = require("../../api/mount-redraw")
var apiRouter = require("../../api/router")
var Promise = require("../../promise/promise")
@ -15,7 +17,7 @@ o.spec("route", 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
var $window, root, mountRedraw, route, throttleMock
o.beforeEach(function() {
$window = browserMock(env)
@ -23,8 +25,8 @@ o.spec("route", function() {
root = $window.document.body
redrawService = apiRedraw($window, throttleMock.throttle)
route = apiRouter($window, redrawService)
mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console)
route = apiRouter($window, mountRedraw)
route.prefix(prefix)
})
@ -55,6 +57,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()
@ -63,7 +224,7 @@ o.spec("route", function() {
o(view.callCount).equals(1)
redrawService.redraw()
mountRedraw.redraw()
o(view.callCount).equals(1)
@ -83,7 +244,7 @@ o.spec("route", function() {
o(view.callCount).equals(1)
redrawService.redraw()
mountRedraw.redraw()
o(view.callCount).equals(1)
@ -102,7 +263,7 @@ o.spec("route", function() {
o(view.callCount).equals(1)
redrawService.redraw()
mountRedraw.redraw()
o(view.callCount).equals(1)
@ -124,8 +285,7 @@ o.spec("route", function() {
o(root.firstChild.nodeName).equals("DIV")
// unsubscribe as if via `m.mount(root)`
redrawService.unsubscribe(root)
mountRedraw.mount(root)
o(root.childNodes.length).equals(0)
})
@ -192,7 +352,7 @@ o.spec("route", function() {
o(oninit.callCount).equals(1)
redrawService.redraw()
mountRedraw.redraw()
throttleMock.fire()
o(onupdate.callCount).equals(1)
@ -259,8 +419,6 @@ o.spec("route", function() {
root.firstChild.dispatchEvent(e)
o(e.redraw).notEquals(false)
// Wrapped to ensure no redraw fired
callAsync(function() {
o(onupdate.callCount).equals(0)
@ -550,7 +708,7 @@ o.spec("route", function() {
})
})
o("changing `vnode.key` in `render` resets the component", function(done){
o("changing `key` param resets the component", function(done){
var oninit = o.spy()
var Component = {
oninit: oninit,
@ -560,9 +718,7 @@ o.spec("route", function() {
}
$window.location.href = prefix + "/abc"
route(root, "/abc", {
"/:id": {render: function(vnode) {
return m(Component, {key: vnode.attrs.id})
}}
"/:key": Component,
})
callAsync(function() {
o(oninit.callCount).equals(1)
@ -661,7 +817,7 @@ o.spec("route", function() {
o(matchCount).equals(1)
o(renderCount).equals(1)
redrawService.redraw()
mountRedraw.redraw()
throttleMock.fire()
o(matchCount).equals(1)
@ -697,7 +853,7 @@ o.spec("route", function() {
o(matchCount).equals(1)
o(renderCount).equals(1)
redrawService.redraw()
mountRedraw.redraw()
throttleMock.fire()
o(matchCount).equals(1)
@ -1022,7 +1178,7 @@ o.spec("route", function() {
o(view.callCount).equals(1)
o(onmatch.callCount).equals(1)
redrawService.redraw()
mountRedraw.redraw()
throttleMock.fire()
o(view.callCount).equals(2)
@ -1286,10 +1442,10 @@ o.spec("route", function() {
})
var before = i
redrawService.redraw()
redrawService.redraw()
redrawService.redraw()
redrawService.redraw()
mountRedraw.redraw()
mountRedraw.redraw()
mountRedraw.redraw()
mountRedraw.redraw()
var after = i
throttleMock.fire()

View file

@ -0,0 +1,282 @@
"use strict"
// Low-priority TODO: remove the dependency on the renderer here.
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 apiMountRedraw = require("../../api/mount-redraw")
var coreRenderer = require("../../render/render")
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, mountRedraw, route, throttleMock
o.beforeEach(function() {
$window = browserMock(env)
throttleMock = throttleMocker()
root = $window.document.body
mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console)
route = apiRouter($window, mountRedraw)
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()
})
})
})
})
})
})